Compare commits

..
47 Commits
Author SHA1 Message Date
Stefan Haller 6fcb7eb8bb Correctly request force-pushing in a triangular workflow (#3528)
- **PR Description**

Some people push to a different branch (or even remote) than they pull
from. One example is described in #3437. Our logic of when to request a
force push is not appropriate for these workflows: we check the
configured upstream branch for divergence, but that's the one you pull
from. We should instead check the push-to branch for divergence.

Fixes #3437.
2024-05-19 10:00:32 +02:00
Stefan Haller c5cf1b2428 Correctly request force-pushing in triangular workflows
To determine whether we need to ask for force pushing, we need to query the push
branch rather than the upstream branch, in case they are not the same.
2024-05-19 09:44:38 +02:00
Stefan Haller d890c68cd0 Add ahead/behind information for @{push}
In a triangular workflow the branch that you're pulling from is not the same as
the one that you are pushing to. For example, some people find it useful to set
the upstream branch to origin/master so that pulling effectively rebases onto
master, and set the push.default git config to "current" so that "feature"
pushes to origin/feature.

Another example is a fork-based workflow where "feature" has upstream set to
upstream/main, and the repo has remote.pushDefault set to "origin", so pushing
on "feature" pushes to origin/feature.

This commit adds new fields to models.Branch that store the ahead/behind
information against the push branch; for the "normal" workflow where you pull
and push from/to the upstream branch, AheadForPush/BehindForPush will be the
same as AheadForPull/BehindForPull.
2024-05-19 09:44:38 +02:00
Stefan Haller 0aba686f97 Rename Pushables/Pullables to AheadForPull/BehindForPull
In preparation for adding AheadForPush/BehindForPush in the next commit.
2024-05-19 09:44:38 +02:00
Stefan Haller b91b40ba4d Add test demonstrating the problem with force-pushing in a triangular workflow
Our code doesn't realize that we need to prompt the user to force push, when the
branch is up-to-date with its upstream but not with the branch that we're
pushing to.
2024-05-19 09:44:38 +02:00
Stefan Haller c4927e21c5 Rename PushBranch to PushBranchAndSetUpstream
It is unexpected that a function called PushBranch also sets the upstream
branch; also, we want to add a PushBranch function in the next commit that
doesn't.
2024-05-19 09:44:38 +02:00
Stefan Haller 6afcc5bda8 Create shims for all model classes in SessionStateLoader
This guards against accidentally renaming a model field and thereby breaking
user's custom commands. With this change we'll get a build failure when we do
that.
2024-05-19 09:44:38 +02:00
Jesse Duffield 9fc7a5177b Bump go version to 1.22 (#3574)
- **PR Description**

Bumps go from 1.21 to 1.22, and removes newly redundant loop variable
re-declarations now that in 1.22 the variables are automatically
redeclared in each iteration for us.

- **Please check if the PR fulfills these requirements**

* [ ] Cheatsheets are up-to-date (run `go generate ./...`)
* [ ] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] Docs (specifically `docs/Config.md`) have been updated if
necessary
* [ ] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
2024-05-19 17:22:42 +10:00
Jesse Duffield fdff2dec79 Remove redundant variable dedeclarations
In go 1.22, loop variables are redeclared with each iteration of the
loop, rather than simple updated on each iteration. This means that we
no longer need to manually redeclare variables when they're closed over
by a function.
2024-05-19 16:38:21 +10:00
Jesse Duffield 9124d8dbaa Bump go version to 1.22 2024-05-19 16:38:21 +10:00
Jesse Duffield 4404aacdee README.md: Update Sponsors (#3503)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action
2024-05-19 16:15:32 +10:00
github-actions[bot] 8f5ce14dc0 README.md: Update Sponsors 2024-05-19 05:09:25 +00:00
Stefan Haller 866e80529b Delete and edit custom commands history items (#3534)
- **PR Description**

Allow deleting and editing custom command history items. Deleting is
done by hitting `d` on a suggestion; editing is done by hitting `e`,
which fills the selected item into the command prompt for further
editing.

Closes #2528.
2024-05-19 07:09:13 +02:00
Stefan Haller 010b0ae923 Show delete/edit keybindings in suggestions subtitle if available 2024-05-19 07:06:18 +02:00
Stefan Haller a7041cf492 Allow editing a custom command from the suggestions list by pressing 'e'
For custom commands it is useful to select an earlier command and have it copied
to the prompt for further editing. This can be done by hitting 'e' now.

For other types of suggestion panels we don't enable this behavior, as you can't
create arbitrary new items there that don't already exist as a suggestion.
2024-05-19 07:06:18 +02:00
Stefan Haller da3e0f7147 Support deleting items from the custom commands history
In the custom commands panel you can now tab to the suggestions and hit 'd' to
delete items from there. Useful if you mistyped a command and don't want it to
appear in your history any more.
2024-05-19 07:06:18 +02:00
Jesse Duffield 269d01233f Improve nvim-remote mode (#3508)
- If _not_ inside a neovim session, treat as a normal nvim invocation
and suspend lazygit.

- If inside a neovim session:
  - Do not try to suspend lazygit.
  - Send `q` keystroke to neovim session to quit lazygit.
  - Send filename/line/etc. to neovim session.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] Docs (specifically `docs/Config.md`) have been updated if
necessary
* [x] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
2024-05-19 14:42:47 +10:00
Charlie Moog 615ff09afa improve nvim-remote mode
- If _not_ inside a neovim session, treat as
  a normal nvim session and suspend lazygit.

- If inside a neovim session:
  - Do not try to suspend lazygit.
  - Send `q` keystroke to neovim session to quit lazygit.
  - Send filename/line/etc. to neovim session.
2024-05-19 14:39:02 +10:00
Jesse Duffield 772388294a Attempt #2 at preventing codacy coverage step from running on forks (#3572)
- **PR Description**

First attempt didn't work. Let's try this one, based on
https://github.com/orgs/community/discussions/25217

<img width="910" alt="image"
src="https://github.com/jesseduffield/lazygit/assets/8456633/fa2e82b0-db34-486b-8bea-ec3807d10a33">

- **Please check if the PR fulfills these requirements**

* [ ] Cheatsheets are up-to-date (run `go generate ./...`)
* [ ] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] Docs (specifically `docs/Config.md`) have been updated if
necessary
* [ ] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
2024-05-19 14:38:48 +10:00
Jesse Duffield e09b38153c Attempt #2 at preventing codacy coverage step from running on forks 2024-05-19 14:36:25 +10:00
Jesse Duffield cd62c719f1 Only run code coverage report on non-fork branches (#3571)
- **PR Description**

Codacy's coverage report feature requires the use of a secret key, which
is only available on the main repo and is not available on forks. So,
the step has been always failing on any forks. This commit ensures that
we only run it on non-forks.

This greatly diminishes the value of the coverage reports. I've talked
to one of the Codacy people and advised that they should just have an
API key for coverage reports which is not a secret, like what bugsnag
does.

I've disabled the gate in codacy meaning if the coverage ever drops
beneath some percentage, the job won't fail. It wouldn't make sense to
fail the job if some other PR from a fork was responsible for reducing
the coverage percentage beneath some threshold.

- **Please check if the PR fulfills these requirements**

* [ ] Cheatsheets are up-to-date (run `go generate ./...`)
* [ ] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] Docs (specifically `docs/Config.md`) have been updated if
necessary
* [ ] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
2024-05-19 14:10:16 +10:00
Jesse Duffield 36e7524146 Only run code coverage report on non-fork branches
Codacy's coverage report feature requires the use of a secret key, which
is only available on the main repo and is not available on forks. So,
the step has been always failing on any forks. This commit ensures that
we only run it on non-forks.

This greatly diminishes the value of the coverage reports. I've talked
to one of the Codacy people and advised that they should just have an
API key for coverage reports which is not a secret, like what bugsnag
does.
2024-05-19 14:05:30 +10:00
Stefan Haller 189f39de2b Fix Stashing partial files for git version >= 2.35.0 (#3569)
- **PR Description**
Use git's `--staged` flag to stash staged changes if available (requires
git 2.35.0 or later), and fall back to our previous method if not. This
is a lot faster than our previous method, and it fixes two bugs, see
linked issues.

Fixes #3333 and #3563.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] Docs (specifically `docs/Config.md`) have been updated if
necessary
* [x] You've read through your own file changes for silly mistakes etc
2024-05-18 10:07:24 +02:00
dsolerhww 5b80c0c792 Fix stashing partialy staged files for git version >= 2.35.0
Use `git stash push --staged` git feature available on git version >
2.35.0.
2024-05-18 09:59:00 +02:00
Stefan Haller d8b3c0e568 Fix loading commits with very long subjects (#3533)
- **PR Description**

This PR fixes a problem with lazygit locking up completely when there's
a commit whose subject line is longer than approximately 65500
characters.

Fixes #3529.
2024-05-15 13:29:28 +02:00
Stefan Haller f69eb6dc48 Use ScanLinesAndTruncateWhenLongerThanBuffer instead of bufio.ScanLines 2024-05-15 13:27:01 +02:00
Stefan Haller e0a2d97f0f Put subject last in git log's prettyFormat
We are going to truncate overly long lines returned from git log, and the most
likely field that is going to make the line too long is the subject; so we must
put it last, otherwise we'd end up with not enough fields to split when it's too
long.

It might not be obvious from the diff what's happening to the mock command
output in the test: it didn't have the divergence field (">") at all, which was
kind of a bug. It didn't matter for these tests though, because we are not
testing the divergence here, and our production code happens to be resilient
against it missing. But now we must add the ">" field before the subject.
2024-05-15 13:27:01 +02:00
Stefan Haller 66d0ce841c Implement ScanLinesAndTruncateWhenLongerThanBuffer 2024-05-15 13:27:01 +02:00
Stefan Haller 6bb8c180b2 Handle scanner error in RunAndProcessLines
Scanners can return errors (e.g. ErrTooLong), and if we don't handle it, the
cmd.Wait() call below will block forever because nobody drains the command's
output.

This happens for CommitLoader.GetCommits when there's a commit whose subject
line is longer than approx. 65500 characters; in that case, lazygit would lock
up completely. With this fix it remains usable, but the commit list is truncated
before the bad commit, which is not good enough. We'll improve that in the
remaining commits of this branch.
2024-05-15 13:27:01 +02:00
Stefan Haller 5558d873af Fix clicking in status side panel (#3547)
- **PR Description**

Clicking in the status side panel should activate it; also, clicking on
the repo name should bring up the recent repositories menu, and clicking
on the "(rebasing)" text should bring up the rebase options menu. All of
this was broken for a long time, since somewhere around the big
refactoring of March 2023. (The exact commit where it broke is hard to
bisect, since many of the commits in that area either don't compile or
crash at startup...)

I'm fixing this not because I think it's super important functionality
(nobody seems to have missed it for over a year), but because I have to
touch this code in another PR, and noticed that it wasn't working.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] Docs (specifically `docs/Config.md`) have been updated if
necessary
* [x] You've read through your own file changes for silly mistakes etc
2024-05-15 13:24:07 +02:00
Stefan Haller 380855d4e8 Add tests for clicking in status side panel
To prevent this from breaking again. All three tests would fail without the fix
from the previous commit.
2024-05-15 13:21:17 +02:00
Stefan Haller 88e7c44552 Fix clicking in status side panel
Seems to have been broken since that big refactoring in March 23.
2024-05-15 13:21:17 +02:00
Stefan Haller ac0524f7cb Fix deadlock reporting (#3550)
Deadlock reporting broke in e1ceb6892a (last September); since then, it
was *off* when running debug builds normally, but *on* when debugging an
integration test. Both of which are exactly opposite of what we want.
2024-05-15 13:19:41 +02:00
Stefan Haller 10e29ce7dd Fix deadlock reporting
Deadlock reporting broke in e1ceb6892a; since then, it was *off* when running
debug builds normally, but *on* when debugging an integration test. Both of
which are exactly opposite of what we want.
2024-05-05 15:23:58 +02:00
Stefan Haller 618fe533f8 Add commitPrefix for defining a prefix for any project (#3291)
- **PR Description**

Adds a new option `git.comitPrefix` to act similarly to
`git.commitPrefixes`, except, if defined, it applies to any repo that
doesn't have an entry in `git.commitPrefixes`.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] Docs (specifically `docs/Config.md`) have been updated if
necessary
* [x] You've read through your own file changes for silly mistakes etc
2024-05-01 19:12:53 +02:00
Jonathan Duck 01ff18dd92 Add commitPrefix for defining a prefix for any project 2024-05-01 19:03:12 +02:00
Stefan Haller af0897f18f Enable the commit graph in the divergence view (#3537)
- **PR Description**

In the "View divergence from upstream" view we have so far disabled the
commit graph because it was too difficult to implement properly. I
really miss it though, so here's a PR that enables it there, too.

For feature branches it is not essential, because these usually don't
contain merges and the graph is a trivial line. However, for the master
branch against its upstream it is useful too see how many PRs were
merged since you last fetched it, and the graph helps a lot with that.
Also, when we implement #3536 it will be very useful there, too.
2024-04-30 13:53:35 +02:00
Stefan Haller b7673577a2 Enable the commit graph in the divergence view 2024-04-30 13:50:46 +02:00
Stefan Haller 00c55d5711 chore: fix some comments and typos (#3535)
- **PR Description**
fix some comments and typos
2024-04-28 10:02:59 +02:00
knowmost 0677a58e9f chore: fix some comments and typos
Signed-off-by: knowmost <knowmost@outlook.com>
2024-04-28 09:44:59 +02:00
Stefan Haller b3a60ce407 Add config options for length of commit hash displayed in commits view (#3505)
- **PR Description**

Add a new config option `gui.commitHashLength` to change the length of
the commit hash displayed in commits view.

default:
<img width="472" alt="image"
src="https://github.com/jesseduffield/lazygit/assets/98684296/36dced1e-0c74-4dbd-8670-98e17a75d83a">

With config:
```yaml
gui:
  commitHashLength: 3
```
<img width="463" alt="image"
src="https://github.com/jesseduffield/lazygit/assets/98684296/e8023cd8-f138-4af8-ae0e-3661f80206ca">


- Changes
- Added the user config option to to `pkg/config/user_config.go` and
`schema/config.json`
  - documented in `docs/Config.md`
- Changed the code that displays the hash in
`pkg/gui/presentation/commits.go`
  
---

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] Docs (specifically `docs/Config.md`) have been updated if
necessary
* [x] You've read through your own file changes for silly mistakes etc
2024-04-28 08:22:48 +02:00
Stefan Haller b63321a302 Refactor pkg/gui/presentation/commits.go slightly to be consistent
Change `func displayCommit()` so all the individual strings are built first,
then the whole thing `cols` is put together. Before, most strings were built
prior to constructing `cols`, but a few were built inside the `cols`
construction.
2024-04-27 11:31:19 +02:00
Olivia Bahr a4354ccdfb Add config option for length of commit hash displayed in commits view
- Add config option `commitHashLength` to to pkg/config/user_config.go
- Changed the hash display in pkg/gui/presentation/commits.go
2024-04-27 11:30:49 +02:00
Stefan Haller aa81e191e2 Support externalDiffCommand in diffing mode (#3519)
- **PR Description**

Support `git.paging.externalDiffCommand` config in diffing mode (i.e.
when showing the diff between two commits using `W`).

Fixes #3518.
2024-04-25 08:53:14 +02:00
Stefan Haller 496308e023 Support external diff command in diffing mode 2024-04-25 08:50:30 +02:00
Stefan Haller 98c569749f Use git.paging.colorArg in diffing mode diff 2024-04-25 08:50:30 +02:00
Stefan Haller 047380f311 Cleanup: use separate Arg calls for unrelated arguments 2024-04-25 08:50:30 +02:00
100 changed files with 1410 additions and 453 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ name: Continuous Delivery
on:
push:
tags:
- 'v*'
- "v*"
jobs:
goreleaser:
@@ -16,7 +16,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.21.x
go-version: 1.22.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v4
with:
+12 -6
View File
@@ -1,7 +1,7 @@
name: Continuous Integration
env:
GO_VERSION: 1.21
GO_VERSION: 1.22
on:
push:
@@ -32,7 +32,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.21.x
go-version: 1.22.x
- name: Test code
# we're passing -short so that we skip the integration tests, which will be run in parallel below
run: |
@@ -89,7 +89,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.21.x
go-version: 1.22.x
- name: Print git version
run: git --version
- name: Test code
@@ -115,7 +115,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.21.x
go-version: 1.22.x
- name: Build linux binary
run: |
GOOS=linux go build
@@ -142,7 +142,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.21.x
go-version: 1.22.x
- name: Check Vendor Directory
# ensure our vendor directory matches up with our go modules
run: |
@@ -168,7 +168,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.21.x
go-version: 1.22.x
- name: Lint
uses: golangci/golangci-lint-action@v3.7.0
with:
@@ -188,11 +188,17 @@ jobs:
upload-coverage:
# List all jobs that produce coverage files
needs: [unit-tests, integration-tests]
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.22.x
- name: Download all coverage artifacts
uses: actions/download-artifact@v3
with:
+1 -1
View File
@@ -30,5 +30,5 @@ linters-settings:
max-func-lines: 0
run:
go: '1.21'
go: '1.22'
timeout: 10m
+1 -1
View File
@@ -2,7 +2,7 @@
# docker build -t lazygit .
# docker run -it lazygit:latest /bin/sh
FROM golang:1.21 as build
FROM golang:1.22 as build
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY go.mod go.sum ./
RUN go mod download
+1 -1
View File
File diff suppressed because one or more lines are too long
+10
View File
@@ -80,6 +80,7 @@ gui:
showIcons: false # deprecated: use nerdFontsVersion instead
nerdFontsVersion: "" # nerd fonts version to use ("2" or "3"); empty means don't show nerd font icons
showFileIcons: true # for hiding file icons in the file views
commitHashLength: 8 # length of commit hash in commits view. 0 shows '*' if NF icons aren't enabled
commandLogSize: 8
splitDiff: 'auto' # one of 'auto' | 'always'
skipRewordInEditorWarning: false # for skipping the confirmation before launching the reword editor
@@ -548,6 +549,15 @@ Example:
- Branch name: feature/AB-123
- Commit message: [AB-123] Adding feature
```yaml
git:
commitPrefix:
pattern: "^\\w+\\/(\\w+-\\w+).*"
replace: '[$1] '
```
If you want repository-specific prefixes, you can map them with `commitPrefixes`. If you have both `commitPrefixes` defined and an entry in `commitPrefixes` for the current repo, the `commitPrefixes` entry is given higher precedence. Repository folder names must be an exact match.
```yaml
git:
commitPrefixes:
+1 -1
View File
@@ -305,7 +305,7 @@ SelectedWorktree
CheckedOutBranch
```
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit Lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Hash}}` and `{{.SelectedLocalBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/services/custom_commands/models.go) (all the modelling lives in the same file).
## Keybinding collisions
+1 -1
View File
@@ -13,6 +13,6 @@ includes interactive rebases, so for example amending a commit in the first
branch of the stack will "just work" in the sense that it keeps the other
branches properly stacked onto it.
Lazygit visualizes the invidual branch heads in the stack by marking them with a
Lazygit visualizes the individual branch heads in the stack by marking them with a
cyan asterisk (or a cyan branch symbol if you are using [nerd
fonts](Config.md#display-nerd-fonts-icons)).
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/jesseduffield/lazygit
go 1.21
go 1.22
require (
github.com/adrg/xdg v0.4.0
+1 -1
View File
@@ -134,7 +134,7 @@ func NewGitCommandAux(
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
blameCommands := git_commands.NewBlameCommands(gitCommon)
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands)
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon)
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
+32 -19
View File
@@ -40,6 +40,7 @@ type BranchInfo struct {
// BranchLoader returns a list of Branch objects for the current repo
type BranchLoader struct {
*common.Common
*GitCommon
cmd oscommands.ICmdObjBuilder
getCurrentBranchInfo func() (BranchInfo, error)
config BranchLoaderConfigCommands
@@ -47,12 +48,14 @@ type BranchLoader struct {
func NewBranchLoader(
cmn *common.Common,
gitCommon *GitCommon,
cmd oscommands.ICmdObjBuilder,
getCurrentBranchInfo func() (BranchInfo, error),
config BranchLoaderConfigCommands,
) *BranchLoader {
return &BranchLoader{
Common: cmn,
GitCommon: gitCommon,
cmd: cmd,
getCurrentBranchInfo: getCurrentBranchInfo,
config: config,
@@ -61,7 +64,7 @@ func NewBranchLoader(
// Load the list of branches for the current repo
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
branches := self.obtainBranches()
branches := self.obtainBranches(self.version.IsAtLeast(2, 22, 0))
if self.AppState.LocalBranchSortOrder == "recency" {
reflogBranches := self.obtainReflogBranches(reflogCommits)
@@ -124,7 +127,7 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch
return branches, nil
}
func (self *BranchLoader) obtainBranches() []*models.Branch {
func (self *BranchLoader) obtainBranches(canUsePushTrack bool) []*models.Branch {
output, err := self.getRawBranches()
if err != nil {
panic(err)
@@ -147,7 +150,7 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
}
storeCommitDateAsRecency := self.AppState.LocalBranchSortOrder != "recency"
return obtainBranch(split, storeCommitDateAsRecency), true
return obtainBranch(split, storeCommitDateAsRecency, canUsePushTrack), true
})
}
@@ -183,23 +186,31 @@ var branchFields = []string{
"refname:short",
"upstream:short",
"upstream:track",
"push:track",
"subject",
"objectname",
"committerdate:unix",
}
// Obtain branch information from parsed line output of getRawBranches()
func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch {
func obtainBranch(split []string, storeCommitDateAsRecency bool, canUsePushTrack bool) *models.Branch {
headMarker := split[0]
fullName := split[1]
upstreamName := split[2]
track := split[3]
subject := split[4]
commitHash := split[5]
commitDate := split[6]
pushTrack := split[4]
subject := split[5]
commitHash := split[6]
commitDate := split[7]
name := strings.TrimPrefix(fullName, "heads/")
pushables, pullables, gone := parseUpstreamInfo(upstreamName, track)
aheadForPull, behindForPull, gone := parseUpstreamInfo(upstreamName, track)
var aheadForPush, behindForPush string
if canUsePushTrack {
aheadForPush, behindForPush, _ = parseUpstreamInfo(upstreamName, pushTrack)
} else {
aheadForPush, behindForPush = aheadForPull, behindForPull
}
recency := ""
if storeCommitDateAsRecency {
@@ -209,14 +220,16 @@ func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch
}
return &models.Branch{
Name: name,
Recency: recency,
Pushables: pushables,
Pullables: pullables,
UpstreamGone: gone,
Head: headMarker == "*",
Subject: subject,
CommitHash: commitHash,
Name: name,
Recency: recency,
AheadForPull: aheadForPull,
BehindForPull: behindForPull,
AheadForPush: aheadForPush,
BehindForPush: behindForPush,
UpstreamGone: gone,
Head: headMarker == "*",
Subject: subject,
CommitHash: commitHash,
}
}
@@ -232,10 +245,10 @@ func parseUpstreamInfo(upstreamName string, track string) (string, string, bool)
return "?", "?", true
}
pushables := parseDifference(track, `ahead (\d+)`)
pullables := parseDifference(track, `behind (\d+)`)
ahead := parseDifference(track, `ahead (\d+)`)
behind := parseDifference(track, `behind (\d+)`)
return pushables, pullables, false
return ahead, behind, false
}
func parseDifference(track string, regexStr string) string {
+57 -45
View File
@@ -25,89 +25,101 @@ func TestObtainBranch(t *testing.T) {
scenarios := []scenario{
{
testName: "TrimHeads",
input: []string{"", "heads/a_branch", "", "", "subject", "123", timeStamp},
input: []string{"", "heads/a_branch", "", "", "", "subject", "123", timeStamp},
storeCommitDateAsRecency: false,
expectedBranch: &models.Branch{
Name: "a_branch",
Pushables: "?",
Pullables: "?",
Head: false,
Subject: "subject",
CommitHash: "123",
Name: "a_branch",
AheadForPull: "?",
BehindForPull: "?",
AheadForPush: "?",
BehindForPush: "?",
Head: false,
Subject: "subject",
CommitHash: "123",
},
},
{
testName: "NoUpstream",
input: []string{"", "a_branch", "", "", "subject", "123", timeStamp},
input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp},
storeCommitDateAsRecency: false,
expectedBranch: &models.Branch{
Name: "a_branch",
Pushables: "?",
Pullables: "?",
Head: false,
Subject: "subject",
CommitHash: "123",
Name: "a_branch",
AheadForPull: "?",
BehindForPull: "?",
AheadForPush: "?",
BehindForPush: "?",
Head: false,
Subject: "subject",
CommitHash: "123",
},
},
{
testName: "IsHead",
input: []string{"*", "a_branch", "", "", "subject", "123", timeStamp},
input: []string{"*", "a_branch", "", "", "", "subject", "123", timeStamp},
storeCommitDateAsRecency: false,
expectedBranch: &models.Branch{
Name: "a_branch",
Pushables: "?",
Pullables: "?",
Head: true,
Subject: "subject",
CommitHash: "123",
Name: "a_branch",
AheadForPull: "?",
BehindForPull: "?",
AheadForPush: "?",
BehindForPush: "?",
Head: true,
Subject: "subject",
CommitHash: "123",
},
},
{
testName: "IsBehindAndAhead",
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "subject", "123", timeStamp},
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "[behind 2, ahead 3]", "subject", "123", timeStamp},
storeCommitDateAsRecency: false,
expectedBranch: &models.Branch{
Name: "a_branch",
Pushables: "3",
Pullables: "2",
Head: false,
Subject: "subject",
CommitHash: "123",
Name: "a_branch",
AheadForPull: "3",
BehindForPull: "2",
AheadForPush: "3",
BehindForPush: "2",
Head: false,
Subject: "subject",
CommitHash: "123",
},
},
{
testName: "RemoteBranchIsGone",
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "subject", "123", timeStamp},
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "[gone]", "subject", "123", timeStamp},
storeCommitDateAsRecency: false,
expectedBranch: &models.Branch{
Name: "a_branch",
UpstreamGone: true,
Pushables: "?",
Pullables: "?",
Head: false,
Subject: "subject",
CommitHash: "123",
Name: "a_branch",
UpstreamGone: true,
AheadForPull: "?",
BehindForPull: "?",
AheadForPush: "?",
BehindForPush: "?",
Head: false,
Subject: "subject",
CommitHash: "123",
},
},
{
testName: "WithCommitDateAsRecency",
input: []string{"", "a_branch", "", "", "subject", "123", timeStamp},
input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp},
storeCommitDateAsRecency: true,
expectedBranch: &models.Branch{
Name: "a_branch",
Recency: "2h",
Pushables: "?",
Pullables: "?",
Head: false,
Subject: "subject",
CommitHash: "123",
Name: "a_branch",
Recency: "2h",
AheadForPull: "?",
BehindForPull: "?",
AheadForPush: "?",
BehindForPush: "?",
Head: false,
Subject: "subject",
CommitHash: "123",
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
branch := obtainBranch(s.input, s.storeCommitDateAsRecency)
branch := obtainBranch(s.input, s.storeCommitDateAsRecency, true)
assert.EqualValues(t, s.expectedBranch, branch)
})
}
-5
View File
@@ -41,7 +41,6 @@ func TestBranchGetCommitDifferences(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildBranchCommands(commonDeps{runner: s.runner})
pushables, pullables := instance.GetCommitDifferences("HEAD", "@{u}")
@@ -89,7 +88,6 @@ func TestBranchDeleteBranch(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildBranchCommands(commonDeps{runner: s.runner})
@@ -150,7 +148,6 @@ func TestBranchMerge(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs(s.expected, "", nil)
@@ -190,7 +187,6 @@ func TestBranchCheckout(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildBranchCommands(commonDeps{runner: s.runner})
s.test(instance.Checkout("test", CheckoutOptions{Force: s.force}))
@@ -279,7 +275,6 @@ func TestBranchCurrentBranchInfo(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildBranchCommands(commonDeps{runner: s.runner})
s.test(instance.CurrentBranchInfo())
+1 -32
View File
@@ -22,13 +22,7 @@ func NewCommitCommands(gitCommon *GitCommon) *CommitCommands {
// ResetAuthor resets the author of the topmost commit
func (self *CommitCommands) ResetAuthor() error {
message, err := self.GetCommitMessage("HEAD")
if err != nil {
return err
}
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
cmdArgs := NewGitCmd("commit").
ArgIf(skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix), "--no-verify").
Arg("--allow-empty", "--only", "--no-edit", "--amend", "--reset-author").
ToArgv()
@@ -37,14 +31,7 @@ func (self *CommitCommands) ResetAuthor() error {
// Sets the commit's author to the supplied value. Value is expected to be of the form 'Name <Email>'
func (self *CommitCommands) SetAuthor(value string) error {
message, err := self.GetCommitMessage("HEAD")
if err != nil {
return err
}
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
cmdArgs := NewGitCmd("commit").
ArgIf(skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix), "--no-verify").
Arg("--allow-empty", "--only", "--no-edit", "--amend", "--author="+value).
ToArgv()
@@ -60,10 +47,7 @@ func (self *CommitCommands) AddCoAuthor(hash string, author string) error {
message = AddCoAuthorToMessage(message, author)
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
cmdArgs := NewGitCmd("commit").
ArgIf(skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix), "--no-verify").
Arg("--allow-empty", "--amend", "--only", "-m", message).
ToArgv()
@@ -116,15 +100,11 @@ func (self *CommitCommands) CommitCmdObj(summary string, description string) osc
}
func (self *CommitCommands) RewordLastCommitInEditorCmdObj() oscommands.ICmdObj {
return self.cmd.New(NewGitCmd("commit").
// TODO: how to decide if we should add --no-verify if we're using the editor?
Arg("--allow-empty", "--amend", "--only").ToArgv())
return self.cmd.New(NewGitCmd("commit").Arg("--allow-empty", "--amend", "--only").ToArgv())
}
func (self *CommitCommands) RewordLastCommitInEditorWithMessageFileCmdObj(tmpMessageFile string) oscommands.ICmdObj {
return self.cmd.New(NewGitCmd("commit").
// TODO: how to decide if we should add --no-verify if we're using the editor?
Arg("--allow-empty", "--amend", "--only", "--edit", "--file="+tmpMessageFile).ToArgv())
}
@@ -140,10 +120,7 @@ func (self *CommitCommands) CommitInEditorWithMessageFileCmdObj(tmpMessageFile s
func (self *CommitCommands) RewordLastCommit(summary string, description string) error {
messageArgs := self.commitMessageArgs(summary, description)
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
cmdArgs := NewGitCmd("commit").
ArgIf(skipHookPrefix != "" && strings.HasPrefix(summary, skipHookPrefix), "--no-verify").
Arg("--allow-empty", "--amend", "--only").
Arg(messageArgs...).
ToArgv()
@@ -271,15 +248,7 @@ func (self *CommitCommands) AmendHead() error {
}
func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
message, err := self.GetCommitMessage("HEAD")
if err != nil {
// TODO: what to do here? we can't return err
// return err
}
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
cmdArgs := NewGitCmd("commit").
ArgIf(skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix), "--no-verify").
Arg("--amend", "--no-edit", "--allow-empty").
ToArgv()
+3 -5
View File
@@ -211,11 +211,11 @@ func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool
authorEmail := split[3]
extraInfo := strings.TrimSpace(split[4])
parentHashes := split[5]
message := split[6]
divergence := models.DivergenceNone
if showDivergence {
divergence = lo.Ternary(split[7] == "<", models.DivergenceLeft, models.DivergenceRight)
divergence = lo.Ternary(split[6] == "<", models.DivergenceLeft, models.DivergenceRight)
}
message := split[7]
tags := []string{}
@@ -505,8 +505,6 @@ func (self *CommitLoader) getExistingMainBranches() []string {
for i, branchName := range mainBranches {
wg.Add(1)
i := i
branchName := branchName
go utils.Safe(func() {
defer wg.Done()
@@ -605,4 +603,4 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
return self.cmd.New(cmdArgs).DontLog()
}
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m`
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s`
+16 -17
View File
@@ -15,16 +15,16 @@ import (
"github.com/stretchr/testify/assert"
)
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|better typing for rebase mode
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com|origin/better-tests|e94e8fc5b6fab4cb755f|fix logging
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com|tag: 123, tag: 456|d8084cd558925eb7c9c3|refactor
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|WIP
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|WIP
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|WIP
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1)
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com|origin/better-tests|e94e8fc5b6fab4cb755f|>|fix logging
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com|tag: 123, tag: 456|d8084cd558925eb7c9c3|>|refactor
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|>|WIP
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|>|WIP
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|>|WIP
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|>|WIP
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|>|refactoring the config struct`, "|", "\x00", -1)
var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|better typing for rebase mode`, "|", "\x00", -1)
var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode`, "|", "\x00", -1)
func TestGetCommits(t *testing.T) {
type scenario struct {
@@ -46,7 +46,7 @@ func TestGetCommits(t *testing.T) {
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
@@ -58,7 +58,7 @@ func TestGetCommits(t *testing.T) {
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", RefForPushedStatus: "refs/heads/mybranch", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"merge-base", "refs/heads/mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
@@ -73,7 +73,7 @@ func TestGetCommits(t *testing.T) {
// here it's seeing which commits are yet to be pushed
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
// here it's actually getting all the commits in a formatted form, one per line
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
// here it's testing which of the configured main branches have an upstream
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). // this one does
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead
@@ -210,7 +210,7 @@ func TestGetCommits(t *testing.T) {
// here it's seeing which commits are yet to be pushed
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
// here it's actually getting all the commits in a formatted form, one per line
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
// here it's testing which of the configured main branches exist; neither does
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "", errors.New("error")).
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/master"}, "", errors.New("error")).
@@ -247,7 +247,7 @@ func TestGetCommits(t *testing.T) {
// here it's seeing which commits are yet to be pushed
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
// here it's actually getting all the commits in a formatted form, one per line
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
// here it's testing which of the configured main branches exist
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil).
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")).
@@ -283,7 +283,7 @@ func TestGetCommits(t *testing.T) {
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
@@ -295,7 +295,7 @@ func TestGetCommits(t *testing.T) {
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", FilterPath: "src"},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
@@ -303,7 +303,6 @@ func TestGetCommits(t *testing.T) {
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
common := utils.NewDummyCommon()
common.AppState = &config.AppState{}
-8
View File
@@ -30,7 +30,6 @@ func TestCommitRewordCommit(t *testing.T) {
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner})
@@ -100,7 +99,6 @@ func TestCommitCommitCmdObj(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
userConfig.Git.Commit.SignOff = s.configSignoff
@@ -136,7 +134,6 @@ func TestCommitCommitEditorCmdObj(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
userConfig.Git.Commit.SignOff = s.configSignoff
@@ -171,7 +168,6 @@ func TestCommitCreateFixupCommit(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner})
s.test(instance.CreateFixupCommit(s.hash))
@@ -221,7 +217,6 @@ func TestCommitCreateAmendCommit(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner})
err := instance.CreateAmendCommit(s.originalSubject, s.newSubject, s.newDescription, s.includeFileChanges)
@@ -285,7 +280,6 @@ func TestCommitShowCmdObj(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
userConfig.Git.Paging.ExternalDiffCommand = s.extDiffCmd
@@ -334,7 +328,6 @@ func TestGetCommitMsg(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{
runner: oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"-c", "log.showsignature=false", "log", "--format=%B", "--max-count=1", "deadbeef"}, s.input, nil),
@@ -374,7 +367,6 @@ func TestGetCommitMessageFromHistory(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner})
+12 -2
View File
@@ -1,6 +1,10 @@
package git_commands
import "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
type DiffCommands struct {
*GitCommon
@@ -13,10 +17,16 @@ func NewDiffCommands(gitCommon *GitCommon) *DiffCommands {
}
func (self *DiffCommands) DiffCmdObj(diffArgs []string) oscommands.ICmdObj {
extDiffCmd := self.UserConfig.Git.Paging.ExternalDiffCommand
useExtDiff := extDiffCmd != ""
return self.cmd.New(
NewGitCmd("diff").
Config("diff.noprefix=false").
Arg("--submodule", "--no-ext-diff", "--color").
ConfigIf(useExtDiff, "diff.external="+extDiffCmd).
ArgIfElse(useExtDiff, "--ext-diff", "--no-ext-diff").
Arg("--submodule").
Arg(fmt.Sprintf("--color=%s", self.UserConfig.Git.Paging.ColorArg)).
Arg(diffArgs...).
Dir(self.repoPaths.worktreePath).
ToArgv(),
@@ -172,7 +172,6 @@ func TestFileGetStatusFiles(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
-2
View File
@@ -23,7 +23,6 @@ func TestStartCmdObj(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildFlowCommands(commonDeps{})
@@ -69,7 +68,6 @@ func TestFinishCmdObj(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildFlowCommands(commonDeps{
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
-3
View File
@@ -67,7 +67,6 @@ func TestRebaseRebaseBranch(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildRebaseCommands(commonDeps{runner: s.runner, gitVersion: s.gitVersion})
s.test(instance.RebaseBranch(s.arg))
@@ -89,7 +88,6 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
`^GIT_SEQUENCE_EDITOR=.*$`,
"^" + daemon.DaemonKindEnvKey + "=" + strconv.Itoa(int(daemon.DaemonKindExitImmediately)) + "$",
} {
regexStr := regexStr
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
return regexp.MustCompile(regexStr).MatchString(envVar)
})
@@ -163,7 +161,6 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildRebaseCommands(commonDeps{
runner: s.runner,
@@ -176,7 +176,6 @@ func TestGetReflogCommits(t *testing.T) {
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
builder := &ReflogCommitLoader{
Common: utils.NewDummyCommon(),
@@ -101,7 +101,6 @@ func TestGetRepoPaths(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.Name, func(t *testing.T) {
runner := oscommands.NewFakeRunner(t)
cmd := oscommands.NewDummyCmdObjBuilder(runner)
+15 -2
View File
@@ -121,9 +121,22 @@ func (self *StashCommands) StashUnstagedChanges(message string) error {
return nil
}
// SaveStagedChanges stashes only the currently staged changes. This takes a few steps
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
// SaveStagedChanges stashes only the currently staged changes.
func (self *StashCommands) SaveStagedChanges(message string) error {
if self.version.IsAtLeast(2, 35, 0) {
return self.cmd.New(NewGitCmd("stash").Arg("push").Arg("--staged").Arg("-m", message).ToArgv()).Run()
}
// Git versions older than 2.35.0 don't support the --staged flag, so we
// need to fall back to a more complex solution.
// Shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
//
// Note that this method has a few bugs:
// - it fails when there are *only* staged changes
// - it fails when staged and unstaged changes within a single file are too close together
// We don't bother fixing these, because users can simply update git when
// they are affected by these issues.
// wrap in 'writing', which uses a mutex
if err := self.cmd.New(
NewGitCmd("stash").Arg("--keep-index").ToArgv(),
@@ -47,7 +47,6 @@ func TestGetStashEntries(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
-3
View File
@@ -74,7 +74,6 @@ func TestStashStore(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs(s.expected, "", nil)
@@ -131,7 +130,6 @@ func TestStashStashEntryCmdObj(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
appState := &config.AppState{}
@@ -181,7 +179,6 @@ func TestStashRename(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs(s.expectedHashCmd, s.hashResult, nil).
-3
View File
@@ -86,7 +86,6 @@ func TestSyncPush(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildSyncCommands(commonDeps{})
task := gocui.NewFakeTask()
@@ -124,7 +123,6 @@ func TestSyncFetch(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildSyncCommands(commonDeps{})
instance.UserConfig.Git.FetchAll = s.fetchAllConfig
@@ -163,7 +161,6 @@ func TestSyncFetchBackground(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildSyncCommands(commonDeps{})
instance.UserConfig.Git.FetchAll = s.fetchAllConfig
@@ -44,7 +44,6 @@ func TestGetTags(t *testing.T) {
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
loader := &TagLoader{
Common: utils.NewDummyCommon(),
+1 -1
View File
@@ -363,7 +363,7 @@ func (self *WorkingTreeCommands) ResetAndClean() error {
return self.RemoveUntrackedFiles()
}
// ResetHardHead runs `git reset --hard`
// ResetHard runs `git reset --hard`
func (self *WorkingTreeCommands) ResetHard(ref string) error {
cmdArgs := NewGitCmd("reset").Arg("--hard", ref).
ToArgv()
@@ -61,7 +61,6 @@ func TestWorkingTreeUnstageFile(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.UnStageFile([]string{"test.txt"}, s.reset))
@@ -190,7 +189,6 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, removeFile: s.removeFile})
err := instance.DiscardAllFileChanges(s.file)
@@ -306,7 +304,6 @@ func TestWorkingTreeDiff(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
appState := &config.AppState{}
@@ -375,7 +372,6 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
appState := &config.AppState{}
@@ -428,7 +424,6 @@ func TestWorkingTreeCheckoutFile(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
@@ -459,7 +454,6 @@ func TestWorkingTreeDiscardUnstagedFileChanges(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.DiscardUnstagedFileChanges(s.file))
@@ -487,7 +481,6 @@ func TestWorkingTreeDiscardAnyUnstagedFileChanges(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.DiscardAnyUnstagedFileChanges())
@@ -515,7 +508,6 @@ func TestWorkingTreeRemoveUntrackedFiles(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.RemoveUntrackedFiles())
@@ -545,7 +537,6 @@ func TestWorkingTreeResetHard(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.ResetHard(s.ref))
@@ -76,8 +76,6 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
wg := sync.WaitGroup{}
wg.Add(len(worktrees))
for _, worktree := range worktrees {
worktree := worktree
go utils.Safe(func() {
defer wg.Done()
@@ -181,7 +181,6 @@ branch refs/heads/mybranch-worktree
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
runner := oscommands.NewFakeRunner(t)
fs := afero.NewMemMapFs()
@@ -50,7 +50,6 @@ func TestGetBool(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
fake := NewFakeGitConfig(s.mockResponses)
real := NewCachedGitConfig(
@@ -87,7 +86,6 @@ func TestGet(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
fake := NewFakeGitConfig(s.mockResponses)
real := NewCachedGitConfig(
@@ -413,7 +413,6 @@ func TestGetPullRequestURL(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
tr := i18n.EnglishTranslationSet()
log := &fakes.FakeFieldLogger{}
+19 -11
View File
@@ -10,10 +10,14 @@ type Branch struct {
DisplayName string
// indicator of when the branch was last checked out e.g. '2d', '3m'
Recency string
// how many commits ahead we are from the remote branch (how many commits we can push)
Pushables string
// how many commits ahead we are from the remote branch (how many commits we can push, assuming we push to our tracked remote branch)
AheadForPull string
// how many commits behind we are from the remote branch (how many commits we can pull)
Pullables string
BehindForPull string
// how many commits ahead we are from the branch we're pushing to (which might not be the same as our upstream branch in a triangular workflow)
AheadForPush string
// how many commits behind we are from the branch we're pushing to (which might not be the same as our upstream branch in a triangular workflow)
BehindForPush string
// whether the remote branch is 'gone' i.e. we're tracking a remote branch that has been deleted
UpstreamGone bool
// whether this is the current branch. Exactly one branch should have this be true
@@ -80,26 +84,30 @@ func (b *Branch) IsTrackingRemote() bool {
// we know that the remote branch is not stored locally based on our pushable/pullable
// count being question marks.
func (b *Branch) RemoteBranchStoredLocally() bool {
return b.IsTrackingRemote() && b.Pushables != "?" && b.Pullables != "?"
return b.IsTrackingRemote() && b.AheadForPull != "?" && b.BehindForPull != "?"
}
func (b *Branch) RemoteBranchNotStoredLocally() bool {
return b.IsTrackingRemote() && b.Pushables == "?" && b.Pullables == "?"
return b.IsTrackingRemote() && b.AheadForPull == "?" && b.BehindForPull == "?"
}
func (b *Branch) MatchesUpstream() bool {
return b.RemoteBranchStoredLocally() && b.Pushables == "0" && b.Pullables == "0"
return b.RemoteBranchStoredLocally() && b.AheadForPull == "0" && b.BehindForPull == "0"
}
func (b *Branch) HasCommitsToPush() bool {
return b.RemoteBranchStoredLocally() && b.Pushables != "0"
func (b *Branch) IsAheadForPull() bool {
return b.RemoteBranchStoredLocally() && b.AheadForPull != "0"
}
func (b *Branch) HasCommitsToPull() bool {
return b.RemoteBranchStoredLocally() && b.Pullables != "0"
func (b *Branch) IsBehindForPull() bool {
return b.RemoteBranchStoredLocally() && b.BehindForPull != "0"
}
func (b *Branch) IsBehindForPush() bool {
return b.BehindForPush != "" && b.BehindForPush != "0"
}
// for when we're in a detached head state
func (b *Branch) IsRealBranch() bool {
return b.Pushables != "" && b.Pullables != ""
return b.AheadForPull != "" && b.BehindForPull != ""
}
+6 -1
View File
@@ -161,7 +161,7 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize))
if err := cmd.Start(); err != nil {
return err
}
@@ -178,6 +178,11 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
}
}
if scanner.Err() != nil {
_ = Kill(cmd)
return scanner.Err()
}
_ = cmd.Wait()
self.log.Infof("%s (%s)", cmdObj.ToString(), time.Since(t))
+3 -4
View File
@@ -239,14 +239,13 @@ func (c *OSCommand) PipeCommands(cmdObjs ...ICmdObj) error {
wg.Add(len(cmds))
for _, cmd := range cmds {
currentCmd := cmd
go utils.Safe(func() {
stderr, err := currentCmd.StderrPipe()
stderr, err := cmd.StderrPipe()
if err != nil {
c.Log.Error(err)
}
if err := currentCmd.Start(); err != nil {
if err := cmd.Start(); err != nil {
c.Log.Error(err)
}
@@ -256,7 +255,7 @@ func (c *OSCommand) PipeCommands(cmdObjs ...ICmdObj) error {
}
}
if err := currentCmd.Wait(); err != nil {
if err := cmd.Wait(); err != nil {
c.Log.Error(err)
}
-4
View File
@@ -509,7 +509,6 @@ func TestTransform(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
lineIndices := ExpandRange(s.firstLineIndex, s.lastLineIndex)
@@ -566,7 +565,6 @@ func TestParseAndFormatPlain(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
// here we parse the patch, then format it, and ensure the result
// matches the original patch. Note that unified diffs allow omitting
@@ -604,7 +602,6 @@ func TestLineNumberOfLine(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
for i, idx := range s.indexes {
patch := Parse(s.patchStr)
@@ -633,7 +630,6 @@ func TestGetNextStageableLineIndex(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
for i, idx := range s.indexes {
patch := Parse(s.patchStr)
+20 -13
View File
@@ -1,5 +1,7 @@
package config
import "os"
func GetEditTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
preset := getPreset(osConfig, guessDefaultEditor)
template := osConfig.Edit
@@ -42,9 +44,11 @@ type editPreset struct {
editAtLineTemplate string
editAtLineAndWaitTemplate string
openDirInEditorTemplate string
suspend bool
suspend func() bool
}
func returnBool(a bool) func() bool { return (func() bool { return a }) }
// IF YOU ADD A PRESET TO THIS FUNCTION YOU MUST UPDATE THE `Supported presets` SECTION OF docs/Config.md
func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
presets := map[string]*editPreset{
@@ -52,12 +56,15 @@ func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset
"vim": standardTerminalEditorPreset("vim"),
"nvim": standardTerminalEditorPreset("nvim"),
"nvim-remote": {
editTemplate: `nvim --server "$NVIM" --remote-tab {{filename}}`,
editAtLineTemplate: `nvim --server "$NVIM" --remote-tab {{filename}}; [ -z "$NVIM" ] || nvim --server "$NVIM" --remote-send ":{{line}}<CR>"`,
editTemplate: `[ -z "$NVIM" ] && (nvim -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}})`,
editAtLineTemplate: `[ -z "$NVIM" ] && (nvim +{{line}} -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}} && nvim --server "$NVIM" --remote-send ":{{line}}<CR>")`,
// No remote-wait support yet. See https://github.com/neovim/neovim/pull/17856
editAtLineAndWaitTemplate: `nvim +{{line}} {{filename}}`,
openDirInEditorTemplate: `nvim --server "$NVIM" --remote-tab {{dir}}`,
suspend: false,
openDirInEditorTemplate: `[ -z "$NVIM" ] && (nvim -- {{dir}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{dir}})`,
suspend: func() bool {
_, ok := os.LookupEnv("NVIM")
return !ok
},
},
"lvim": standardTerminalEditorPreset("lvim"),
"emacs": standardTerminalEditorPreset("emacs"),
@@ -69,42 +76,42 @@ func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset
editAtLineTemplate: "helix -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "helix -- {{filename}}:{{line}}",
openDirInEditorTemplate: "helix -- {{dir}}",
suspend: true,
suspend: returnBool(true),
},
"helix (hx)": {
editTemplate: "hx -- {{filename}}",
editAtLineTemplate: "hx -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "hx -- {{filename}}:{{line}}",
openDirInEditorTemplate: "hx -- {{dir}}",
suspend: true,
suspend: returnBool(true),
},
"vscode": {
editTemplate: "code --reuse-window -- {{filename}}",
editAtLineTemplate: "code --reuse-window --goto -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "code --reuse-window --goto --wait -- {{filename}}:{{line}}",
openDirInEditorTemplate: "code -- {{dir}}",
suspend: false,
suspend: returnBool(false),
},
"sublime": {
editTemplate: "subl -- {{filename}}",
editAtLineTemplate: "subl -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "subl --wait -- {{filename}}:{{line}}",
openDirInEditorTemplate: "subl -- {{dir}}",
suspend: false,
suspend: returnBool(false),
},
"bbedit": {
editTemplate: "bbedit -- {{filename}}",
editAtLineTemplate: "bbedit +{{line}} -- {{filename}}",
editAtLineAndWaitTemplate: "bbedit +{{line}} --wait -- {{filename}}",
openDirInEditorTemplate: "bbedit -- {{dir}}",
suspend: false,
suspend: returnBool(false),
},
"xcode": {
editTemplate: "xed -- {{filename}}",
editAtLineTemplate: "xed --line {{line}} -- {{filename}}",
editAtLineAndWaitTemplate: "xed --line {{line}} --wait -- {{filename}}",
openDirInEditorTemplate: "xed -- {{dir}}",
suspend: false,
suspend: returnBool(false),
},
}
@@ -141,7 +148,7 @@ func standardTerminalEditorPreset(editor string) *editPreset {
editAtLineTemplate: editor + " +{{line}} -- {{filename}}",
editAtLineAndWaitTemplate: editor + " +{{line}} -- {{filename}}",
openDirInEditorTemplate: editor + " -- {{dir}}",
suspend: true,
suspend: returnBool(true),
}
}
@@ -149,5 +156,5 @@ func getEditInTerminal(osConfig *OSConfig, preset *editPreset) bool {
if osConfig.SuspendOnEdit != nil {
return *osConfig.SuspendOnEdit
}
return preset.suspend
return preset.suspend()
}
+5
View File
@@ -123,6 +123,8 @@ type GuiConfig struct {
NerdFontsVersion string `yaml:"nerdFontsVersion" jsonschema:"enum=2,enum=3,enum="`
// If true (default), file icons are shown in the file views. Only relevant if NerdFontsVersion is not empty.
ShowFileIcons bool `yaml:"showFileIcons"`
// Length of commit hash in commits view. 0 shows '*' if NF icons aren't on.
CommitHashLength int `yaml:"commitHashLength" jsonschema:"minimum=0"`
// If true, show commit hashes alongside branch names in the branches view.
ShowBranchCommitHash bool `yaml:"showBranchCommitHash"`
// Height of the command log view
@@ -220,6 +222,8 @@ type GitConfig struct {
// If true, do not allow force pushes
DisableForcePushing bool `yaml:"disableForcePushing"`
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
CommitPrefix *CommitPrefixConfig `yaml:"commitPrefix"`
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
// If true, parse emoji strings in commit messages e.g. render :rocket: as 🚀
// (This should really be under 'gui', not 'git')
@@ -675,6 +679,7 @@ func GetDefaultConfig() *UserConfig {
ShowIcons: false,
NerdFontsVersion: "",
ShowFileIcons: true,
CommitHashLength: 8,
ShowBranchCommitHash: false,
CommandLogSize: 8,
SplitDiff: "auto",
-1
View File
@@ -105,7 +105,6 @@ func (self *MenuViewModel) GetNonModelItems() []*NonModelItem {
menuItems := self.FilteredListViewModel.GetItems()
var prevSection *types.MenuSection = nil
for i, menuItem := range menuItems {
menuItem := menuItem
if menuItem.Section != nil && menuItem.Section != prevSection {
if prevSection != nil {
result = append(result, &NonModelItem{
+1 -1
View File
@@ -75,7 +75,7 @@ func NewSubCommitsContext(
endIdx,
// Don't show the graph in the left/right view; we'd like to, but
// it's too complicated:
shouldShowGraph(c) && viewModel.GetRefToShowDivergenceFrom() == "",
shouldShowGraph(c),
git_commands.NewNullBisectInfo(),
false,
)
+7 -4
View File
@@ -14,10 +14,13 @@ type SuggestionsContext struct {
}
type SuggestionsContextState struct {
Suggestions []*types.Suggestion
OnConfirm func() error
OnClose func() error
AsyncHandler *tasks.AsyncHandler
Suggestions []*types.Suggestion
OnConfirm func() error
OnClose func() error
OnDeleteSuggestion func() error
AsyncHandler *tasks.AsyncHandler
AllowEditSuggestion bool
// FindSuggestions will take a string that the user has typed into a prompt
// and return a slice of suggestions which match that string.
+1 -1
View File
@@ -620,7 +620,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
if !branch.RemoteBranchStoredLocally() {
return errors.New(self.c.Tr.FwdNoLocalUpstream)
}
if branch.HasCommitsToPush() {
if branch.IsAheadForPull() {
return errors.New(self.c.Tr.FwdCommitsToPush)
}
@@ -1,6 +1,8 @@
package controllers
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@@ -39,6 +41,14 @@ func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) [
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error {
if len(self.c.Contexts().Suggestions.State.Suggestions) > 0 {
subtitle := ""
if self.c.State().GetRepoState().GetCurrentPopupOpts().HandleDeleteSuggestion != nil {
// We assume that whenever things are deletable, they
// are also editable, so we show both keybindings
subtitle = fmt.Sprintf(self.c.Tr.SuggestionsSubtitle,
self.c.UserConfig.Keybinding.Universal.Remove, self.c.UserConfig.Keybinding.Universal.Edit)
}
self.c.Views().Suggestions.Subtitle = subtitle
return self.c.ReplaceContext(self.c.Contexts().Suggestions)
}
return nil
+25 -2
View File
@@ -1,6 +1,7 @@
package controllers
import (
"slices"
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
@@ -17,6 +18,7 @@ func (self *CustomCommandAction) Call() error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.CustomCommand,
FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(),
AllowEditSuggestion: true,
HandleConfirm: func(command string) error {
if self.shouldSaveCommand(command) {
self.c.GetAppState().CustomCommandsHistory = utils.Limit(
@@ -32,13 +34,34 @@ func (self *CustomCommandAction) Call() error {
self.c.OS().Cmd.NewShell(command),
)
},
HandleDeleteSuggestion: func(index int) error {
// index is the index in the _filtered_ list of suggestions, so we
// need to map it back to the full list. There's no really good way
// to do this, but fortunately we keep the items in the
// CustomCommandsHistory unique, which allows us to simply search
// for it by string.
item := self.c.Contexts().Suggestions.GetItems()[index].Value
fullIndex := lo.IndexOf(self.c.GetAppState().CustomCommandsHistory, item)
if fullIndex == -1 {
// Should never happen, but better be safe
return nil
}
self.c.GetAppState().CustomCommandsHistory = slices.Delete(
self.c.GetAppState().CustomCommandsHistory, fullIndex, fullIndex+1)
self.c.SaveAppStateAndLogError()
self.c.Contexts().Suggestions.RefreshSuggestions()
return nil
},
})
}
func (self *CustomCommandAction) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
history := self.c.GetAppState().CustomCommandsHistory
return func(input string) []*types.Suggestion {
history := self.c.GetAppState().CustomCommandsHistory
return helpers.FilterFunc(history, self.c.UserConfig.Gui.UseFuzzySearch())
return helpers.FilterFunc(history, self.c.UserConfig.Gui.UseFuzzySearch())(input)
}
}
// this mimics the shell functionality `ignorespace`
@@ -17,7 +17,6 @@ func (self *DiffingMenuAction) Call() error {
menuItems := []*types.MenuItem{}
for _, name := range names {
name := name
menuItems = append(menuItems, []*types.MenuItem{
{
Label: fmt.Sprintf("%s %s", self.c.Tr.Diff, name),
@@ -159,6 +159,7 @@ func (self *ConfirmationHelper) prepareConfirmationPanel(
suggestionsContext.SetSuggestions(opts.FindSuggestionsFunc(""))
suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(self.c.Tr.SuggestionsTitle, self.c.UserConfig.Keybinding.Universal.TogglePanel)
suggestionsView.Subtitle = ""
}
self.ResizeConfirmationPanel()
@@ -223,6 +224,8 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
return err
}
self.c.Contexts().Suggestions.State.AllowEditSuggestion = opts.AllowEditSuggestion
self.c.State().GetRepoState().SetCurrentPopupOpts(&opts)
return self.c.PushContext(self.c.Contexts().Confirmation)
@@ -270,10 +273,20 @@ func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts
onClose := self.wrappedConfirmationFunction(cancel, opts.HandleClose)
onDeleteSuggestion := func() error {
if opts.HandleDeleteSuggestion == nil {
return nil
}
idx := self.c.Contexts().Suggestions.GetSelectedLineIdx()
return opts.HandleDeleteSuggestion(idx)
}
self.c.Contexts().Confirmation.State.OnConfirm = onConfirm
self.c.Contexts().Confirmation.State.OnClose = onClose
self.c.Contexts().Suggestions.State.OnConfirm = onSuggestionConfirm
self.c.Contexts().Suggestions.State.OnClose = onClose
self.c.Contexts().Suggestions.State.OnDeleteSuggestion = onDeleteSuggestion
return nil
}
@@ -284,6 +297,7 @@ func (self *ConfirmationHelper) clearConfirmationViewKeyBindings() {
self.c.Contexts().Confirmation.State.OnClose = noop
self.c.Contexts().Suggestions.State.OnConfirm = noop
self.c.Contexts().Suggestions.State.OnClose = noop
self.c.Contexts().Suggestions.State.OnDeleteSuggestion = noop
}
func (self *ConfirmationHelper) getSelectedSuggestionValue() string {
@@ -220,9 +220,9 @@ func (self *WorkingTreeHelper) prepareFilesForCommit() error {
func (self *WorkingTreeHelper) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
cfg, ok := self.c.UserConfig.Git.CommitPrefixes[self.c.Git().RepoPaths.RepoName()]
if !ok {
return nil
if ok {
return &cfg
}
return &cfg
return self.c.UserConfig.Git.CommitPrefix
}
@@ -801,7 +801,6 @@ func (self *LocalCommitsController) revert(commit *models.Commit) error {
func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.Commit) error {
menuItems := make([]*types.MenuItem, len(commit.Parents))
for i, parentHash := range commit.Parents {
i := i
message, err := self.c.Git().Commit.GetCommitMessageFirstLine(parentHash)
if err != nil {
return err
+9 -9
View File
@@ -75,6 +75,11 @@ func (self *StatusController) GetMouseKeybindings(opts types.KeybindingsOpts) []
Key: gocui.MouseLeft,
Handler: self.onClickMain,
},
{
ViewName: self.Context().GetViewName(),
Key: gocui.MouseLeft,
Handler: self.onClick,
},
}
}
@@ -95,15 +100,11 @@ func (self *StatusController) GetOnRenderToMain() func() error {
}
}
func (self *StatusController) GetOnClick() func() error {
return self.onClick
}
func (self *StatusController) Context() types.Context {
return self.c.Contexts().Status
}
func (self *StatusController) onClick() error {
func (self *StatusController) onClick(opts gocui.ViewMouseBindingOpts) error {
// TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives)
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
if currentBranch == nil {
@@ -115,21 +116,20 @@ func (self *StatusController) onClick() error {
return err
}
cx, _ := self.c.Views().Status.Cursor()
upstreamStatus := presentation.BranchStatus(currentBranch, types.ItemOperationNone, self.c.Tr, time.Now(), self.c.UserConfig)
repoName := self.c.Git().RepoPaths.RepoName()
workingTreeState := self.c.Git().Status.WorkingTreeState()
switch workingTreeState {
case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING:
workingTreeStatus := fmt.Sprintf("(%s)", presentation.FormatWorkingTreeStateLower(self.c.Tr, workingTreeState))
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
if cursorInSubstring(opts.X, upstreamStatus+" ", workingTreeStatus) {
return self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu()
}
if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) {
if cursorInSubstring(opts.X, upstreamStatus+" "+workingTreeStatus+" ", repoName) {
return self.c.Helpers().Repos.CreateRecentReposMenu()
}
default:
if cursorInSubstring(cx, upstreamStatus+" ", repoName) {
if cursorInSubstring(opts.X, upstreamStatus+" ", repoName) {
return self.c.Helpers().Repos.CreateRecentReposMenu()
}
}
+26 -2
View File
@@ -40,8 +40,32 @@ func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []
Handler: func() error { return self.context().State.OnClose() },
},
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error { return self.c.ReplaceContext(self.c.Contexts().Confirmation) },
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error {
self.c.Views().Suggestions.Subtitle = ""
return self.c.ReplaceContext(self.c.Contexts().Confirmation)
},
},
{
Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: func() error {
return self.context().State.OnDeleteSuggestion()
},
},
{
Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: func() error {
if self.context().State.AllowEditSuggestion {
if selectedItem := self.c.Contexts().Suggestions.GetSelected(); selectedItem != nil {
self.c.Contexts().Confirmation.GetView().TextArea.Clear()
self.c.Contexts().Confirmation.GetView().TextArea.TypeString(selectedItem.Value)
self.c.Contexts().Confirmation.GetView().RenderTextArea()
self.c.Contexts().Suggestions.RefreshSuggestions()
return self.c.ReplaceContext(self.c.Contexts().Confirmation)
}
}
return nil
},
},
}
+2 -2
View File
@@ -87,10 +87,10 @@ func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func(
}
func (self *SyncController) push(currentBranch *models.Branch) error {
// if we have pullables we'll ask if the user wants to force push
// if we are behind our upstream branch we'll ask if the user wants to force push
if currentBranch.IsTrackingRemote() {
opts := pushOpts{}
if currentBranch.HasCommitsToPull() {
if currentBranch.IsBehindForPush() {
return self.requestToForcePush(currentBranch, opts)
} else {
return self.pushAux(currentBranch, opts)
+1 -1
View File
@@ -16,7 +16,7 @@ import (
// we then do the reverse of what that reflog describes.
// When we do this, we create a new reflog entry, and tag it as either an undo or redo
// Then, next time we want to undo, we'll use those entries to know which user-initiated
// actions we can skip. E.g. if I do do three things, A, B, and C, and hit undo twice,
// actions we can skip. E.g. if I do three things, A, B, and C, and hit undo twice,
// the reflog will read UUCBA, and when I read the first two undos, I know to skip the following
// two user actions, meaning we end up undoing reflog entry C. Redoing works in a similar way.
+1 -5
View File
@@ -127,7 +127,7 @@ func TestBuildTreeFromFiles(t *testing.T) {
expected: &Node[models.File]{
Path: "",
// it is a little strange that we're not bubbling up our merge conflict
// here but we are technically still in in tree mode and that's the rule
// here but we are technically still in tree mode and that's the rule
Children: []*Node[models.File]{
{
File: &models.File{Name: "a"},
@@ -147,7 +147,6 @@ func TestBuildTreeFromFiles(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
result := BuildTreeFromFiles(s.files)
assert.EqualValues(t, s.expected, result)
@@ -306,7 +305,6 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
result := BuildFlatTreeFromFiles(s.files)
assert.EqualValues(t, s.expected, result)
@@ -420,7 +418,6 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
result := BuildTreeFromCommitFiles(s.files)
assert.EqualValues(t, s.expected, result)
@@ -521,7 +518,6 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
result := BuildFlatTreeFromCommitFiles(s.files)
assert.EqualValues(t, s.expected, result)
-2
View File
@@ -125,7 +125,6 @@ func TestCompress(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
s.root.Compress()
assert.EqualValues(t, s.expected, s.root)
@@ -155,7 +154,6 @@ func TestGetFile(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
assert.EqualValues(t, s.expected, s.viewModel.GetFile(s.path))
})
-1
View File
@@ -71,7 +71,6 @@ func TestFilterAction(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
mngr := &FileTree{getFiles: func() []*models.File { return s.files }, filter: s.filter}
result := mngr.getFilesForDisplay()
+1 -1
View File
@@ -662,7 +662,7 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
// disable deadlock reporting if we're not running in debug mode, or if
// we're debugging an integration test. In this latter case, stopping at
// breakpoints and stepping through code can easily take more than 30s.
deadlock.Opts.Disable = !gui.Debug || os.Getenv(components.WAIT_FOR_DEBUGGER_ENV_VAR) == ""
deadlock.Opts.Disable = !gui.Debug || os.Getenv(components.WAIT_FOR_DEBUGGER_ENV_VAR) != ""
if err := gui.Config.ReloadUserConfig(); err != nil {
return nil
+1 -1
View File
@@ -99,7 +99,7 @@ func FileHasConflictMarkers(path string) (bool, error) {
// Efficiently scans through a file looking for merge conflict markers. Returns true if it does
func fileHasConflictMarkersAux(file io.Reader) bool {
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize))
for scanner.Scan() {
line := scanner.Bytes()
-1
View File
@@ -115,7 +115,6 @@ baz
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
assert.EqualValues(t, s.expected, findConflicts(s.content))
})
-1
View File
@@ -122,7 +122,6 @@ func TestNewOrigin(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
assert.EqualValues(t, s.expected, calculateOrigin(s.origin, s.bufferHeight, s.numLines, s.firstLineIdx, s.lastLineIdx, s.selectedLineIdx, s.selectMode))
})
+9 -7
View File
@@ -104,13 +104,15 @@ func (self *PopupHandler) Confirm(opts types.ConfirmOpts) error {
func (self *PopupHandler) Prompt(opts types.PromptOpts) error {
return self.createPopupPanelFn(context.Background(), types.CreatePopupPanelOpts{
Title: opts.Title,
Prompt: opts.InitialContent,
Editable: true,
HandleConfirmPrompt: opts.HandleConfirm,
HandleClose: opts.HandleClose,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
Mask: opts.Mask,
Title: opts.Title,
Prompt: opts.InitialContent,
Editable: true,
HandleConfirmPrompt: opts.HandleConfirm,
HandleClose: opts.HandleClose,
HandleDeleteSuggestion: opts.HandleDeleteSuggestion,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
AllowEditSuggestion: opts.AllowEditSuggestion,
Mask: opts.Mask,
})
}
+4 -4
View File
@@ -196,11 +196,11 @@ func BranchStatus(
}
result := ""
if branch.HasCommitsToPush() {
result = fmt.Sprintf("↑%s", branch.Pushables)
if branch.IsAheadForPull() {
result = fmt.Sprintf("↑%s", branch.AheadForPull)
}
if branch.HasCommitsToPull() {
result = fmt.Sprintf("%s↓%s", result, branch.Pullables)
if branch.IsBehindForPull() {
result = fmt.Sprintf("%s↓%s", result, branch.BehindForPull)
}
return result
+12 -12
View File
@@ -58,8 +58,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
Name: "branch_name",
Recency: "1m",
UpstreamRemote: "origin",
Pushables: "0",
Pullables: "0",
AheadForPull: "0",
BehindForPull: "0",
},
itemOperation: types.ItemOperationNone,
fullDescription: false,
@@ -73,8 +73,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
Name: "branch_name",
Recency: "1m",
UpstreamRemote: "origin",
Pushables: "3",
Pullables: "5",
AheadForPull: "3",
BehindForPull: "5",
},
itemOperation: types.ItemOperationNone,
fullDescription: false,
@@ -99,8 +99,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
CommitHash: "1234567890",
UpstreamRemote: "origin",
UpstreamBranch: "branch_name",
Pushables: "0",
Pullables: "0",
AheadForPull: "0",
BehindForPull: "0",
Subject: "commit title",
},
itemOperation: types.ItemOperationNone,
@@ -144,8 +144,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
Name: "branch_name",
Recency: "1m",
UpstreamRemote: "origin",
Pushables: "0",
Pullables: "0",
AheadForPull: "0",
BehindForPull: "0",
},
itemOperation: types.ItemOperationNone,
fullDescription: false,
@@ -159,8 +159,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
Name: "branch_name",
Recency: "1m",
UpstreamRemote: "origin",
Pushables: "3",
Pullables: "5",
AheadForPull: "3",
BehindForPull: "5",
},
itemOperation: types.ItemOperationNone,
fullDescription: false,
@@ -212,8 +212,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
CommitHash: "1234567890",
UpstreamRemote: "origin",
UpstreamBranch: "branch_name",
Pushables: "0",
Pullables: "0",
AheadForPull: "0",
BehindForPull: "0",
Subject: "commit title",
},
itemOperation: types.ItemOperationNone,
+100 -30
View File
@@ -24,6 +24,7 @@ import (
type pipeSetCacheKey struct {
commitHash string
commitCount int
divergence models.Divergence
}
var (
@@ -78,24 +79,76 @@ func GetCommitListDisplayStrings(
// function expects to be passed the index of the commit in terms of the `commits` slice
var getGraphLine func(int) string
if showGraph {
// this is where the graph begins (may be beyond the TODO commits depending on startIdx,
// but we'll never include TODO commits as part of the graph because it'll be messy)
graphOffset := max(startIdx, rebaseOffset)
if len(commits) > 0 && commits[0].Divergence != models.DivergenceNone {
// Showing a divergence log; we know we don't have any rebasing
// commits in this case. But we need to render separate graphs for
// the Local and Remote sections.
allGraphLines := []string{}
pipeSets := loadPipesets(commits[rebaseOffset:])
pipeSetOffset := max(startIdx-rebaseOffset, 0)
graphPipeSets := pipeSets[pipeSetOffset:max(endIdx-rebaseOffset, 0)]
graphCommits := commits[graphOffset:endIdx]
graphLines := graph.RenderAux(
graphPipeSets,
graphCommits,
selectedCommitHash,
)
getGraphLine = func(idx int) string {
if idx >= graphOffset {
return graphLines[idx-graphOffset]
} else {
return ""
_, localSectionStart, found := lo.FindIndexOf(
commits, func(c *models.Commit) bool { return c.Divergence == models.DivergenceLeft })
if !found {
localSectionStart = len(commits)
}
if localSectionStart > 0 {
// we have some remote commits
pipeSets := loadPipesets(commits[:localSectionStart])
if startIdx < localSectionStart {
// some of the remote commits are visible
start := startIdx
end := min(endIdx, localSectionStart)
graphPipeSets := pipeSets[start:end]
graphCommits := commits[start:end]
graphLines := graph.RenderAux(
graphPipeSets,
graphCommits,
selectedCommitHash,
)
allGraphLines = append(allGraphLines, graphLines...)
}
}
if localSectionStart < len(commits) {
// we have some local commits
pipeSets := loadPipesets(commits[localSectionStart:])
if localSectionStart < endIdx {
// some of the local commits are visible
graphOffset := max(startIdx, localSectionStart)
pipeSetOffset := max(startIdx-localSectionStart, 0)
graphPipeSets := pipeSets[pipeSetOffset : endIdx-localSectionStart]
graphCommits := commits[graphOffset:endIdx]
graphLines := graph.RenderAux(
graphPipeSets,
graphCommits,
selectedCommitHash,
)
allGraphLines = append(allGraphLines, graphLines...)
}
}
getGraphLine = func(idx int) string {
return allGraphLines[idx-startIdx]
}
} else {
// this is where the graph begins (may be beyond the TODO commits depending on startIdx,
// but we'll never include TODO commits as part of the graph because it'll be messy)
graphOffset := max(startIdx, rebaseOffset)
pipeSets := loadPipesets(commits[rebaseOffset:])
pipeSetOffset := max(startIdx-rebaseOffset, 0)
graphPipeSets := pipeSets[pipeSetOffset:max(endIdx-rebaseOffset, 0)]
graphCommits := commits[graphOffset:endIdx]
graphLines := graph.RenderAux(
graphPipeSets,
graphCommits,
selectedCommitHash,
)
getGraphLine = func(idx int) string {
if idx >= graphOffset {
return graphLines[idx-graphOffset]
} else {
return ""
}
}
}
} else {
@@ -205,6 +258,7 @@ func loadPipesets(commits []*models.Commit) [][]*graph.Pipe {
cacheKey := pipeSetCacheKey{
commitHash: commits[0].Hash,
commitCount: len(commits),
divergence: commits[0].Divergence,
}
pipeSets, ok := pipeSetCache[cacheKey]
@@ -312,9 +366,33 @@ func displayCommit(
bisectInfo *git_commands.BisectInfo,
isYouAreHereCommit bool,
) []string {
hashColor := getHashColor(commit, diffName, cherryPickedCommitHashSet, bisectStatus, bisectInfo)
bisectString := getBisectStatusText(bisectStatus, bisectInfo)
hashString := ""
hashColor := getHashColor(commit, diffName, cherryPickedCommitHashSet, bisectStatus, bisectInfo)
hashLength := common.UserConfig.Gui.CommitHashLength
if hashLength >= len(commit.Hash) {
hashString = hashColor.Sprint(commit.Hash)
} else if hashLength > 0 {
hashString = hashColor.Sprint(commit.Hash[:hashLength])
} else if !icons.IsIconEnabled() { // hashLength <= 0
hashString = hashColor.Sprint("*")
}
divergenceString := ""
if commit.Divergence != models.DivergenceNone {
divergenceString = hashColor.Sprint(lo.Ternary(commit.Divergence == models.DivergenceLeft, "↑", "↓"))
} else if icons.IsIconEnabled() {
divergenceString = hashColor.Sprint(icons.IconForCommit(commit))
}
descriptionString := ""
if fullDescription {
descriptionString = style.FgBlue.Sprint(
utils.UnixToDateSmart(now, commit.UnixTimestamp, timeFormat, shortTimeFormat),
)
}
actionString := ""
if commit.Action != models.ActionNone {
todoString := lo.Ternary(commit.Action == models.ActionConflict, "conflict", commit.Action.String())
@@ -368,20 +446,12 @@ func displayCommit(
}
cols := make([]string, 0, 7)
if commit.Divergence != models.DivergenceNone {
cols = append(cols, hashColor.Sprint(lo.Ternary(commit.Divergence == models.DivergenceLeft, "↑", "↓")))
} else if icons.IsIconEnabled() {
cols = append(cols, hashColor.Sprint(icons.IconForCommit(commit)))
}
cols = append(cols, hashColor.Sprint(commit.ShortHash()))
cols = append(cols, bisectString)
if fullDescription {
cols = append(cols, style.FgBlue.Sprint(
utils.UnixToDateSmart(now, commit.UnixTimestamp, timeFormat, shortTimeFormat),
))
}
cols = append(
cols,
divergenceString,
hashString,
bisectString,
descriptionString,
actionString,
authorFunc(commit.AuthorName),
graphLine+mark+tagString+theme.DefaultTextColor.Sprint(name),
+179 -1
View File
@@ -359,6 +359,185 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
hash3 ◯ commit3
`),
},
{
testName: "graph in divergence view - all commits visible",
commits: []*models.Commit{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
},
startIdx: 0,
endIdx: 8,
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↓ hash1r ◯ commit1
↓ hash2r ⏣─╮ commit2
↓ hash3r ◯ │ commit3
↑ hash1l ◯ commit1
↑ hash2l ⏣─╮ commit2
↑ hash3l ◯ │ commit3
↑ hash4l ◯─╯ commit4
↑ hash5l ◯ commit5
`),
},
{
testName: "graph in divergence view - not all remote commits visible",
commits: []*models.Commit{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
},
startIdx: 2,
endIdx: 8,
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↓ hash3r ◯ │ commit3
↑ hash1l ◯ commit1
↑ hash2l ⏣─╮ commit2
↑ hash3l ◯ │ commit3
↑ hash4l ◯─╯ commit4
↑ hash5l ◯ commit5
`),
},
{
testName: "graph in divergence view - not all local commits",
commits: []*models.Commit{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
},
startIdx: 0,
endIdx: 5,
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↓ hash1r ◯ commit1
↓ hash2r ⏣─╮ commit2
↓ hash3r ◯ │ commit3
↑ hash1l ◯ commit1
↑ hash2l ⏣─╮ commit2
`),
},
{
testName: "graph in divergence view - no remote commits visible",
commits: []*models.Commit{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
},
startIdx: 4,
endIdx: 8,
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↑ hash2l ⏣─╮ commit2
↑ hash3l ◯ │ commit3
↑ hash4l ◯─╯ commit4
↑ hash5l ◯ commit5
`),
},
{
testName: "graph in divergence view - no local commits visible",
commits: []*models.Commit{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
},
startIdx: 0,
endIdx: 2,
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↓ hash1r ◯ commit1
↓ hash2r ⏣─╮ commit2
`),
},
{
testName: "graph in divergence view - no remote commits present",
commits: []*models.Commit{
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit4", Hash: "hash4l", Parents: []string{"hash5l"}, Divergence: models.DivergenceLeft},
{Name: "commit5", Hash: "hash5l", Parents: []string{"hash6l"}, Divergence: models.DivergenceLeft},
},
startIdx: 0,
endIdx: 5,
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↑ hash1l ◯ commit1
↑ hash2l ⏣─╮ commit2
↑ hash3l ◯ │ commit3
↑ hash4l ◯─╯ commit4
↑ hash5l ◯ commit5
`),
},
{
testName: "graph in divergence view - no local commits present",
commits: []*models.Commit{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
},
startIdx: 0,
endIdx: 3,
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↓ hash1r ◯ commit1
↓ hash2r ⏣─╮ commit2
↓ hash3r ◯ │ commit3
`),
},
{
testName: "custom time format",
commits: []*models.Commit{
@@ -396,7 +575,6 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
common := utils.NewDummyCommon()
for _, s := range scenarios {
s := s
if !focusing || s.focus {
t.Run(s.testName, func(t *testing.T) {
result := GetCommitListDisplayStrings(
-2
View File
@@ -66,7 +66,6 @@ M file1
defer color.ForceSetColorLevel(oldColorLevel)
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
viewModel := filetree.NewFileTree(func() []*models.File { return s.files }, utils.NewDummyLog(), true)
viewModel.SetTree()
@@ -128,7 +127,6 @@ M file1
defer color.ForceSetColorLevel(oldColorLevel)
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
viewModel := filetree.NewCommitFileTreeViewModel(func() []*models.CommitFile { return s.files }, utils.NewDummyLog(), true)
viewModel.SetRef(&models.Commit{})
-1
View File
@@ -84,7 +84,6 @@ func RenderAux(pipeSets [][]*Pipe, commits []*models.Commit, selectedCommitHash
wg.Add(maxProcs)
for i := 0; i < maxProcs; i++ {
i := i
go func() {
from := i * perProc
to := (i + 1) * perProc
-2
View File
@@ -217,7 +217,6 @@ func TestRenderCommitGraph(t *testing.T) {
defer color.ForceSetColorLevel(oldColorLevel)
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault }
lines := RenderCommitGraph(test.commits, "blah", getStyle)
@@ -454,7 +453,6 @@ func TestRenderPipeSet(t *testing.T) {
defer color.ForceSetColorLevel(oldColorLevel)
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
actualStr := renderPipeSet(test.pipes, "selected", test.prevCommit)
t.Log("actual cells:")
@@ -81,7 +81,6 @@ func TestMenuGenerator(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.test(NewMenuGenerator(utils.NewDummyCommon()).call(s.cmdOut, s.filter, s.valueFormat, s.labelFormat))
})
+100
View File
@@ -0,0 +1,100 @@
package custom_commands
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stefanhaller/git-todo-parser/todo"
)
// We create shims for all the model classes in order to get a more stable API
// for custom commands. At the moment these are almost identical to the model
// classes, but this allows us to add "private" fields to the model classes that
// we don't want to expose to custom commands, or rename a model field to a
// better name without breaking people's custom commands. In such a case we add
// the new, better name to the shim but keep the old one for backwards
// compatibility. We already did this for Commit.Sha, which was renamed to Hash.
type Commit struct {
Hash string // deprecated: use Sha
Sha string
Name string
Status models.CommitStatus
Action todo.TodoCommand
Tags []string
ExtraInfo string
AuthorName string
AuthorEmail string
UnixTimestamp int64
Divergence models.Divergence
Parents []string
}
type File struct {
Name string
PreviousName string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Added bool
Deleted bool
HasMergeConflicts bool
HasInlineMergeConflicts bool
DisplayString string
ShortStatus string
IsWorktree bool
}
type Branch struct {
Name string
DisplayName string
Recency string
Pushables string // deprecated: use AheadForPull
Pullables string // deprecated: use BehindForPull
AheadForPull string
BehindForPull string
AheadForPush string
BehindForPush string
UpstreamGone bool
Head bool
DetachedHead bool
UpstreamRemote string
UpstreamBranch string
Subject string
CommitHash string
}
type RemoteBranch struct {
Name string
RemoteName string
}
type Remote struct {
Name string
Urls []string
Branches []*RemoteBranch
}
type Tag struct {
Name string
Message string
}
type StashEntry struct {
Index int
Recency string
Name string
}
type CommitFile struct {
Name string
ChangeStatus string
}
type Worktree struct {
IsMain bool
IsCurrent bool
Path string
IsPathMissing bool
GitDir string
Branch string
Name string
}
@@ -72,7 +72,6 @@ func (self *Resolver) resolvePrompt(
func (self *Resolver) resolveMenuOptions(prompt *config.CustomCommandPrompt, resolveTemplate func(string) (string, error)) ([]config.CustomCommandMenuOption, error) {
newOptions := make([]config.CustomCommandMenuOption, 0, len(prompt.Options))
for _, option := range prompt.Options {
option := option
newOption, err := self.resolveMenuOption(&option, resolveTemplate)
if err != nil {
return nil, err
@@ -3,7 +3,7 @@ package custom_commands
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/stefanhaller/git-todo-parser/todo"
"github.com/samber/lo"
)
// loads the session state at the time that a custom command is invoked, for use
@@ -20,22 +20,7 @@ func NewSessionStateLoader(c *helpers.HelperCommon, refsHelper *helpers.RefsHelp
}
}
type Commit struct {
Hash string
Sha string
Name string
Status models.CommitStatus
Action todo.TodoCommand
Tags []string
ExtraInfo string
AuthorName string
AuthorEmail string
UnixTimestamp int64
Divergence models.Divergence
Parents []string
}
func commitWrapperFromModelCommit(commit *models.Commit) *Commit {
func commitShimFromModelCommit(commit *models.Commit) *Commit {
if commit == nil {
return nil
}
@@ -56,39 +41,160 @@ func commitWrapperFromModelCommit(commit *models.Commit) *Commit {
}
}
func fileShimFromModelFile(file *models.File) *File {
if file == nil {
return nil
}
return &File{
Name: file.Name,
PreviousName: file.PreviousName,
HasStagedChanges: file.HasStagedChanges,
HasUnstagedChanges: file.HasUnstagedChanges,
Tracked: file.Tracked,
Added: file.Added,
Deleted: file.Deleted,
HasMergeConflicts: file.HasMergeConflicts,
HasInlineMergeConflicts: file.HasInlineMergeConflicts,
DisplayString: file.DisplayString,
ShortStatus: file.ShortStatus,
IsWorktree: file.IsWorktree,
}
}
func branchShimFromModelBranch(branch *models.Branch) *Branch {
if branch == nil {
return nil
}
return &Branch{
Name: branch.Name,
DisplayName: branch.DisplayName,
Recency: branch.Recency,
Pushables: branch.AheadForPull,
Pullables: branch.BehindForPull,
AheadForPull: branch.AheadForPull,
BehindForPull: branch.BehindForPull,
AheadForPush: branch.AheadForPush,
BehindForPush: branch.BehindForPush,
UpstreamGone: branch.UpstreamGone,
Head: branch.Head,
DetachedHead: branch.DetachedHead,
UpstreamRemote: branch.UpstreamRemote,
UpstreamBranch: branch.UpstreamBranch,
Subject: branch.Subject,
CommitHash: branch.CommitHash,
}
}
func remoteBranchShimFromModelRemoteBranch(remoteBranch *models.RemoteBranch) *RemoteBranch {
if remoteBranch == nil {
return nil
}
return &RemoteBranch{
Name: remoteBranch.Name,
RemoteName: remoteBranch.RemoteName,
}
}
func remoteShimFromModelRemote(remote *models.Remote) *Remote {
if remote == nil {
return nil
}
return &Remote{
Name: remote.Name,
Urls: remote.Urls,
Branches: lo.Map(remote.Branches, func(branch *models.RemoteBranch, _ int) *RemoteBranch {
return remoteBranchShimFromModelRemoteBranch(branch)
}),
}
}
func tagShimFromModelRemote(tag *models.Tag) *Tag {
if tag == nil {
return nil
}
return &Tag{
Name: tag.Name,
Message: tag.Message,
}
}
func stashEntryShimFromModelRemote(stashEntry *models.StashEntry) *StashEntry {
if stashEntry == nil {
return nil
}
return &StashEntry{
Index: stashEntry.Index,
Recency: stashEntry.Recency,
Name: stashEntry.Name,
}
}
func commitFileShimFromModelRemote(commitFile *models.CommitFile) *CommitFile {
if commitFile == nil {
return nil
}
return &CommitFile{
Name: commitFile.Name,
ChangeStatus: commitFile.ChangeStatus,
}
}
func worktreeShimFromModelRemote(worktree *models.Worktree) *Worktree {
if worktree == nil {
return nil
}
return &Worktree{
IsMain: worktree.IsMain,
IsCurrent: worktree.IsCurrent,
Path: worktree.Path,
IsPathMissing: worktree.IsPathMissing,
GitDir: worktree.GitDir,
Branch: worktree.Branch,
Name: worktree.Name,
}
}
// SessionState captures the current state of the application for use in custom commands
type SessionState struct {
SelectedLocalCommit *Commit
SelectedReflogCommit *Commit
SelectedSubCommit *Commit
SelectedFile *models.File
SelectedFile *File
SelectedPath string
SelectedLocalBranch *models.Branch
SelectedRemoteBranch *models.RemoteBranch
SelectedRemote *models.Remote
SelectedTag *models.Tag
SelectedStashEntry *models.StashEntry
SelectedCommitFile *models.CommitFile
SelectedLocalBranch *Branch
SelectedRemoteBranch *RemoteBranch
SelectedRemote *Remote
SelectedTag *Tag
SelectedStashEntry *StashEntry
SelectedCommitFile *CommitFile
SelectedCommitFilePath string
SelectedWorktree *models.Worktree
CheckedOutBranch *models.Branch
SelectedWorktree *Worktree
CheckedOutBranch *Branch
}
func (self *SessionStateLoader) call() *SessionState {
return &SessionState{
SelectedFile: self.c.Contexts().Files.GetSelectedFile(),
SelectedFile: fileShimFromModelFile(self.c.Contexts().Files.GetSelectedFile()),
SelectedPath: self.c.Contexts().Files.GetSelectedPath(),
SelectedLocalCommit: commitWrapperFromModelCommit(self.c.Contexts().LocalCommits.GetSelected()),
SelectedReflogCommit: commitWrapperFromModelCommit(self.c.Contexts().ReflogCommits.GetSelected()),
SelectedLocalBranch: self.c.Contexts().Branches.GetSelected(),
SelectedRemoteBranch: self.c.Contexts().RemoteBranches.GetSelected(),
SelectedRemote: self.c.Contexts().Remotes.GetSelected(),
SelectedTag: self.c.Contexts().Tags.GetSelected(),
SelectedStashEntry: self.c.Contexts().Stash.GetSelected(),
SelectedCommitFile: self.c.Contexts().CommitFiles.GetSelectedFile(),
SelectedLocalCommit: commitShimFromModelCommit(self.c.Contexts().LocalCommits.GetSelected()),
SelectedReflogCommit: commitShimFromModelCommit(self.c.Contexts().ReflogCommits.GetSelected()),
SelectedLocalBranch: branchShimFromModelBranch(self.c.Contexts().Branches.GetSelected()),
SelectedRemoteBranch: remoteBranchShimFromModelRemoteBranch(self.c.Contexts().RemoteBranches.GetSelected()),
SelectedRemote: remoteShimFromModelRemote(self.c.Contexts().Remotes.GetSelected()),
SelectedTag: tagShimFromModelRemote(self.c.Contexts().Tags.GetSelected()),
SelectedStashEntry: stashEntryShimFromModelRemote(self.c.Contexts().Stash.GetSelected()),
SelectedCommitFile: commitFileShimFromModelRemote(self.c.Contexts().CommitFiles.GetSelectedFile()),
SelectedCommitFilePath: self.c.Contexts().CommitFiles.GetSelectedPath(),
SelectedSubCommit: commitWrapperFromModelCommit(self.c.Contexts().SubCommits.GetSelected()),
SelectedWorktree: self.c.Contexts().Worktrees.GetSelected(),
CheckedOutBranch: self.refsHelper.GetCheckedOutRef(),
SelectedSubCommit: commitShimFromModelCommit(self.c.Contexts().SubCommits.GetSelected()),
SelectedWorktree: worktreeShimFromModelRemote(self.c.Contexts().Worktrees.GetSelected()),
CheckedOutBranch: branchShimFromModelBranch(self.refsHelper.GetCheckedOutRef()),
}
}
-2
View File
@@ -161,7 +161,6 @@ func TestMerge(t *testing.T) {
defer color.ForceSetColorLevel(oldColorLevel)
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
style := New()
for _, other := range s.toMerge {
@@ -212,7 +211,6 @@ func TestTemplateFuncMapAddColors(t *testing.T) {
defer color.ForceSetColorLevel(oldColorLevel)
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
tmpl, err := template.New("test template").Funcs(TemplateFuncMapAddColors(template.FuncMap{})).Parse(s.tmpl)
assert.NoError(t, err)
+13 -9
View File
@@ -165,16 +165,18 @@ type CreateMenuOptions struct {
}
type CreatePopupPanelOpts struct {
HasLoader bool
Editable bool
Title string
Prompt string
HandleConfirm func() error
HandleConfirmPrompt func(string) error
HandleClose func() error
HasLoader bool
Editable bool
Title string
Prompt string
HandleConfirm func() error
HandleConfirmPrompt func(string) error
HandleClose func() error
HandleDeleteSuggestion func(int) error
FindSuggestionsFunc func(string) []*Suggestion
Mask bool
AllowEditSuggestion bool
}
type ConfirmOpts struct {
@@ -192,9 +194,11 @@ type PromptOpts struct {
InitialContent string
FindSuggestionsFunc func(string) []*Suggestion
HandleConfirm func(string) error
AllowEditSuggestion bool
// CAPTURE THIS
HandleClose func() error
Mask bool
HandleClose func() error
HandleDeleteSuggestion func(int) error
Mask bool
}
type MenuSection struct {
+2
View File
@@ -631,6 +631,7 @@ type TranslationSet struct {
SuggestionsCheatsheetTitle string
// Unlike the cheatsheet title above, the real suggestions title has a little message saying press tab to focus
SuggestionsTitle string
SuggestionsSubtitle string
ExtrasTitle string
PushingTagStatus string
PullRequestURLCopiedToClipboard string
@@ -1593,6 +1594,7 @@ func EnglishTranslationSet() TranslationSet {
NavigationTitle: "List panel navigation",
SuggestionsCheatsheetTitle: "Suggestions",
SuggestionsTitle: "Suggestions (press %s to focus)",
SuggestionsSubtitle: "(press %s to delete, %s to edit)",
ExtrasTitle: "Command log",
PushingTagStatus: "Pushing tag",
PullRequestURLCopiedToClipboard: "Pull request URL copied to clipboard",
@@ -82,3 +82,21 @@ func (self *PromptDriver) ConfirmSuggestion(matcher *TextMatcher) {
NavigateToLine(matcher).
PressEnter()
}
func (self *PromptDriver) DeleteSuggestion(matcher *TextMatcher) *PromptDriver {
self.t.press(self.t.keys.Universal.TogglePanel)
self.t.Views().Suggestions().
IsFocused().
NavigateToLine(matcher)
self.t.press(self.t.keys.Universal.Remove)
return self
}
func (self *PromptDriver) EditSuggestion(matcher *TextMatcher) *PromptDriver {
self.t.press(self.t.keys.Universal.TogglePanel)
self.t.Views().Suggestions().
IsFocused().
NavigateToLine(matcher)
self.t.press(self.t.keys.Universal.Edit)
return self
}
-2
View File
@@ -48,8 +48,6 @@ func RunTests(args RunTestArgs) error {
}
for _, test := range args.Tests {
test := test
args.TestWrapper(test, func() error { //nolint: thelper
paths := NewPaths(
filepath.Join(testDir, test.Name()),
+4
View File
@@ -195,6 +195,10 @@ func (self *Shell) CreateAnnotatedTag(name string, message string, ref string) *
}
func (self *Shell) PushBranch(upstream, branch string) *Shell {
return self.RunCommand([]string{"git", "push", upstream, branch})
}
func (self *Shell) PushBranchAndSetUpstream(upstream, branch string) *Shell {
return self.RunCommand([]string{"git", "push", "--set-upstream", upstream, branch})
}
+2 -2
View File
@@ -15,9 +15,9 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
CloneIntoRemote("origin").
EmptyCommit("blah").
NewBranch("branch-one").
PushBranch("origin", "branch-one").
PushBranchAndSetUpstream("origin", "branch-one").
NewBranch("branch-two").
PushBranch("origin", "branch-two").
PushBranchAndSetUpstream("origin", "branch-two").
EmptyCommit("deletion blocker").
NewBranch("branch-three")
},
@@ -18,7 +18,7 @@ var DeleteRemoteBranchWithCredentialPrompt = NewIntegrationTest(NewIntegrationTe
shell.NewBranch("mybranch")
shell.PushBranch("origin", "mybranch")
shell.PushBranchAndSetUpstream("origin", "mybranch")
// actually getting a password prompt is tricky: it requires SSH'ing into localhost under a newly created, restricted, user.
// This is not easy to do in a cross-platform way, nor is it easy to do in a docker container.
@@ -15,7 +15,7 @@ var RebaseToUpstream = NewIntegrationTest(NewIntegrationTestArgs{
CloneIntoRemote("origin").
EmptyCommit("ensure-master").
EmptyCommit("to-be-added"). // <- this will only exist remotely
PushBranch("origin", "master").
PushBranchAndSetUpstream("origin", "master").
HardReset("HEAD~1").
NewBranchFrom("base-branch", "master").
EmptyCommit("base-branch-commit").
@@ -15,10 +15,10 @@ var ResetToUpstream = NewIntegrationTest(NewIntegrationTestArgs{
CloneIntoRemote("origin").
NewBranch("hard-branch").
EmptyCommit("hard commit").
PushBranch("origin", "hard-branch").
PushBranchAndSetUpstream("origin", "hard-branch").
NewBranch("soft-branch").
EmptyCommit("soft commit").
PushBranch("origin", "soft-branch").
PushBranchAndSetUpstream("origin", "soft-branch").
NewBranch("base").
EmptyCommit("base-branch commit").
CreateFile("file-1", "content").
@@ -1,71 +0,0 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
const preCommitHook = `#!/bin/bash
exit 1
`
var CommitSkipHook = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Commit with pre-commit hook and skip hook config option in various situations.",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(testConfig *config.AppConfig) {
testConfig.UserConfig.Git.SkipHookPrefix = "skip! "
},
SetupRepo: func(shell *Shell) {
shell.SetConfig("user.email", "Bill@example.com")
shell.SetConfig("user.name", "Bill Smith")
shell.CreateFileAndAdd("initial file", "initial content")
shell.Commit("initial commit")
shell.SetConfig("user.email", "John@example.com")
shell.SetConfig("user.name", "John Smith")
shell.CreateFile(".git/hooks/pre-commit", preCommitHook)
shell.MakeExecutable(".git/hooks/pre-commit")
shell.CreateFileAndAdd("testfile", "I'm just testing pre-commit hooks")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
Focus().
Press(keys.Files.CommitChanges)
// hook should trigger when creating a regular commit
t.ExpectPopup().CommitMessagePanel().Type("my commit message").Confirm()
t.ExpectPopup().Alert().Title(Equals("Error")).Content(Contains("Git command failed")).Confirm()
t.Views().Files().
Focus().
Press(keys.Files.CommitChanges)
// we should be able to skip hooks when creating a regular commit
t.ExpectPopup().CommitMessagePanel().Clear().Type("skip! my commit message").Confirm()
t.Views().Commits().Focus().Lines(
Contains("skip! my commit message"),
Contains("initial commit"),
)
// we should be able to skip hooks when rewording a commit
t.Views().Commits().Focus().Press(keys.Commits.RenameCommit)
t.ExpectPopup().CommitMessagePanel().Type(" (reworded)").Confirm()
t.Views().Commits().IsFocused().
Lines(
Contains("skip! my commit message (reworded)"),
Contains("initial commit"),
)
// we should be able to skip hooks when changing authors
t.Views().Commits().IsFocused().SelectedLine(Contains("JS").IsSelected())
t.Views().Commits().Focus().Press(keys.Commits.ResetCommitAuthor)
t.Views().Commits().IsFocused().Lines(Contains("JS").IsSelected())
},
})
@@ -0,0 +1,47 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CommitWithGlobalPrefix = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Commit with defined config commitPrefix",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(testConfig *config.AppConfig) {
testConfig.UserConfig.Git.CommitPrefix = &config.CommitPrefixConfig{Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[$1]: "}
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("feature/TEST-001")
shell.CreateFile("test-commit-prefix", "This is foo bar")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
IsEmpty()
t.Views().Files().
IsFocused().
PressPrimaryAction().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Title(Equals("Commit summary")).
InitialText(Equals("[TEST-001]: ")).
Type("my commit message").
Cancel()
t.Views().Files().
IsFocused().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Title(Equals("Commit summary")).
InitialText(Equals("[TEST-001]: my commit message")).
Type(". Added something else").
Confirm()
t.Views().Commits().Focus()
t.Views().Main().Content(Contains("[TEST-001]: my commit message. Added something else"))
},
})
@@ -6,7 +6,7 @@ import (
)
var CommitWithPrefix = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Commit with defined config commitPrefix",
Description: "Commit with defined config commitPrefixes",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(testConfig *config.AppConfig) {
@@ -0,0 +1,41 @@
package custom_commands
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DeleteFromHistory = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Delete an entry from the custom commands history",
ExtraCmdArgs: []string{},
Skip: false,
SetupRepo: func(shell *Shell) {},
SetupConfig: func(cfg *config.AppConfig) {},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
createCustomCommand := func(command string) {
t.GlobalPress(keys.Universal.ExecuteCustomCommand)
t.ExpectPopup().Prompt().
Title(Equals("Custom command:")).
Type(command).
Confirm()
}
createCustomCommand("echo 1")
createCustomCommand("echo 2")
createCustomCommand("echo 3")
t.GlobalPress(keys.Universal.ExecuteCustomCommand)
t.ExpectPopup().Prompt().
Title(Equals("Custom command:")).
SuggestionLines(
Contains("3"),
Contains("2"),
Contains("1"),
).
DeleteSuggestion(Contains("2")).
SuggestionLines(
Contains("3"),
Contains("1"),
)
},
})
@@ -0,0 +1,31 @@
package custom_commands
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var EditHistory = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Edit an entry from the custom commands history",
ExtraCmdArgs: []string{},
Skip: false,
SetupRepo: func(shell *Shell) {},
SetupConfig: func(cfg *config.AppConfig) {},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.GlobalPress(keys.Universal.ExecuteCustomCommand)
t.ExpectPopup().Prompt().
Title(Equals("Custom command:")).
Type("echo x").
Confirm()
t.GlobalPress(keys.Universal.ExecuteCustomCommand)
t.ExpectPopup().Prompt().
Title(Equals("Custom command:")).
Type("ec").
SuggestionLines(
Equals("echo x"),
).
EditSuggestion(Equals("echo x")).
InitialText(Equals("echo x"))
},
})
@@ -0,0 +1,70 @@
package stash
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var StashStagedPartialFile = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Stash staged changes when a file is partially staged",
ExtraCmdArgs: []string{},
GitVersion: AtLeast("git version 2.35.0"),
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file-staged", "line1\nline2\nline3\nline4\n")
shell.Commit("initial commit")
shell.UpdateFile("file-staged", "line1\nline2 mod\nline3\nline4 mod\n")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
PressEnter()
t.Views().Staging().
Content(
Contains(" line1\n-line2\n+line2 mod\n line3\n-line4\n+line4 mod\n"),
).
PressPrimaryAction().
PressPrimaryAction().
Content(
Contains(" line1\n line2 mod\n line3\n-line4\n+line4 mod\n"),
).
PressEscape()
t.Views().Files().
IsFocused().
Press(keys.Files.ViewStashOptions)
t.ExpectPopup().Menu().Title(Equals("Stash options")).Select(MatchesRegexp("Stash staged changes$")).Confirm()
t.ExpectPopup().Prompt().Title(Equals("Stash changes")).Type("my stashed file").Confirm()
t.Views().Stash().
Focus().
Lines(
Contains("my stashed file"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file-staged").IsSelected(),
)
t.Views().Main().
Content(
Contains(" line1\n-line2\n+line2 mod\n line3\n line4\n"),
)
t.Views().Files().
Lines(
Contains("file-staged"),
)
t.Views().Staging().
Content(
Contains(" line1\n line2\n line3\n-line4\n+line4 mod\n"),
)
},
})
@@ -0,0 +1,18 @@
package status
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ClickRepoNameToOpenReposMenu = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Click on the repo name in the status side panel to open the recent repositories menu",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Status().Click(1, 0)
t.ExpectPopup().Menu().Title(Equals("Recent repositories"))
},
})
@@ -0,0 +1,35 @@
package status
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ClickToFocus = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Click in the status side panel to activate it",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().Focus()
t.Views().Main().Lines(
Contains("No changed files"),
)
t.Views().Status().Click(0, 0)
t.Views().Status().IsFocused()
t.Views().Main().ContainsLines(
Contains(` _`),
Contains(` | | (_) |`),
Contains(` | | __ _ _____ _ __ _ _| |_`),
Contains(" | |/ _` |_ / | | |/ _` | | __|"),
Contains(` | | (_| |/ /| |_| | (_| | | |_`),
Contains(` |_|\__,_/___|\__, |\__, |_|\__|`),
Contains(` __/ | __/ |`),
Contains(` |___/ |___/`),
Contains(``),
Contains(`Copyright `),
)
},
})
@@ -0,0 +1,27 @@
package status
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ClickWorkingTreeStateToOpenRebaseOptionsMenu = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Click on the working tree state in the status side panel to open the rebase options menu",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateNCommits(2)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Press(keys.Universal.Edit)
t.Views().Status().
Content(Contains("(rebasing) repo")).
Click(1, 0)
t.ExpectPopup().Menu().Title(Equals("Rebase options"))
},
})
@@ -0,0 +1,65 @@
package sync
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ForcePushTriangular = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Push to a remote, requiring a force push because the branch is behind the remote push branch but not the upstream",
ExtraCmdArgs: []string{},
Skip: false,
GitVersion: AtLeast("2.22.0"),
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.SetConfig("push.default", "current")
shell.EmptyCommit("one")
shell.CloneIntoRemote("origin")
shell.NewBranch("feature")
shell.SetBranchUpstream("feature", "origin/master")
shell.EmptyCommit("two")
shell.PushBranch("origin", "feature")
// remove the 'two' commit so that we are behind the push branch
shell.HardReset("HEAD^")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Lines(
Contains("one"),
)
t.Views().Status().Content(Contains("✓ repo → feature"))
t.Views().Files().IsFocused().Press(keys.Universal.Push)
t.ExpectPopup().Confirmation().
Title(Equals("Force push")).
Content(Equals("Your branch has diverged from the remote branch. Press <esc> to cancel, or <enter> to force push.")).
Confirm()
t.Views().Commits().
Lines(
Contains("one"),
)
t.Views().Status().Content(Contains("✓ repo → feature"))
t.Views().Remotes().Focus().
Lines(Contains("origin")).
PressEnter()
t.Views().RemoteBranches().IsFocused().
Lines(
Contains("feature"),
Contains("master"),
).
PressEnter()
t.Views().SubCommits().IsFocused().
Lines(Contains("one"))
},
})
+9 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/integration/tests/reflog"
"github.com/jesseduffield/lazygit/pkg/integration/tests/staging"
"github.com/jesseduffield/lazygit/pkg/integration/tests/stash"
"github.com/jesseduffield/lazygit/pkg/integration/tests/status"
"github.com/jesseduffield/lazygit/pkg/integration/tests/submodule"
"github.com/jesseduffield/lazygit/pkg/integration/tests/sync"
"github.com/jesseduffield/lazygit/pkg/integration/tests/tag"
@@ -71,8 +72,8 @@ var tests = []*components.IntegrationTest{
commit.Commit,
commit.CommitMultiline,
commit.CommitSwitchToEditor,
commit.CommitSkipHook,
commit.CommitWipWithPrefix,
commit.CommitWithGlobalPrefix,
commit.CommitWithPrefix,
commit.CreateAmendCommit,
commit.CreateTag,
@@ -104,6 +105,8 @@ var tests = []*components.IntegrationTest{
custom_commands.BasicCmdFromConfig,
custom_commands.CheckForConflicts,
custom_commands.ComplexCmdAtRuntime,
custom_commands.DeleteFromHistory,
custom_commands.EditHistory,
custom_commands.FormPrompts,
custom_commands.History,
custom_commands.MenuFromCommand,
@@ -256,7 +259,11 @@ var tests = []*components.IntegrationTest{
stash.StashAndKeepIndex,
stash.StashIncludingUntrackedFiles,
stash.StashStaged,
stash.StashStagedPartialFile,
stash.StashUnstaged,
status.ClickRepoNameToOpenReposMenu,
status.ClickToFocus,
status.ClickWorkingTreeStateToOpenRebaseOptionsMenu,
submodule.Add,
submodule.Enter,
submodule.EnterNested,
@@ -268,6 +275,7 @@ var tests = []*components.IntegrationTest{
sync.ForcePush,
sync.ForcePushMultipleMatching,
sync.ForcePushMultipleUpstream,
sync.ForcePushTriangular,
sync.Pull,
sync.PullAndSetUpstream,
sync.PullMerge,
+1 -1
View File
@@ -162,7 +162,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
done := make(chan struct{})
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanLines)
scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize))
lineChan := make(chan []byte)
lineWrittenChan := make(chan struct{})
+58 -1
View File
@@ -1,6 +1,9 @@
package utils
import "strings"
import (
"bytes"
"strings"
)
// SplitLines takes a multiline string and splits it on newlines
// currently we are also stripping \r's which may have adverse effects for
@@ -43,3 +46,57 @@ func EscapeSpecialChars(str string) string {
"\v", "\\v",
).Replace(str)
}
func dropCR(data []byte) []byte {
if len(data) > 0 && data[len(data)-1] == '\r' {
return data[0 : len(data)-1]
}
return data
}
// ScanLinesAndTruncateWhenLongerThanBuffer returns a split function that can be
// used with bufio.Scanner.Split(). It is very similar to bufio.ScanLines,
// except that it will truncate lines that are longer than the scanner's read
// buffer (whereas bufio.ScanLines will return an error in that case, which is
// often difficult to handle).
//
// If you are using your own buffer for the scanner, you must set maxBufferSize
// to the same value as the max parameter that you passed to scanner.Buffer().
// Otherwise, maxBufferSize must be set to bufio.MaxScanTokenSize.
func ScanLinesAndTruncateWhenLongerThanBuffer(maxBufferSize int) func(data []byte, atEOF bool) (int, []byte, error) {
skipOverRemainderOfLongLine := false
return func(data []byte, atEOF bool) (int, []byte, error) {
if atEOF && len(data) == 0 {
// Done
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
if skipOverRemainderOfLongLine {
skipOverRemainderOfLongLine = false
return i + 1, nil, nil
}
return i + 1, dropCR(data[0:i]), nil
}
if atEOF {
if skipOverRemainderOfLongLine {
return len(data), nil, nil
}
return len(data), dropCR(data), nil
}
// Buffer is full, so we can't get more data
if len(data) >= maxBufferSize {
if skipOverRemainderOfLongLine {
return len(data), nil, nil
}
skipOverRemainderOfLongLine = true
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
}
+64
View File
@@ -1,6 +1,8 @@
package utils
import (
"bufio"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -100,3 +102,65 @@ func TestNormalizeLinefeeds(t *testing.T) {
assert.EqualValues(t, string(s.expected), NormalizeLinefeeds(string(s.byteArray)))
}
}
func TestScanLinesAndTruncateWhenLongerThanBuffer(t *testing.T) {
type scenario struct {
input string
expectedLines []string
}
scenarios := []scenario{
{
"",
[]string{},
},
{
"\n",
[]string{""},
},
{
"abc",
[]string{"abc"},
},
{
"abc\ndef",
[]string{"abc", "def"},
},
{
"abc\n\ndef",
[]string{"abc", "", "def"},
},
{
"abc\r\ndef\r",
[]string{"abc", "def"},
},
{
"abcdef",
[]string{"abcde"},
},
{
"abcdef\n",
[]string{"abcde"},
},
{
"abcdef\nghijkl\nx",
[]string{"abcde", "ghijk", "x"},
},
{
"abc\ndefghijklmnopqrstuvw\nx",
[]string{"abc", "defgh", "x"},
},
}
for _, s := range scenarios {
scanner := bufio.NewScanner(strings.NewReader(s.input))
scanner.Buffer(make([]byte, 5), 5)
scanner.Split(ScanLinesAndTruncateWhenLongerThanBuffer(5))
result := []string{}
for scanner.Scan() {
result = append(result, scanner.Text())
}
assert.NoError(t, scanner.Err())
assert.EqualValues(t, s.expectedLines, result)
}
}
-4
View File
@@ -49,7 +49,6 @@ func TestNextIndex(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, NextIndex(s.list, s.element))
})
@@ -93,7 +92,6 @@ func TestPrevIndex(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, PrevIndex(s.list, s.element))
})
@@ -126,7 +124,6 @@ func TestEscapeSpecialChars(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, EscapeSpecialChars(s.input))
})
@@ -303,7 +300,6 @@ func TestMoveElement(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, MoveElement(s.list, s.from, s.to))
})
-1
View File
@@ -102,7 +102,6 @@ func TestUpdateYamlValue(t *testing.T) {
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
out, actualErr := UpdateYamlValue([]byte(test.in), test.path, test.value)
if test.expectedErr == "" {
+29
View File
@@ -309,6 +309,12 @@
"description": "If true (default), file icons are shown in the file views. Only relevant if NerdFontsVersion is not empty.",
"default": true
},
"commitHashLength": {
"type": "integer",
"minimum": 0,
"description": "Length of commit hash in commits view. 0 shows '*' if NF icons aren't on.",
"default": 8
},
"showBranchCommitHash": {
"type": "boolean",
"description": "If true, show commit hashes alongside branch names in the branches view."
@@ -533,6 +539,29 @@
"type": "boolean",
"description": "If true, do not allow force pushes"
},
"commitPrefix": {
"properties": {
"pattern": {
"type": "string",
"minLength": 1,
"description": "pattern to match on. E.g. for 'feature/AB-123' to match on the AB-123 use \"^\\\\w+\\\\/(\\\\w+-\\\\w+).*\"",
"examples": [
"^\\w+\\/(\\w+-\\w+).*"
]
},
"replace": {
"type": "string",
"minLength": 1,
"description": "Replace directive. E.g. for 'feature/AB-123' to start the commit message with 'AB-123 ' use \"[$1] \"",
"examples": [
"[$1] "
]
}
},
"additionalProperties": false,
"type": "object",
"description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix"
},
"commitPrefixes": {
"additionalProperties": {
"properties": {