Compare commits

..
238 Commits
Author SHA1 Message Date
README-bot 5e388e21c8 Updated README.md 2023-08-07 14:03:45 +00:00
Jesse Duffield 4c89f9fba5 Fix seg-fault when opening submodule in nested folder (#2903) 2023-08-08 00:03:28 +10:00
Jesse Duffield 8e7958f78b Fix seg-fault when opening submodule in nested folder 2023-08-07 23:57:28 +10:00
Jesse Duffield 579791e7bc Fix opening lazygit in a bare repo with specified worktree (#2902) 2023-08-07 22:48:14 +10:00
Jesse Duffield 595e28d335 Support bare worktrees where worktree does not have its own .git file
This was on oversight on my part: I assumed that the --work-tree arg was
always intended for use with linked worktrees which have a .git file
pointing back to the repo.

I'm honestly confused now: seems like there are three kinds of worktrees:
* the main worktree of a non-bare repo
* a linked worktree (with its own gitdir in the repo's worktrees/ dir)
* a random folder which you specify as a worktree with the --work-tree arg

I'm pretty sure the --work-tree arg is only intended to be used with this
third kind or workree
2023-08-07 22:40:53 +10:00
Jesse Duffield 0551f29de9 Test bare repos with dotfile setup 2023-08-07 22:21:23 +10:00
Jesse Duffield ecaf1e9002 Use nerdfont version 3 in demos (#2901) 2023-08-07 21:23:42 +10:00
Jesse Duffield 69ff621494 Use nerdfont version 3
Turns out I was on version 2 when recording these. I've switched
to v3 now.
2023-08-07 21:19:10 +10:00
Stefan Haller 2ded352f3a Don't run the check-required-label check on master (#2896) 2023-08-07 08:45:48 +02:00
Stefan Haller c877a8528f Don't run the check-required-label check on master
master doesn't have a label, so CI for master currently always fails.
2023-08-07 08:41:13 +02:00
Stefan Haller d1754a9490 update worktree icons for NerdFont V3 (#2895) 2023-08-07 08:16:42 +02:00
README-bot 33a96a3dd7 Updated README.md 2023-08-07 06:14:41 +00:00
Stefan Haller cd889adfa0 Import stefanhaller's tcell fork for real rather than just replacing it (#2888) 2023-08-07 08:14:28 +02:00
Saafo 77447ea6c3 👽️ update worktree icons for NerdFont V3 2023-08-07 05:47:57 +00:00
Stefan Haller 7ccb871a45 Bump gocui
... and import stefanhaller's tcell fork for real rather than just replacing it

This solves the problem that people trying to
"go install github.com/jesseduffield/lazygit@latest" would get the error

go: github.com/jesseduffield/lazygit@latest (in github.com/jesseduffield/lazygit@v0.40.0):
  The go.mod file for the module providing named packages contains one or
  more replace directives. It must not contain directives that would cause
  it to be interpreted differently than if it were the main module.
2023-08-06 12:03:23 +02:00
README-bot 9cc18bd7e6 Updated README.md 2023-08-06 06:29:45 +00:00
Stefan Haller 8e2ecd17a9 Fix README.md typo backkground -> background (#2884) 2023-08-06 08:29:32 +02:00
Mattias Johnson 78676dcff6 Fix README.md typo backkground -> background 2023-08-05 20:09:53 +02:00
README-bot 7847203d6e Updated README.md 2023-08-05 06:14:48 +00:00
Jesse Duffield af3101c7f2 Fix confirmation view sizing (#2879) 2023-08-05 16:14:34 +10:00
Jesse Duffield ec7e9f9228 Fix confirmation view sizing
The proper fix is to actually have these two functions share code,
or for views to be able to manage their own heights based on their contents.

But I want to get this out for the sake of a Lazygit Anniversary release.
2023-08-05 16:09:02 +10:00
Jesse Duffield ab5875c78f Add more demos (#2874) 2023-08-04 09:18:25 +10:00
Jesse Duffield f6af4c29d4 Add custom patch demo 2023-08-04 09:15:07 +10:00
Jesse Duffield 996ad5bf26 Add worktree demo 2023-08-04 09:14:58 +10:00
Jesse Duffield 096628e366 Add custom command demo 2023-08-04 08:38:15 +10:00
Jesse Duffield b432fb5efe Add undo/redo demo 2023-08-03 21:02:12 +10:00
Jesse Duffield c390c9d58e Add filter demo to README 2023-08-03 18:22:30 +10:00
Jesse Duffield e25403c650 Move features to top of readme (#2867) 2023-08-03 17:25:48 +10:00
Jesse Duffield 5c9a9e777c Move features to top of readme 2023-08-03 17:23:47 +10:00
Jesse Duffield 39d24fdeb7 Add more demos to the README (#2866) 2023-08-03 17:02:40 +10:00
Jesse Duffield bbb2b32816 Point to new demos 2023-08-03 16:58:11 +10:00
Jesse Duffield 0152639ef0 Add demo for staging lines 2023-08-02 22:32:57 +10:00
Jesse Duffield c43195efb6 Add demo for amending old commit 2023-08-02 22:32:51 +10:00
Jesse Duffield 17db918cba Add demo for filtering branches 2023-08-02 22:32:51 +10:00
Jesse Duffield befd7fe63e Support mp4 videos for demos
For all videos but the first video in the readme we want to use mp4 because it's faster, better quality,
smaller, and allows you to play/pause (don't quote me on the smaller part).

HOWEVER: github won't let us reference mp4s stored in our repo from the readme, like it does for gifs
(who knows why). This is annoying because it prevents us from easily re-recording things if the UI
changes. So I've got the logic for recording to mp4 but I'm thinking of sticking to gifs for now
2023-08-02 22:21:25 +10:00
Jesse Duffield de28391052 Remove file watcher code (#2865) 2023-08-02 22:20:39 +10:00
Jesse Duffield 92f0aa23cc Remove file watcher code
Now that we refresh upon focus, we can scrap this file watching code.
Stefan says few git UIs use file watching, and I understand why: the
reason this code was problematic in the first place is that watching
files is expensive and if you have too many open file handles that
can cause problems.

Importantly: this code that's being removed was _already_ dead.
2023-08-02 21:50:44 +10:00
Jesse Duffield c8520fbe78 Auto-refresh on window activation (#2854) 2023-08-02 20:42:47 +10:00
Stefan Haller 8ce60662f2 Refresh when the terminal window gets the focus 2023-08-02 11:35:36 +02:00
Stefan Haller c5acbb6c7c Bump gocui 2023-08-02 11:35:36 +02:00
Stefan Haller 4aca854b59 Point tcell at stefanhaller's fork
This is temporary as long as https://github.com/gdamore/tcell/pull/599 is not
merged. Once that PR is merged, we can revert this.
2023-08-02 11:35:36 +02:00
Jesse Duffield f7bf125037 Stop worktrees view from stealing the window (#2863) 2023-08-02 09:24:51 +10:00
Jesse Duffield c92ed07082 Appease linter 2023-08-02 08:32:57 +10:00
Jesse Duffield df0c3b3ea8 Increase timeout for linter
we've had a timeout issue in CI
2023-08-02 08:32:57 +10:00
Jesse Duffield 280b0429c3 Fix focus issue
When opening lazygit with `lazygit log` the worktrees view was appearing in front of the files view.
This is because it had higher precedence than the files view in the ordered view mapping, and
that was because it originally was in the branches window so it was further down the list.

The reason this didn't cause issues on typical startup is that the files context is activated at the
start so it is brought to the front.
2023-08-02 07:37:40 +10:00
Jesse Duffield cb22db675a Update README.md 2023-08-02 00:05:04 +10:00
Jesse Duffield bb21034972 Update README.md 2023-08-01 22:37:53 +10:00
Jesse Duffield ec5bc93576 Show visual explosion effect when nuking worktree (#2861) 2023-08-01 22:31:29 +10:00
Jesse Duffield f43fd7af79 Wait in demo after setting caption
This looks nicer than waiting a second and then showing the caption as the action begins
2023-08-01 22:19:04 +10:00
Jesse Duffield a200fccba9 Add explosion animation when nuking working tree
I've been thinking about this for a while: I think it looks really cool if nuking your working tree
actually results in a nuke animation.

So I've added an opt-out config for it
2023-08-01 22:16:04 +10:00
Jesse Duffield 37dc31b815 Output correct path in demo record script 2023-08-01 22:10:59 +10:00
Jesse Duffield 1712ad08ae Start in fullscreen when passing a git arg
Often we just want to see the desired view in fullscreen so I'm making that the default
2023-08-01 22:10:59 +10:00
README-bot 53f0c1eb4f Updated README.md 2023-08-01 12:03:12 +00:00
Jesse Duffield c53adbbb84 Update README.md 2023-08-01 22:02:56 +10:00
Jesse Duffield e932aaaeaf Internationalise logging of commands (#2852) 2023-08-01 10:06:48 +10:00
Jesse Duffield b44d0c4bde Show loader when rebasing (#2851) 2023-08-01 09:58:11 +10:00
Karl Heitmann 917cfd7586 Adds EditRebaseFromBaseCommit log message to i18n 2023-07-31 19:57:14 -04:00
Karl Heitmann c7e6a73512 Wraps rebase func with WithWaitingStatus to show loader when rebasing 2023-07-31 19:46:59 -04:00
Karl Heitmann b7ba06fa5b Moves log related translations into its own Tr.Log. namespace 2023-07-31 19:42:41 -04:00
Karl Heitmann 51e205ce11 Moves hard coded strings for LogCommand to i18n. 2023-07-31 19:35:55 -04:00
README-bot 6984a4f4d4 Updated README.md 2023-07-31 23:12:55 +00:00
Jesse Duffield 4baecec7bf Create demo output dir if it doesn't already exist (#2857) 2023-08-01 09:12:39 +10:00
Jesse Duffield b262e7f9b8 Create demo output dir if it doesn't already exist 2023-08-01 09:12:23 +10:00
Jesse Duffield b92c294059 Add automated demo recordings (#2853) 2023-07-31 22:43:36 +10:00
Jesse Duffield 07d03df8df Update README.md 2023-07-31 22:36:40 +10:00
Jesse Duffield 9cc1d65280 Add demo test variant
We're piggybacking on our existing integration test framework to record  demos that we can include in our docs
2023-07-31 22:33:04 +10:00
Jesse Duffield 893c1cab75 Update README.md 2023-07-31 22:19:11 +10:00
Jesse Duffield 25d57350df Update README.md 2023-07-31 22:12:30 +10:00
Jesse Duffield eb68591396 Update README.md 2023-07-31 20:31:43 +10:00
Stefan Haller 6de9e230a0 Save IgnoreWhitespaceInDiffView in state.yml (#2830) 2023-07-31 11:48:51 +02:00
Stefan Haller 668d29736c Log error when saving app state fails after showing/hiding command log 2023-07-31 11:41:55 +02:00
Stefan Haller be667682f0 Move IgnoreWhitespaceInDiffView to AppState, and persist it when it changes 2023-07-31 11:41:55 +02:00
Stefan Haller 045bce5dfc Allow force-tagging if tag exists (#2827) 2023-07-31 11:40:40 +02:00
Stefan Haller 3827479643 Show correct keybindings in force-push prompt 2023-07-31 11:38:52 +02:00
Stefan Haller d41a195ee6 Allow force-tagging if tag exists 2023-07-31 11:38:52 +02:00
Stefan Haller 71d2fd37e2 Rebase onto branch from a marked base commit (#2835) 2023-07-31 08:52:58 +02:00
Stefan Haller 66de981e91 Add a "Mark commit as base commit for rebase" command
This allows to do the equivalent of "git rebase --onto <target> <base>", by
first marking the <base> commit with the new command, and then selecting the
target branch and invoking the usual rebase command there.
2023-07-31 08:41:41 +02:00
Stefan Haller 375451785c Cleanup: remove extra OutsideFilterMode guard
We are in the outsideFilterModeBindings section here; all of these handlers are
wrapped in a OutsideFilterMode guard in a loop below. No need to add one
manually here.
2023-07-31 08:40:47 +02:00
Stefan Haller a6af31a4cb Visualize local branch heads in commits panel, 2nd approach (#2775) 2023-07-31 08:40:09 +02:00
Stefan Haller 94daf7bddc Add documentation for working with stacked branches 2023-07-31 08:34:01 +02:00
Stefan Haller 4eb73393bb Don't show branch marker for head commit unless updateRefs config is on 2023-07-31 08:34:01 +02:00
Stefan Haller f5c9764dd2 Don't show branch heads in reflog subcommits
It's tricky to get this right for reflog commits wrt what's the current branch
for each one; so just disable it entirely here, it's probably not something
anybody needs here.
2023-07-31 08:34:01 +02:00
Stefan Haller 6dc25d796b Visualize local branch heads in commits panel
We want to mark all local branch heads with a "*" in the local commits panel, to
make it easier to see how branches are stacked onto each other. In order to not
confuse users with "*" markers that they don't understand, do this only for the
case where users actually use stacked branches; those users are likely not going
to be confused by the display. This means we want to filter out a few branch
heads that shouldn't get the marker: the current branch, any main branch, and
any old branch that has been merged to master already.
2023-07-31 08:34:01 +02:00
Stefan Haller 0c07963a2e Store full sha in branch model
The model will be used for logic, so the full hash is needed there; a shortened
hash of 8 characters might be too short to be unique in very large repos. If
some view wants to display a shortened hash, it should truncate it at
presentation time.
2023-07-31 08:34:01 +02:00
Stefan Haller 092d5dd608 Add CheckedOutBranch to Model struct 2023-07-31 08:34:01 +02:00
Stefan Haller e9bbd816de Make bisect/basic.go test more concrete
- check out a non-main branch before we start
- add authors to expected commits so that we can see whether the commits show an
  asterisk
- explicitly check what the top line displays after bisecting has started

This shows that the detached head shows an asterisk, which we don't want. We'll
fix that in the next commit.
2023-07-31 08:34:01 +02:00
Stefan Haller eb6f089a2a Add author short names to commits in test
This allow us to check not only whether a given commit has the branch head
marker, but also that other commits _don't_ have it, which is important.
2023-07-31 08:34:00 +02:00
Stefan Haller cb240081a8 Improve updateRef test
This test not only tests the correct handling and display of the updateRef
command, but also the visualization of branch heads in the commits panel. Since
we are about to change the behavior here, extend the test so that a master
commit is added (we don't want this to be visualized as a branch head), and then
a stack of two non-main branches. At the end of this branch we only want to
visualize the head commit of the first.
2023-07-31 08:34:00 +02:00
Stefan Haller 9c57444adc Remove the old experimentalShowBranchHeads mechanism and config
We are going to replace it with a better one later in this branch.
2023-07-31 08:34:00 +02:00
Stefan Haller 7b8ebfa27e Fix merge status for update-ref command (#2845) 2023-07-31 08:32:48 +02:00
Stefan Haller 8ab05d6834 Fix merge status of commits when update-ref command is present
Update-ref commands have an empty sha, and strings.HasPrefix returns true when
called with an empty second argument, so whenever an update-ref command is
present in a rebase, all commits from there on down were drawn with a green sha.
2023-07-31 08:28:03 +02:00
Stefan Haller 774df817fd Add tests for setCommitMergedStatuses
The test for update-ref shows demonstrates a problem. See next commit for the
fix.
2023-07-31 08:28:03 +02:00
Stefan Haller 18903a7dde Make setCommitMergedStatuses a non-member function
It doesn't depend on anything in CommitLoader, so it can be free-standing, and
that makes it easier to test (see next commit).
2023-07-31 08:28:03 +02:00
Jesse Duffield 7cfbfb7183 Remove redundant secureexec package (#2847) 2023-07-30 20:04:06 +10:00
Jesse Duffield 975d2bedb6 Remove secureexec package
From the go 1.19 release notes:

Command and LookPath no longer allow results from a PATH search to be found relative to the current directory. This removes a common source of security problems but may also break existing programs that depend on using, say, exec.Command("prog") to run a binary named prog (or, on Windows, prog.exe) in the current directory. See the os/exec package documentation for information about how best to update such programs.
2023-07-30 19:59:51 +10:00
Jesse Duffield 5c78394299 Standardise on using lo for slice functions (#2846) 2023-07-30 18:55:12 +10:00
Jesse Duffield e33fe37a99 Standardise on using lo for slice functions
We've been sometimes using lo and sometimes using my slices package, and we need to pick one
for consistency. Lo is more extensive and better maintained so we're going with that.

My slices package was a superset of go's own slices package so in some places I've just used
the official one (the methods were just wrappers anyway).

I've also moved the remaining methods into the utils package.
2023-07-30 18:51:23 +10:00
README-bot ea54cb6e9c Updated README.md 2023-07-30 08:42:37 +00:00
Jesse Duffield 08f0e28e55 Add worktrees view (#2147) 2023-07-30 18:42:25 +10:00
Jesse Duffield 2d4706e96e fix go mod 2023-07-30 18:36:04 +10:00
Jesse Duffield d30eef69b1 Use double dash for disambuating path in editor templates 2023-07-30 18:35:36 +10:00
Jesse Duffield 91909331b5 Fix flakey worktree tests
In the presentation layer, when showing branches, we'll show worktrees against branches if they're
associated. But there was a race condition: if the worktree model was refreshed after the branches model,
it wouldn't be used in the presentation layer when it came time to render the branches.

A better solution would be to have some way of signalling that a particular context needs to be refreshed
and after all the models are done being refreshed, we then refresh the contexts. This will prevent
double-renders
2023-07-30 18:35:36 +10:00
Jesse Duffield f31e213edc rename files 2023-07-30 18:35:36 +10:00
Jesse Duffield 7b302d8c29 Write unit tests with the help of afero
Afero is a package that lets you mock out a filesystem with an in-memory filesystem.
It allows us to easily create the files required for a given test without worrying about
a cleanup step or different tests tripping on eachother when run in parallel.

Later on I'll standardise on using afero over the vanilla os package
2023-07-30 18:35:36 +10:00
Jesse Duffield a1fae41051 Fix bug where worktree view would take over window upon switching branches
When switching worktrees (which we can now do via the branch view) we re-layout the windows and their views.
We had the worktree view ahead of the file view based on the Flatten() method in context.go, because it used
to be associated with the branches panel.
2023-07-30 18:35:24 +10:00
Jesse Duffield b16bb409fc Use forward-slashes on windows
We want to be using forward slashes everywhere internally, so if we get a path from windows
we should immediately convert it to use forward slashes.

I'm leaving out the recent repos list because that would require a migration
2023-07-30 18:35:24 +10:00
Jesse Duffield a77d24fdc6 Add section in integration readme about testing against old git versions 2023-07-30 18:35:24 +10:00
Jesse Duffield eecf07cd12 Add more i18n for worktrees 2023-07-30 18:35:24 +10:00
Jesse Duffield 06be88aef7 Use fields rather than methods on worktrees
I would prefer to use methods to keep things immutable but I'd rather be consistent with the other
models and update them all at once
2023-07-30 18:35:24 +10:00
Jesse Duffield 4c5b1574f1 Centralise logic for obtaining repo paths
There are quite a few paths you might want to get e.g. the repo's path, the worktree's path,
the repo's git dir path, the worktree's git dir path. I want these all obtained once and
then used when needed rather than having to have IO whenever we need them. This is not so
much about reducing time spent on IO as it is about not having to care about errors every time
we want a path.
2023-07-30 18:35:24 +10:00
Jesse Duffield de57cfd6ff Remove IO logic from presentation code for worktrees
We're doing all the IO in our workers loader method so that we don't need to do any
in our presentation code
2023-07-30 18:35:24 +10:00
Jesse Duffield 2b24c15938 Add test for opening lazygit in the worktree of a bare repo 2023-07-30 18:35:23 +10:00
Jesse Duffield e874f94cf8 Remove dead function 2023-07-30 18:35:23 +10:00
Jesse Duffield 0a410141ae Fix test by making branches appear deterministically
This fixes pkg/integration/tests/worktree/rebase.go which was failing on old git versions due to a difference in
order of branches that don't have recency values
2023-07-30 18:35:23 +10:00
Jesse Duffield e356b29b4a Allow entering a submodule by pressing space 2023-07-30 18:35:23 +10:00
Jesse Duffield bc4ce26cef Allow entering a worktree by pressing enter 2023-07-30 18:35:23 +10:00
Jesse Duffield ae66f720f5 Update repo switch logic
We now always re-use the state of the repo if we're returning to it, and we always reset the windows to their default tabs.

We reset to default tabs because it's easy to implement. If people want to:
* have tab states be retained when switching
* have tab states specific to the current repo retained when switching back

Then we'll need to revisit this
2023-07-30 18:35:23 +10:00
Jesse Duffield 9c15ba0c0b Add test for retained context focus when switching worktrees 2023-07-30 18:35:23 +10:00
Jesse Duffield b3060065d9 Support fastforwarding worktree 2023-07-30 18:35:23 +10:00
Jesse Duffield a313b16704 Add more worktree tests 2023-07-30 18:35:23 +10:00
Jesse Duffield b93b9dae88 Add worktree tests for removing/detaching 2023-07-30 18:35:23 +10:00
Jesse Duffield 277142fc4b Add worktree integration tests 2023-07-30 18:35:23 +10:00
Jesse Duffield 18a508b29c Update cheatsheets 2023-07-30 18:35:23 +10:00
Jesse Duffield cdfad864ae Remove worktree version guards
Our min required git version is 2.20 so there's no need to add guards
for worktrees because they were added in 2.5
2023-07-30 18:35:23 +10:00
Jesse Duffield fb4453c18a Fix unit tests 2023-07-30 18:35:23 +10:00
Jesse Duffield b5ff55e538 Show loader when switching worktrees 2023-07-30 18:35:23 +10:00
Jesse Duffield 81a9133261 Support older versions of git when fetching worktrees
Older versions of git don't support the -z flag in `git worktree list`.
So we're using newlines.

Also, we're not raising an error upon error because that triggers another refresh,
which gets us into an infinite loop
2023-07-30 18:35:23 +10:00
Jesse Duffield ca6f9c4155 Fix tests
Going and fixing up some submodule tests which were broken by bad assumptions with worktree code
2023-07-30 18:35:23 +10:00
Jesse Duffield 7569180cac Fix tests
We now change directories to the repo on startup so we don't need to determine the test path in some special way
2023-07-30 18:35:23 +10:00
Jesse Duffield eeec373728 Safer fetching of linked worktree paths 2023-07-30 18:35:23 +10:00
Jesse Duffield 0604e43813 Move worktrees tab to files window 2023-07-30 18:35:23 +10:00
Jesse Duffield 27ade502ee Change directory to worktree if given as an argument
Previously we used an env var for this and it's not clear to me how that worked but
with this PR current directory = worktree directory
2023-07-30 18:35:23 +10:00
Jesse Duffield 3a7468ecac Support opening worktree in editor 2023-07-30 18:35:23 +10:00
Jesse Duffield 7b05dacb98 Properly render worktrees in files panel 2023-07-30 18:35:22 +10:00
Jesse Duffield b73efb2c22 Better logic for knowing which repo we're in 2023-07-30 18:35:22 +10:00
Jesse Duffield a06a5cadee Only show worktree in status panel if not the main worktree and worktrees are supported 2023-07-30 18:35:22 +10:00
Jesse Duffield 3cd2d6fa5c Hide worktree functionality on old git versions 2023-07-30 18:35:22 +10:00
Jesse Duffield ec839e9e96 Associate branches with worktrees even when mid-rebase 2023-07-30 18:35:22 +10:00
Jesse Duffield 6f2f9f6677 Assume that the base of a worktree can be checked out 2023-07-30 18:35:22 +10:00
Jesse Duffield 2082fdf84a i18n for worktrees 2023-07-30 18:35:22 +10:00
Jesse Duffield 894485190b Don't quit on error 2023-07-30 18:35:22 +10:00
Jesse Duffield 87b2455dbb Allow opening worktree in editor
This does the job but I think we need yet another editor command for opening a directory in a new window.
2023-07-30 18:35:22 +10:00
Jesse Duffield 2e68967e02 Show base ref suggestions when creating worktree 2023-07-30 18:35:22 +10:00
Jesse Duffield 71422a8549 Refresh work trees when discarding file changes
We do this because we may be deleting a worktree folder so we'll need to show that in the worktrees view
2023-07-30 18:35:22 +10:00
Jesse Duffield 9c69a5df69 Checkout worktree when creating from worktree view 2023-07-30 18:35:22 +10:00
Jesse Duffield 9b24995990 Use 'M' for months in branches panel 2023-07-30 18:35:22 +10:00
Jesse Duffield 142f06357f Fix filtering logic in worktrees view 2023-07-30 18:35:22 +10:00
Jesse Duffield 18ea68c23a Support creating worktrees from refs 2023-07-30 18:35:22 +10:00
Jesse Duffield 4b2622d93b Fix wording 2023-07-30 18:35:22 +10:00
Jesse Duffield ae0193698e Log when directory is changed 2023-07-30 18:35:22 +10:00
Jesse Duffield 6b4a638415 Handle deleting branch attached to worktree 2023-07-30 18:35:22 +10:00
Jesse Duffield 4b3c9f5b35 Update wording 2023-07-30 18:35:22 +10:00
Jesse Duffield 6c69549ced Don't touch repo stack when switching worktrees
We shouldn't touch this cos we're doing a lateral move
2023-07-30 18:35:22 +10:00
Jesse Duffield 5868750aba Move status panel presentation logic into presentation package 2023-07-30 18:35:22 +10:00
Jesse Duffield ab3052f642 Land in the same panel when switching to a worktree 2023-07-30 18:35:22 +10:00
Jesse Duffield 53f4ccb809 Move current worktree to top of list 2023-07-30 18:35:22 +10:00
Jesse Duffield 077ae99438 Prompt to switch to worktree when branch is checked out by other worktree 2023-07-30 18:35:22 +10:00
Jesse Duffield fe8adf9eb8 Use git lingo 2023-07-30 18:35:22 +10:00
Jesse Duffield c713d550c0 Improve name handling 2023-07-30 18:35:22 +10:00
Jesse Duffield d19d89ea9d Use sentence case 2023-07-30 18:35:22 +10:00
Jesse Duffield e8ec41fb0f Refactor 2023-07-30 18:35:22 +10:00
Jesse Duffield 7682ec029b Update worktree model 2023-07-30 18:35:22 +10:00
Jesse Duffield 03f726038e Alert when attempting to enter the current worktree 2023-07-30 18:35:21 +10:00
Jesse Duffield 796945aad0 Remove comment 2023-07-30 18:35:21 +10:00
Joel Baranick db02c13bf6 Address PR comments 2023-07-30 18:35:21 +10:00
Joel Baranick 1ce9a87544 Basic support for adding a worktree 2023-07-30 18:35:21 +10:00
Joel Baranick 35e6e6347a Put all worktree i18n strings together
Use tabwriter to align worktree panel contents
2023-07-30 18:35:21 +10:00
Joel Baranick 54708233ac Improve worktree panel 2023-07-30 18:35:21 +10:00
Joel Baranick c679fd1924 Style missing worktree as red and display better error when trying to switch to them
Use a broken link icon for missing worktrees
2023-07-30 18:35:21 +10:00
Joel Baranick 9a79154d05 Hide worktrees in the worktree panel if they point at a non-existing filesystem location.
Remove unneeded check when filtering out branches from non-current worktrees from the branch panel.
Add link icon for linked worktrees
2023-07-30 18:35:21 +10:00
Joel Baranick 60872c91e6 Update status to differentiate the main vs linked worktrees 2023-07-30 18:35:21 +10:00
Joel Baranick afc4aedd4f Support for deleting a worktree 2023-07-30 18:35:21 +10:00
Joel Baranick f8ba899b87 Initial addition of support for worktrees 2023-07-30 18:35:21 +10:00
Stefan Haller 52447e5d46 When bisecting, always mark the current commit as good/bad, not the selected (#2837) 2023-07-29 14:56:14 +02:00
Stefan Haller 6794149ec8 When bisecting, always mark the current commit as good/bad, not the selected
For marking as good or bad, the current commit is pretty much always the one you
want to mark, not the selected. It's different for skipping; sometimes you know
already that a certain commit doesn't compile, for example, so you might
navigate there and mark it as skipped. So in the case that the current commit is
not the selected one, we now offer two separate menu entries for skipping, one
for the current commit and one for the selected.
2023-07-29 14:44:00 +02:00
Stefan Haller a4772ae606 Add bisect menu entry that lets you choose bisect terms (#2838) 2023-07-29 12:02:22 +02:00
Stefan Haller f30e09856c Add bisect menu entry that lets you choose bisect terms
This can be useful if you want to find the commit that fixed a bug (you'd use
"broken/fixed" instead of "good/bad" in this case), or if you want to find the
commit that brought a big performance improvement (use "slow/fast"). It's pretty
mind-bending to have to use "good/bad" in these cases, and swap their meanings
in your head.

Thankfully, lazygit already had support for using custom terms during the bisect
(for the case that a bisect was started on the command-line, I suppose), so all
that's needed is adding a way to specify them in lazygit.
2023-07-29 11:59:58 +02:00
Stefan Haller 9b7f978e3e Fix the x/exp dependency (#2844) 2023-07-29 11:59:04 +02:00
Jesse Duffield f17417219a feat: add os.copyToClipboardCmd to allow for a custom command #1055 (#2784) 2023-07-29 19:35:52 +10:00
Red S d7f84aed8a feat: add os.copyToClipboardCmd to allow for a custom command
Issue #1055

test: CopyPatchToClipboard (temporary commit for review)
2023-07-29 19:09:59 +10:00
Stefan Haller deecbadda9 Fix the x/exp dependency
We started to use it directly in 429225da80.
2023-07-29 10:01:41 +02:00
README-bot 1db1fee03a Updated README.md 2023-07-29 02:39:15 +00:00
Jesse Duffield 2f0116170d Faster refresh (#2841) 2023-07-29 12:39:00 +10:00
Jesse Duffield 429225da80 Support random order of command execution in unit tests
Now that we run code concurrently in our loaders, we need to handle that in our tests.
We could enforce a deterministic ordering by mocking waitgroup or something like that,
but I think it's fine to let our tests handle some randomness given that prod itself
will have that randomness.

I've removed the patch test file because it was clunky, not providing much value, and
it would have been hard to refactor to the new pattern
2023-07-29 12:36:17 +10:00
Jesse Duffield 39b77c0fca Have staging refresh wait for files to refresh first 2023-07-29 10:20:15 +10:00
Jesse Duffield 63e5790410 Speed up refresh using concurrency and wait groups
Previously our synchronous refreshes took far longer because nothing
was happening concurrently. We now run refresh functions concurrently
and use a wait group to ensure they're all done before returning
2023-07-29 10:04:11 +10:00
Jesse Duffield 272e021c08 Refactor reflog commit loader
No functional changes
2023-07-29 10:04:11 +10:00
Jesse Duffield 862ebd25cb Speed up remote loader
We're:
* using concurrency with wait groups
* avoiding regex
* processing lines of input as they come rather than storing everything in one string
* avoiding an inner loop by creating a mapping of remote names to branches
2023-07-29 10:04:11 +10:00
Jesse Duffield 5d8a85f7e7 Use wait groups to speed up commit loading
The speedup is most noticeable on first load, when we haven't yet fetched out main branches.
I saw a speedup from 105ms to 60ms. On subsequent loads the gain is more modest;
54ms to 40ms
2023-07-29 10:04:11 +10:00
Jesse Duffield 5f30ccfbc3 Log duration of post-refresh-update call
Notably, the reflog view is taking ages here because it's got a
few thousand lines to write to the view.

In future we should only populate the view's viewport.
2023-07-29 09:39:22 +10:00
Jesse Duffield 71cab4fadc Log duration of commands
This will help us diagnose performance issues
2023-07-29 09:39:22 +10:00
Jesse Duffield 09ce430240 Log duration of refresh 2023-07-29 09:39:10 +10:00
Stefan Haller 6be9109aaa Show error when trying to open patch menu with an empty patch (#2829) 2023-07-26 14:34:58 +02:00
Stefan Haller 67a7293e79 Show error when trying to open patch menu with an empty patch 2023-07-26 14:27:18 +02:00
Jesse Duffield decb055df9 Fix issue where using null to un-map a keybinding was ignored (#2832) 2023-07-26 20:26:51 +10:00
hatredholder 75674d819c bring back yaml library fork 2023-07-26 12:50:39 +03:00
README-bot 35c430c8c9 Updated README.md 2023-07-24 11:15:05 +00:00
Jesse Duffield 94426de533 Fix infinite wait on push/pull on windows (#2821) 2023-07-24 21:14:51 +10:00
Jesse Duffield c4e27bf96c Revert "Fix flakey pull_merge_conflict test"
This reverts commit 90613056ce, or the part that removed
a goroutine at least.

Reverting because this has caused an infinite wait for push/pull on windows.
We'll need to find out why that happens separately
2023-07-24 21:12:03 +10:00
Jesse Duffield 57bb1aa698 Support typing special characters like '[' on non-english keyboards (#2818) 2023-07-24 10:47:15 +10:00
Jesse Duffield 02270e9ccd README.md: Add Gentoo installation documentation (#2811) 2023-07-24 09:48:51 +10:00
Fabio Coatti 1199c18b53 README.md: Add Gentoo installation documentation 2023-07-23 16:43:26 +02:00
Arnaud MASSERANN 4f807eeb19 Could not type special characters on non-english keyboards
On german/french/spanish keyboards, typing [ requires modifier
keys like AltGr, so the `mod==0` condition is wrong.

Fixes #2573

ch != 0 is useless because IsPrint is implemented this way:
	if uint32(r) <= MaxLatin1 {
		return properties[uint8(r)]&128 != 0
	}
with properties[0] set to 1 (so, bit 7 not set)
-> 0 is not printable.
2023-07-23 14:33:50 +02:00
Stefan Haller 7c44b76477 Prompt for commit message when moving a custom patch to a new commit (#2800) 2023-07-23 14:00:41 +02:00
Stefan Haller c21633b1be Prompt for commit message when moving a custom patch to a new commit 2023-07-23 13:55:48 +02:00
Jesse Duffield 1b05ba252c Fix crash caused by simultaneous read/write of scanner buffer (#2813) 2023-07-23 13:55:51 +10:00
Jesse Duffield 28474b08ee Fix crash caused by simultaneous read/write of scanner buffer 2023-07-23 13:17:37 +10:00
Jesse Duffield 6f13b42279 Better word wrap (#2812) 2023-07-23 11:46:52 +10:00
Jesse Duffield 8637587b82 Better word wrap
Word wrapping has been pretty bad so far so let's fix that.
2023-07-23 11:43:10 +10:00
Jesse Duffield f581dc4a56 Update README.md 2023-07-22 15:31:03 +10:00
Jesse Duffield 693e9fc152 Update README.md
Removing unneeded go docs tag
2023-07-22 15:14:18 +10:00
Jesse Duffield c595833883 Better tag creation UX (#2809) 2023-07-22 14:44:18 +10:00
Jesse Duffield 7807b40322 Better tag creation UX
Previously we used a single-line prompt for a tag annotation. Now we're using the commit message
prompt.

I've had to update other uses of that prompt to allow the summary and description labels to
be passed in
2023-07-22 14:36:35 +10:00
Jesse Duffield b284970bac Use fuzzy search when filtering a view (#2808) 2023-07-22 13:17:46 +10:00
Jesse Duffield b46623ebef Use fuzzy search when filtering a view
This adds fuzzy filtering instead of exact match filtering, which is more forgiving of typos
and allows more efficiency.
2023-07-22 13:14:29 +10:00
Jesse Duffield 084c0a19bc Include more commit authors in author suggestions (#2807) 2023-07-22 11:00:38 +10:00
Jesse Duffield 3cee37388c Keep track of authors across local commits and branch commits for suggestions
Previously, we would only show the authors based on local commits, but sometimes you want to set a commit author
to that of a commit on another branch. Now, so long as you've viewed the branch's commits, the author will appear
as a suggestion.
2023-07-22 10:47:04 +10:00
Andrew Savinykh a7969aef2c Fix rendering to main view on windows 2023-07-22 09:14:05 +10:00
Jesse Duffield 39c900c7e7 Fix goreleaser 2023-07-21 09:03:47 +10:00
Jesse Duffield 6e247c1583 Only apply right-alignment on first column of keybindings menu (#2801) 2023-07-20 21:27:01 +10:00
Jesse Duffield 87bf1dbc7f Only apply right-alignment on first column of keybindings menu
Previously we applied a right-align on the first column of _all_ menus, even though we really
only intended for it to be on the first column of the keybindings menu (that you get from pressing
'?')
2023-07-20 21:23:46 +10:00
Jesse Duffield 1f920ae6ba Fix crash on empty menu (#2799) 2023-07-20 21:17:14 +10:00
Jesse Duffield 932e01b41a Add test for crashing on empty menu 2023-07-20 21:08:56 +10:00
Jesse Duffield 373f24c80f Fix crash on empty menu
When a menu is empty (e.g. due to filtering) we shouldn't crash on focus or selection
2023-07-20 21:05:52 +10:00
Jesse Duffield a548b289ef Add missing label to label checker (#2798) 2023-07-20 17:33:10 +10:00
Jesse Duffield b168fc8cdd Add missing label to label checker 2023-07-20 17:31:40 +10:00
Jesse Duffield 94845dcf98 Update release notes config and add CI check (#2797) 2023-07-20 17:19:49 +10:00
Jesse Duffield 1af6dff64e Update release notes config and add CI check 2023-07-20 17:15:10 +10:00
Jesse Duffield 72de4f436e Add release config for generating release notes (#2793) 2023-07-19 23:38:04 +10:00
Jesse Duffield effda8291b Add release config for generating release notes
After going and adding labels for all of these I found out that 'improvement' should be 'enhancement' and 'bugfix' should be 'bug'
but I don't know how to bulk update them (and I can't rename because the desired labels already exist).

I'll work that out later, this is good enough for now
2023-07-19 23:37:34 +10:00
Jesse Duffield 7985e31020 Properly fix accordion issue (#2792) 2023-07-19 22:24:42 +10:00
Jesse Duffield 866e0a618b Add integration test for accordion mode 2023-07-19 22:17:29 +10:00
Jesse Duffield a5ee61c117 Properly fix accordion issue
The true issue was that we were focusing the line in the view before it gets resized in the layout function.
This meant if the view was squashed in accordion mode, the view wouldn't know how to set the cursor/origin to
focus the line.

Now we've got a queue of 'after layout' functions i.e. functions to call at the end of the layout function,
right before views are drawn.

The only caveat is that we can't have an infinite buffer so we're arbitrarily capping it at 1000 and dropping
functions if we exceed that limit. But that really should never happen.
2023-07-19 21:16:27 +10:00
Jesse Duffield 08aad924d7 Fix accordion issue (#2791) 2023-07-19 21:07:25 +10:00
Jesse Duffield be02786dad Fix accordion issue
This fixes the issue in accordion mode where the current line wasn't in the viewport upon focus.

It doesn't perfectly fix it: the current line always appears at the top of the view. But it's good enough
to cut a new release. The proper fix is to only focus the line after the view has had its height adjusted.
2023-07-19 20:39:10 +10:00
README-bot 949022db8c Updated README.md 2023-07-19 08:19:33 +00:00
Stefan Haller b1a090d4c0 Fix crash when a background fetch prompts for credentials (#2789) 2023-07-19 10:19:18 +02:00
Stefan Haller 39f3f150ed Fix crash when a background fetch prompts for credentials
This happens consistently for my when I close my MacBook's lid. It seems that
MacOS locks the user's keychain in this case, and since I have my keychain
provide the pass phrases for my ssh keys, fetching fails because it tries to
prompt me for a pass phrase.

This all worked correctly already, we have the FailOnCredentialRequest()
mechanism specifically for this situation, so all is great. The only problem was
that it was trying to pause the ongoing task while prompting the user for input;
but the task is nil for a background fetch (and should be).
2023-07-18 18:53:35 +02:00
530 changed files with 72814 additions and 9266 deletions
+26
View File
@@ -0,0 +1,26 @@
changelog:
exclude:
labels:
- ignore-for-release
categories:
- title: Features ✨
labels:
- feature
- title: Enhancements 🔥
labels:
- enhancement
- title: Fixes 🔧
labels:
- bug
- title: Maintenance ⚙️
labels:
- maintenance
- title: Docs 📖
labels:
- docs
- title: I18n 🌎
labels:
- i18n
- title: Other Changes
labels:
- "*"
+5 -1
View File
@@ -16,9 +16,13 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
go-version: 1.20.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v1
with:
distribution: goreleaser
version: v1.17.2
args: release --clean
env:
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
homebrew:
+15 -6
View File
@@ -1,7 +1,7 @@
name: Continuous Integration
env:
GO_VERSION: 1.18
GO_VERSION: 1.20
on:
push:
@@ -32,7 +32,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
go-version: 1.20.x
- name: Cache build
uses: actions/cache@v3
with:
@@ -91,7 +91,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
go-version: 1.20.x
- name: Cache build
uses: actions/cache@v1
with:
@@ -117,7 +117,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
go-version: 1.20.x
- name: Cache build
uses: actions/cache@v1
with:
@@ -153,7 +153,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
go-version: 1.20.x
- name: Cache build
uses: actions/cache@v1
with:
@@ -187,7 +187,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
go-version: 1.20.x
- name: Cache build
uses: actions/cache@v1
with:
@@ -204,3 +204,12 @@ jobs:
- name: errors
run: golangci-lint run
if: ${{ failure() }}
check-required-label:
runs-on: ubuntu-latest
if: github.ref != 'refs/heads/master'
steps:
- uses: mheap/github-action-required-labels@v5
with:
mode: exactly
count: 1
labels: "ignore-for-release, feature, enhancement, bug, maintenance, docs, i18n"
+2 -1
View File
@@ -40,4 +40,5 @@ test/results/**
oryxBuildBinary
__debug_bin
.worktrees
.worktrees
demo/output/*
+2 -1
View File
@@ -26,4 +26,5 @@ linters-settings:
max-func-lines: 0
run:
go: 1.18
go: '1.20'
timeout: 10m
+1 -1
View File
@@ -12,7 +12,7 @@ builds:
- amd64
- arm
- arm64
- 386
- '386'
# Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`.
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
+1 -1
View File
@@ -2,7 +2,7 @@
# docker build -t lazygit .
# docker run -it lazygit:latest /bin/sh
FROM golang:1.18 as build
FROM golang:1.20 as build
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY go.mod go.sum ./
RUN go mod download
+108 -36
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
This directory contains stuff for recording lazygit demos.
+112
View File
@@ -0,0 +1,112 @@
# Specify a command to be executed
# like `/bin/bash -l`, `ls`, or any other commands
# the default is bash for Linux
# or powershell.exe for Windows
command: echo "YOU NEED TO SPECIFY YOUR OWN COMMAND WITH THE -d ARG"
# Specify the current working directory path
# the default is the current working directory path
cwd: null
# Export additional ENV variables
env:
recording: true
# Explicitly set the number of columns
# or use `auto` to take the current
# number of columns of your shell
cols: 120 # 100
# Explicitly set the number of rows
# or use `auto` to take the current
# number of rows of your shell
rows: 35 # 30
# Amount of times to repeat GIF
# If value is -1, play once
# If value is 0, loop indefinitely
# If value is a positive number, loop n times
repeat: 0
# Quality
# 1 - 100
# Higher quality seems to make no difference, but running it through
# gifsicle ends up with a much better compressed version.
quality: 100
# Delay between frames in ms
# If the value is `auto` use the actual recording delays
frameDelay: auto
# Maximum delay between frames in ms
# Ignored if the `frameDelay` isn't set to `auto`
# Set to `auto` to prevent limiting the max idle time
maxIdleTime: 2000
# The surrounding frame box
# The `type` can be null, window, floating, or solid`
# To hide the title use the value null
# Don't forget to add a backgroundColor style with a null as type
frameBox:
type: floating
title: Lazygit
style:
border: 0px black solid
backgroundColor: "#1d1d1d"
margin: -5px
# Add a watermark image to the rendered gif
# You need to specify an absolute path for
# the image on your machine or a URL, and you can also
# add your own CSS styles
watermark:
imagePath: null
style:
position: absolute
right: 15px
bottom: 15px
width: 100px
opacity: 0.9
# Cursor style can be one of
# `block`, `underline`, or `bar`
cursorStyle: block
# Font family
# You can use any font that is installed on your machine
# in CSS-like syntax
# Download from:
# https://github.com/ryanoasis/nerd-fonts/releases/download/v3.0.2/DejaVuSansMono.zip
# Not using the mono font because it makes icons too small.
fontFamily: "DejaVuSansM Nerd Font"
# The size of the font
fontSize: 8
# The height of lines
lineHeight: 1
# The spacing between letters
letterSpacing: 0
# Theme
theme:
background: "transparent"
foreground: "#dddad6"
cursor: "#c7c7c7"
black: "#7a7a7a"
red: "#fc4384"
green: "#b3e33b"
yellow: "#ffa727"
blue: "#102895"
magenta: "#c930c7"
cyan: "#00c5c7"
white: "#c7c7c7"
brightBlack: "#676767"
brightRed: "#ff7fac"
brightGreen: "#c8ed71"
brightYellow: "#ebdf86"
brightBlue: "#6871ff"
brightMagenta: "#ff76ff"
brightCyan: "#5ffdff"
brightWhite: "#fffefe"
+81
View File
@@ -0,0 +1,81 @@
#!/bin/sh
set -e
TYPE=$1
TEST=$2
usage() {
echo "Usage: $0 [gif|mp4] <test path>"
echo "e.g. using full path: $0 gif pkg/integration/tests/demo/nuke_working_tree.go"
exit 1
}
if [ "$#" -ne 2 ]
then
usage
fi
if [ "$TYPE" != "gif" ] && [ "$TYPE" != "mp4" ]
then
usage
exit 1
fi
if [ -z "$TEST" ]
then
usage
fi
WORKTREE_PATH=$(git worktree list | grep assets | awk '{print $1}')
if [ -z "$WORKTREE_PATH" ]
then
echo "Could not find assets worktree. You'll need to create a worktree for the assets branch using the following command:"
echo "git worktree add .worktrees/assets assets"
echo "The assets branch has no shared history with the main branch: it exists to store assets which are too large to store in the main branch."
exit 1
fi
OUTPUT_DIR="$WORKTREE_PATH/demo"
if ! command -v terminalizer &> /dev/null
then
echo "terminalizer could not be found"
echo "Install it with: npm install -g terminalizer"
exit 1
fi
if ! command -v "gifsicle" &> /dev/null
then
echo "gifsicle could not be found"
echo "Install it with: npm install -g gifsicle"
exit 1
fi
# Get last part of the test path and set that as the output name
# example test path: pkg/integration/tests/01_basic_test.go
# For that we want: NAME=01_basic_test
NAME=$(echo "$TEST" | sed -e 's/.*\///' | sed -e 's/\..*//')
# Add the demo to the tests list (if missing) so that it can be run
go generate pkg/integration/tests/tests.go
mkdir -p "$OUTPUT_DIR"
# First we record the demo into a yaml representation
terminalizer -c demo/config.yml record --skip-sharing -d "go run cmd/integration_test/main.go cli --slow $TEST" "$OUTPUT_DIR/$NAME"
# Then we render it into a gif
terminalizer render "$OUTPUT_DIR/$NAME" -o "$OUTPUT_DIR/$NAME.gif"
# Then we convert it to either an mp4 or gif based on the command line argument
if [ "$TYPE" = "mp4" ]
then
COMPRESSED_PATH="$OUTPUT_DIR/$NAME.mp4"
ffmpeg -y -i "$OUTPUT_DIR/$NAME.gif" -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "$COMPRESSED_PATH"
else
COMPRESSED_PATH="$OUTPUT_DIR/$NAME-compressed.gif"
gifsicle --colors 256 --use-col=web -O3 < "$OUTPUT_DIR/$NAME.gif" > "$COMPRESSED_PATH"
fi
echo "Demo recorded to $COMPRESSED_PATH"
+17 -1
View File
@@ -62,7 +62,6 @@ gui:
showListFooter: true # for seeing the '5 of 20' message in list panels
showRandomTip: true
showBranchCommitHash: false # show commit hashes alongside branch names
experimentalShowBranchHeads: false # visualize branch heads with (*) in commits list
showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
showCommandLog: true
showIcons: false # deprecated: use nerdFontsVersion instead
@@ -71,6 +70,7 @@ gui:
splitDiff: 'auto' # one of 'auto' | 'always'
skipRewordInEditorWarning: false # for skipping the confirmation before launching the reword editor
border: 'single' # one of 'single' | 'double' | 'rounded' | 'hidden'
animateExplosion: true # shows an explosion animation when nuking the working tree
git:
paging:
colorArg: always
@@ -106,6 +106,7 @@ git:
parseEmoji: false
diffContextSize: 3 # how many lines of context are shown around a change in diffs
os:
copyToClipboardCmd: '' # See 'Custom Command for Copying to Clipboard' section
editPreset: '' # see 'Configuring File Editing' section
edit: ''
editAtLine: ''
@@ -278,6 +279,20 @@ os:
open: 'open {{filename}}'
```
### Custom Command for Copying to Clipboard
```yaml
os:
copyToClipboardCmd: ''
```
Specify an external command to invoke when copying to clipboard is requested. `{{text}` will be replaced by text to be copied. Default is to copy to system clipboard.
If you are working on a terminal that supports OSC52, the following command will let you take advantage of it:
```
os:
copyToClipboardCmd: printf "\033]52;c;$(printf {{text}} | base64)\a" > /dev/tty
```
### Configuring File Editing
There are two commands for opening files, `o` for "open" and `e` for "edit". `o`
@@ -308,6 +323,7 @@ os:
editAtLine: 'myeditor --line={{line}} {{filename}}'
editAtLineAndWait: 'myeditor --block --line={{line}} {{filename}}'
editInTerminal: true
openDirInEditor: 'myeditor {{dir}}'
```
The `editInTerminal` option is used to decide whether lazygit needs to suspend
+2
View File
@@ -74,6 +74,7 @@ The permitted contexts are:
| -------------- | -------------------------------------------------------------------------------------------------------- |
| status | The 'Status' tab |
| files | The 'Files' tab |
| worktrees | The 'Worktrees' tab |
| localBranches | The 'Local Branches' tab |
| remotes | The 'Remotes' tab |
| remoteBranches | The context you get when pressing enter on a remote in the remotes tab |
@@ -300,6 +301,7 @@ SelectedRemote
SelectedTag
SelectedStashEntry
SelectedCommitFile
SelectedWorktree
CheckedOutBranch
```
+1
View File
@@ -6,4 +6,5 @@
* [Keybindings](./keybindings)
* [Undo/Redo](./Undoing.md)
* [Searching/Filtering](./Searching.md)
* [Stacked Branches](./Stacked_Branches.md)
* [Dev docs](./dev)
+18
View File
@@ -0,0 +1,18 @@
# Working with stacked branches
When working on a large branch it can often be useful to break it down into
smaller pieces, and it can help to create separate branches for each independent
chunk of changes. For example, you could have one branch for preparatory
refactorings, one for backend changes, and one for frontend changes. Those
branches would then all be stacked onto each other.
Git has support for rebasing such a stack as a whole; you can enable it by
setting the git config `rebase.updateRfs` to true. If you then rebase the
topmost branch of the stack, the other ones in the stack will follow. This
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
cyan asterisk (or a cyan branch symbol if you are using [nerd
fonts](Config.md#display-nerd-fonts-icons)).
+82
View File
@@ -0,0 +1,82 @@
# Demo Recordings
We want our demo recordings to be consistent and easy to update if we make changes to Lazygit's UI. Luckily for us, we have an existing recording system for the sake of our integration tests, so we can piggyback on that.
You'll want to familiarise yourself with how integration tests are written: see [here](../../pkg/integration/README.md).
## Prerequisites
Ideally we'd run this whole thing through docker but we haven't got that working. So you will need:
```
# for recording
npm i -g terminalizer
# for gif compression
npm i -g gifsicle
# for mp4 conversion
brew install ffmpeg
# font with icons
wget https://github.com/ryanoasis/nerd-fonts/releases/download/v3.0.2/DejaVuSansMono.tar.xz && \
tar -xf DejaVuSansMono.tar.xz -C /usr/local/share/fonts && \
rm DejaVuSansMono.tar.xz
```
## Creating a demo
Demos are found in `pkg/integration/tests/demo/`. They are like regular integration tests but have `IsDemo: true` which has a few effects:
* The bottom row of the UI is quieter so that we can render captions
* Fetch/Push/Pull have artificial latency to mimic a network request
* The loader at the bottom-right does not appear
In demos, we don't need to be as strict in our assertions as we are in tests. But it's still good to have some basic assertions so that if we automate the process of updating demos we'll know if one of them has broken.
You can use the same flow as we use with integration tests when you're writing a demo:
* Setup the repo
* Run the demo in sandbox mode to get a feel of what needs to happen
* Come back and write the code to make it happen
### Adding captions
It's good to add captions explaining what task if being performed. Use the existing demos as a guide.
### Setting up the assets worktree
We store assets (which includes demo recordings) in the `assets` branch, which is a branch that shares no history with the main branch and exists purely for storing assets. Storing them separately means we don't clog up the code branches with large binaries.
The scripts and demo definitions live in the code branches but the output lives in the assets branch so to be able to create a video from a demo you'll need to create a linked worktree for the assets branch which you can do with:
```sh
git worktree add .worktrees/assets assets
```
Outputs will be stored in `.worktrees/assets/demos/`. We'll store three separate things:
* the yaml of the recording
* the original gif
* either the compressed gif or the mp4 depending on the output you chose (see below)
### Recording the demo
Once you're happy with your demo you can record it using:
```sh
scripts/record_demo.sh [gif|mp4] <path>
# e.g.
scripts/record_demo.sh gif pkg/integration/tests/demo/interactive_rebase.go
```
~~The gif format is for use in the first video of the readme (it has a larger size but has auto-play and looping)~~
~~The mp4 format is for everything else (no looping, requires clicking, but smaller size).~~
Turns out that you can't store mp4s in a repo and link them from a README so we're gonna just use gifs across the board for now.
### Including demos in README/docs
If you've followed the above steps you'll end up with your output in your assets worktree.
Within that worktree, stage all three output files and raise a PR against the assets branch.
Then back in the code branch, in the doc, you can embed the recording like so:
```md
![Nuke working tree](../assets/demo/interactive_rebase-compressed.gif)
```
This means we can update assets without needing to update the docs that embed them.
+1
View File
@@ -2,3 +2,4 @@
* [Busy/Idle tracking](./Busy.md).
* [Integration Tests](../../pkg/integration/README.md)
* [Demo Recordings](./Demo_Recordings.md)
+20
View File
@@ -84,11 +84,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;c-j&gt;</kbd>: Move commit down one
<kbd>&lt;c-k&gt;</kbd>: Move commit up one
<kbd>v</kbd>: Paste commits (cherry-pick)
<kbd>B</kbd>: Mark commit as base commit for rebase
<kbd>A</kbd>: Amend commit with staged changes
<kbd>a</kbd>: Set/Reset commit author
<kbd>t</kbd>: Revert commit
<kbd>T</kbd>: Tag commit
<kbd>&lt;c-l&gt;</kbd>: Open log menu
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser
@@ -154,6 +156,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: View reset options
<kbd>R</kbd>: Rename branch
<kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -231,6 +234,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser
@@ -254,6 +258,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Delete branch
<kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -276,6 +281,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Drop
<kbd>n</kbd>: New branch
<kbd>r</kbd>: Rename stash
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -294,6 +300,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser
@@ -311,6 +318,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Copy submodule name to clipboard
<kbd>&lt;enter&gt;</kbd>: Enter submodule
<kbd>&lt;space&gt;</kbd>: Enter submodule
<kbd>d</kbd>: Remove submodule
<kbd>u</kbd>: Update submodule
<kbd>n</kbd>: Add new submodule
@@ -328,6 +336,18 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: Push tag
<kbd>n</kbd>: Create tag
<kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
+20
View File
@@ -52,6 +52,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Drop
<kbd>n</kbd>: 新しいブランチを作成
<kbd>r</kbd>: Stashを変更
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -60,6 +61,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: コミットのSHAをクリップボードにコピー
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: コミットをチェックアウト
<kbd>y</kbd>: コミットの情報をコピー
<kbd>o</kbd>: ブラウザでコミットを開く
@@ -72,6 +74,17 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>/</kbd>: 検索を開始
</pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## コミット
<pre>
@@ -90,11 +103,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;c-j&gt;</kbd>: コミットを1つ下に移動
<kbd>&lt;c-k&gt;</kbd>: コミットを1つ上に移動
<kbd>v</kbd>: コミットを貼り付け (cherry-pick)
<kbd>B</kbd>: Mark commit as base commit for rebase
<kbd>A</kbd>: ステージされた変更でamendコミット
<kbd>a</kbd>: Set/Reset commit author
<kbd>t</kbd>: コミットをrevert
<kbd>T</kbd>: タグを作成
<kbd>&lt;c-l&gt;</kbd>: ログメニューを開く
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: コミットをチェックアウト
<kbd>y</kbd>: コミットの情報をコピー
<kbd>o</kbd>: ブラウザでコミットを開く
@@ -133,6 +148,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: サブモジュール名をクリップボードにコピー
<kbd>&lt;enter&gt;</kbd>: サブモジュールを開く
<kbd>&lt;space&gt;</kbd>: サブモジュールを開く
<kbd>d</kbd>: サブモジュールを削除
<kbd>u</kbd>: サブモジュールを更新
<kbd>n</kbd>: サブモジュールを新規追加
@@ -160,6 +176,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: タグをpush
<kbd>n</kbd>: タグを作成
<kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: コミットを閲覧
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -211,6 +228,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: View reset options
<kbd>R</kbd>: ブランチ名を変更
<kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: コミットを閲覧
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -305,6 +323,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: ブランチを削除
<kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: コミットを閲覧
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -313,6 +332,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: コミットのSHAをクリップボードにコピー
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: コミットをチェックアウト
<kbd>y</kbd>: コミットの情報をコピー
<kbd>o</kbd>: ブラウザでコミットを開く
+20
View File
@@ -48,6 +48,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: 커밋 SHA를 클립보드에 복사
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 커밋을 체크아웃
<kbd>y</kbd>: 커밋 attribute 복사
<kbd>o</kbd>: 브라우저에서 커밋 열기
@@ -68,6 +69,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Drop
<kbd>n</kbd>: 새 브랜치 생성
<kbd>r</kbd>: Rename stash
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -76,6 +78,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: 커밋 SHA를 클립보드에 복사
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 커밋을 체크아웃
<kbd>y</kbd>: 커밋 attribute 복사
<kbd>o</kbd>: 브라우저에서 커밋 열기
@@ -88,6 +91,17 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>/</kbd>: 검색 시작
</pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## 메뉴
<pre>
@@ -177,6 +191,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: View reset options
<kbd>R</kbd>: 브랜치 이름 변경
<kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 커밋 보기
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -196,6 +211,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: 서브모듈 이름을 클립보드에 복사
<kbd>&lt;enter&gt;</kbd>: 서브모듈 열기
<kbd>&lt;space&gt;</kbd>: 서브모듈 열기
<kbd>d</kbd>: 서브모듈 삭제
<kbd>u</kbd>: 서브모듈 업데이트
<kbd>n</kbd>: 새로운 서브모듈 추가
@@ -226,6 +242,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: 브랜치 삭제
<kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 커밋 보기
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -248,11 +265,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;c-j&gt;</kbd>: 커밋을 1개 아래로 이동
<kbd>&lt;c-k&gt;</kbd>: 커밋을 1개 위로 이동
<kbd>v</kbd>: 커밋을 붙여넣기 (cherry-pick)
<kbd>B</kbd>: Mark commit as base commit for rebase
<kbd>A</kbd>: Amend commit with staged changes
<kbd>a</kbd>: Set/Reset commit author
<kbd>t</kbd>: 커밋 되돌리기
<kbd>T</kbd>: Tag commit
<kbd>&lt;c-l&gt;</kbd>: 로그 메뉴 열기
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 커밋을 체크아웃
<kbd>y</kbd>: 커밋 attribute 복사
<kbd>o</kbd>: 브라우저에서 커밋 열기
@@ -294,6 +313,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: 태그를 push
<kbd>n</kbd>: 태그를 생성
<kbd>g</kbd>: View reset options
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 커밋 보기
<kbd>/</kbd>: Filter the current view by text
</pre>
+20
View File
@@ -98,6 +98,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: Bekijk reset opties
<kbd>R</kbd>: Hernoem branch
<kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Bekijk commits
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -142,11 +143,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;c-j&gt;</kbd>: Verplaats commit 1 naar beneden
<kbd>&lt;c-k&gt;</kbd>: Verplaats commit 1 naar boven
<kbd>v</kbd>: Plak commits (cherry-pick)
<kbd>B</kbd>: Mark commit as base commit for rebase
<kbd>A</kbd>: Wijzig commit met staged veranderingen
<kbd>a</kbd>: Set/Reset commit author
<kbd>t</kbd>: Commit ongedaan maken
<kbd>T</kbd>: Tag commit
<kbd>&lt;c-l&gt;</kbd>: Open log menu
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser
@@ -209,6 +212,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Kopieer commit SHA naar klembord
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser
@@ -232,6 +236,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Verwijder branch
<kbd>u</kbd>: Stel in als upstream van uitgecheckte branch
<kbd>g</kbd>: Bekijk reset opties
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Bekijk commits
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -276,6 +281,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Laten vallen
<kbd>n</kbd>: Nieuwe branch
<kbd>r</kbd>: Rename stash
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Bekijk gecommite bestanden
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -294,6 +300,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Kopieer commit SHA naar klembord
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser
@@ -311,6 +318,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Kopieer submodule naam naar klembord
<kbd>&lt;enter&gt;</kbd>: Enter submodule
<kbd>&lt;space&gt;</kbd>: Enter submodule
<kbd>d</kbd>: Remove submodule
<kbd>u</kbd>: Update submodule
<kbd>n</kbd>: Voeg nieuwe submodule toe
@@ -328,6 +336,18 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: Push tag
<kbd>n</kbd>: Creëer tag
<kbd>g</kbd>: Bekijk reset opties
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Bekijk commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
+20
View File
@@ -69,11 +69,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;c-j&gt;</kbd>: Przenieś commit 1 w dół
<kbd>&lt;c-k&gt;</kbd>: Przenieś commit 1 w górę
<kbd>v</kbd>: Wklej commity (przebieranie)
<kbd>B</kbd>: Mark commit as base commit for rebase
<kbd>A</kbd>: Popraw commit zmianami z poczekalni
<kbd>a</kbd>: Set/Reset commit author
<kbd>t</kbd>: Odwróć commit
<kbd>T</kbd>: Tag commit
<kbd>&lt;c-l&gt;</kbd>: Open log menu
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser
@@ -112,6 +114,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: Wyświetl opcje resetu
<kbd>R</kbd>: Rename branch
<kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -208,6 +211,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser
@@ -231,6 +235,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Usuń gałąź
<kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>g</kbd>: Wyświetl opcje resetu
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -269,6 +274,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: Porzuć
<kbd>n</kbd>: Nowa gałąź
<kbd>r</kbd>: Rename stash
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -287,6 +293,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Copy commit SHA to clipboard
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Checkout commit
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: Open commit in browser
@@ -304,6 +311,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Copy submodule name to clipboard
<kbd>&lt;enter&gt;</kbd>: Enter submodule
<kbd>&lt;space&gt;</kbd>: Enter submodule
<kbd>d</kbd>: Remove submodule
<kbd>u</kbd>: Update submodule
<kbd>n</kbd>: Add new submodule
@@ -321,10 +329,22 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: Push tag
<kbd>n</kbd>: Create tag
<kbd>g</kbd>: Wyświetl opcje resetu
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## Zwykłe
<pre>
+20
View File
@@ -44,6 +44,17 @@ _Связки клавиш_
<kbd>[</kbd>: Предыдущая вкладка
</pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## Главная панель (Индексирование)
<pre>
@@ -109,6 +120,7 @@ _Связки клавиш_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Скопировать SHA коммита в буфер обмена
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Переключить коммит
<kbd>y</kbd>: Скопировать атрибут коммита
<kbd>o</kbd>: Открыть коммит в браузере
@@ -139,11 +151,13 @@ _Связки клавиш_
<kbd>&lt;c-j&gt;</kbd>: Переместить коммит вниз на один
<kbd>&lt;c-k&gt;</kbd>: Переместить коммит вверх на один
<kbd>v</kbd>: Вставить отобранные коммиты (cherry-pick)
<kbd>B</kbd>: Mark commit as base commit for rebase
<kbd>A</kbd>: Править последний коммит с проиндексированными изменениями
<kbd>a</kbd>: Установить/убрать автора коммита
<kbd>t</kbd>: Отменить коммит
<kbd>T</kbd>: Пометить коммит тегом
<kbd>&lt;c-l&gt;</kbd>: Открыть меню журнала
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Переключить коммит
<kbd>y</kbd>: Скопировать атрибут коммита
<kbd>o</kbd>: Открыть коммит в браузере
@@ -175,6 +189,7 @@ _Связки клавиш_
<kbd>g</kbd>: Просмотреть параметры сброса
<kbd>R</kbd>: Переименовать ветку
<kbd>u</kbd>: Установить/убрать upstream-ветку
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -198,6 +213,7 @@ _Связки клавиш_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Скопировать SHA коммита в буфер обмена
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: Переключить коммит
<kbd>y</kbd>: Скопировать атрибут коммита
<kbd>o</kbd>: Открыть коммит в браузере
@@ -215,6 +231,7 @@ _Связки клавиш_
<pre>
<kbd>&lt;c-o&gt;</kbd>: Скопировать название подмодуля в буфер обмена
<kbd>&lt;enter&gt;</kbd>: Ввести подмодуль
<kbd>&lt;space&gt;</kbd>: Ввести подмодуль
<kbd>d</kbd>: Удалить подмодуль
<kbd>u</kbd>: Обновить подмодуль
<kbd>n</kbd>: Добавить новый подмодуль
@@ -264,6 +281,7 @@ _Связки клавиш_
<kbd>P</kbd>: Отправить тег
<kbd>n</kbd>: Создать тег
<kbd>g</kbd>: Просмотреть параметры сброса
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -279,6 +297,7 @@ _Связки клавиш_
<kbd>d</kbd>: Удалить ветку
<kbd>u</kbd>: Установить как upstream-ветку переключённую ветку
<kbd>g</kbd>: Просмотреть параметры сброса
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -328,6 +347,7 @@ _Связки клавиш_
<kbd>d</kbd>: Удалить припрятанные изменения из хранилища
<kbd>n</kbd>: Новая ветка
<kbd>r</kbd>: Переименовать хранилище
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: Просмотреть файлы выбранного элемента
<kbd>/</kbd>: Filter the current view by text
</pre>
+20
View File
@@ -48,6 +48,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: 将提交的 SHA 复制到剪贴板
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 检出提交
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: 在浏览器中打开提交
@@ -60,6 +61,17 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>/</kbd>: Filter the current view by text
</pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## 分支页面
<pre>
@@ -80,6 +92,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>g</kbd>: 查看重置选项
<kbd>R</kbd>: 重命名分支
<kbd>u</kbd>: Set/Unset upstream
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 查看提交
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -88,6 +101,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: 将提交的 SHA 复制到剪贴板
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 检出提交
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: 在浏览器中打开提交
@@ -105,6 +119,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;c-o&gt;</kbd>: 将子模块名称复制到剪贴板
<kbd>&lt;enter&gt;</kbd>: 输入子模块
<kbd>&lt;space&gt;</kbd>: 输入子模块
<kbd>d</kbd>: 删除子模块
<kbd>u</kbd>: 更新子模块
<kbd>n</kbd>: 添加新的子模块
@@ -132,11 +147,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;c-j&gt;</kbd>: 下移提交
<kbd>&lt;c-k&gt;</kbd>: 上移提交
<kbd>v</kbd>: 粘贴提交(拣选)
<kbd>B</kbd>: Mark commit as base commit for rebase
<kbd>A</kbd>: 用已暂存的更改来修补提交
<kbd>a</kbd>: Set/Reset commit author
<kbd>t</kbd>: 还原提交
<kbd>T</kbd>: 标签提交
<kbd>&lt;c-l&gt;</kbd>: 打开日志菜单
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 检出提交
<kbd>y</kbd>: Copy commit attribute
<kbd>o</kbd>: 在浏览器中打开提交
@@ -221,6 +238,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>P</kbd>: 推送标签
<kbd>n</kbd>: 创建标签
<kbd>g</kbd>: 查看重置选项
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 查看提交
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -303,6 +321,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: 删除
<kbd>n</kbd>: 新分支
<kbd>r</kbd>: Rename stash
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 查看提交的文件
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -318,6 +337,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>d</kbd>: 删除分支
<kbd>u</kbd>: 设置为检出分支的上游
<kbd>g</kbd>: 查看重置选项
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 查看提交
<kbd>/</kbd>: Filter the current view by text
</pre>
+20
View File
@@ -48,6 +48,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<pre>
<kbd>&lt;c-o&gt;</kbd>: 複製提交 SHA 到剪貼簿
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 檢出提交
<kbd>y</kbd>: 複製提交屬性
<kbd>o</kbd>: 在瀏覽器中開啟提交
@@ -60,6 +61,17 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>/</kbd>: Filter the current view by text
</pre>
## Worktrees
<pre>
<kbd>n</kbd>: Create worktree
<kbd>&lt;space&gt;</kbd>: Switch to worktree
<kbd>&lt;enter&gt;</kbd>: Switch to worktree
<kbd>o</kbd>: Open in editor
<kbd>d</kbd>: Remove worktree
<kbd>/</kbd>: Filter the current view by text
</pre>
## 主視窗 (一般)
<pre>
@@ -133,6 +145,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<pre>
<kbd>&lt;c-o&gt;</kbd>: 複製提交 SHA 到剪貼簿
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 檢出提交
<kbd>y</kbd>: 複製提交屬性
<kbd>o</kbd>: 在瀏覽器中開啟提交
@@ -150,6 +163,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<pre>
<kbd>&lt;c-o&gt;</kbd>: 複製子模組名稱到剪貼簿
<kbd>&lt;enter&gt;</kbd>: 進入子模組
<kbd>&lt;space&gt;</kbd>: 進入子模組
<kbd>d</kbd>: 移除子模組
<kbd>u</kbd>: 更新子模組
<kbd>n</kbd>: 新增子模組
@@ -177,11 +191,13 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>&lt;c-j&gt;</kbd>: 向下移動提交
<kbd>&lt;c-k&gt;</kbd>: 向上移動提交
<kbd>v</kbd>: 貼上提交 (揀選)
<kbd>B</kbd>: Mark commit as base commit for rebase
<kbd>A</kbd>: 使用已預存的更改修正提交
<kbd>a</kbd>: 設置/重設提交作者
<kbd>t</kbd>: 還原提交
<kbd>T</kbd>: 打標籤到提交
<kbd>&lt;c-l&gt;</kbd>: 開啟記錄選單
<kbd>w</kbd>: View worktree options
<kbd>&lt;space&gt;</kbd>: 檢出提交
<kbd>y</kbd>: 複製提交屬性
<kbd>o</kbd>: 在瀏覽器中開啟提交
@@ -223,6 +239,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>d</kbd>: 捨棄
<kbd>n</kbd>: 新分支
<kbd>r</kbd>: 重新命名收藏
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -247,6 +264,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>g</kbd>: 檢視重設選項
<kbd>R</kbd>: 重新命名分支
<kbd>u</kbd>: 設定/取消設定上游
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 檢視提交
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -259,6 +277,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>P</kbd>: 推送標籤
<kbd>n</kbd>: 建立標籤
<kbd>g</kbd>: 檢視重設選項
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 檢視提交
<kbd>/</kbd>: Filter the current view by text
</pre>
@@ -328,6 +347,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>d</kbd>: 刪除分支
<kbd>u</kbd>: 將此分支設為當前分支之上游
<kbd>g</kbd>: 檢視重設選項
<kbd>w</kbd>: View worktree options
<kbd>&lt;enter&gt;</kbd>: 檢視提交
<kbd>/</kbd>: Filter the current view by text
</pre>
+11 -11
View File
@@ -1,31 +1,29 @@
module github.com/jesseduffield/lazygit
go 1.18
go 1.20
require (
github.com/OpenPeeDeeP/xdg v1.0.0
github.com/atotto/clipboard v0.1.4
github.com/aybabtme/humanlog v0.4.1
github.com/cli/safeexec v1.0.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/fsmiamoto/git-todo-parser v0.0.5
github.com/fsnotify/fsnotify v1.4.7
github.com/gdamore/tcell/v2 v2.6.0
github.com/go-errors/errors v1.4.2
github.com/gookit/color v1.4.2
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b
github.com/jesseduffield/gocui v0.3.1-0.20230806095321-ac7b03108825
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/kyokomi/emoji/v2 v2.2.8
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-runewidth v0.0.14
github.com/mattn/go-runewidth v0.0.15
github.com/mgutz/str v1.2.0
github.com/pmezard/go-difflib v1.0.0
github.com/sahilm/fuzzy v0.1.0
@@ -33,9 +31,12 @@ require (
github.com/sanity-io/litter v1.5.2
github.com/sasha-s/go-deadlock v0.3.1
github.com/sirupsen/logrus v1.4.2
github.com/spf13/afero v1.9.5
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stefanhaller/tcell/v2 v2.6.2-0.20230806061358-2dfa11eddb68
github.com/stretchr/testify v1.8.0
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -64,11 +65,10 @@ require (
github.com/rivo/uniseg v0.4.4 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/xanzy/ssh-agent v0.2.1 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
+418 -21
View File
@@ -1,3 +1,43 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OpenPeeDeeP/xdg v1.0.0 h1:UDLmNjCGFZZCaVMB74DqYEtXkHxnTxcr4FeJVF9uCn8=
github.com/OpenPeeDeeP/xdg v1.0.0/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
@@ -11,10 +51,16 @@ github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -24,19 +70,21 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsmiamoto/git-todo-parser v0.0.5 h1:Bhzd/vz/6Qm3udfkd6NO9fWfD3TpwR9ucp3N75/J5I8=
github.com/fsmiamoto/git-todo-parser v0.0.5/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -48,19 +96,78 @@ github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agR
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
@@ -72,22 +179,28 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b h1:8FmmdaYHes1m3oNyNdS+VIgkgkFpNZAWuwTnvp0tG14=
github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
github.com/jesseduffield/gocui v0.3.1-0.20230806095321-ac7b03108825 h1:4Ea8qV/BbZAGcXd8MAufDsbwwfz2pbRZdqIodC/XHZs=
github.com/jesseduffield/gocui v0.3.1-0.20230806095321-ac7b03108825/go.mod h1:trXE7RRGL2hTsv+Ntk+SHLtRobg9JE138n3Ug/X2Cf4=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -99,7 +212,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/kyokomi/emoji/v2 v2.2.8 h1:jcofPxjHWEkJtkIbcLHvZhxKgCPl6C7MyjTrD4KDqUE=
github.com/kyokomi/emoji/v2 v2.2.8/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -111,9 +223,9 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -127,16 +239,19 @@ github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
@@ -149,8 +264,12 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stefanhaller/tcell/v2 v2.6.2-0.20230806061358-2dfa11eddb68 h1:NSTj9xAKUu85d6pAdNFyblL84BfiOB1rVnzxQO/cYUk=
github.com/stefanhaller/tcell/v2 v2.6.2-0.20230806061358-2dfa11eddb68/go.mod h1:PuJ7T6QKbsU7iVOHlhRoV3D/ipIAJsyiV4dbwcVaYj8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
@@ -158,6 +277,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -169,37 +289,161 @@ github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 h1:s/+U+w0teGzcoH2mdIlFQ6KfVKGaYpgyGdUefZrn9TU=
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -207,30 +451,173 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0 h1:KzcWKJ0nMAmGoBhYVMnkWc1rXjB42lKy5aIys4TdLOA=
@@ -246,3 +633,13 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+9 -4
View File
@@ -11,8 +11,8 @@ import (
"github.com/go-errors/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/afero"
"github.com/jesseduffield/generics/slices"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
@@ -75,6 +75,7 @@ func NewCommon(config config.AppConfigurer) (*common.Common, error) {
Tr: tr,
UserConfig: userConfig,
Debug: config.GetDebug(),
Fs: afero.NewOsFs(),
}, nil
}
@@ -265,7 +266,11 @@ func (app *App) Run(startArgs appTypes.StartArgs) error {
// Close closes any resources
func (app *App) Close() error {
return slices.TryForEach(app.closers, func(closer io.Closer) error {
return closer.Close()
})
for _, closer := range app.closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
+2 -2
View File
@@ -5,12 +5,12 @@ import (
"fmt"
"log"
"os"
"os/exec"
"strconv"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
@@ -92,7 +92,7 @@ func getDaemonKind() DaemonKind {
}
func getCommentChar() byte {
cmd := secureexec.Command("git", "config", "--get", "--null", "core.commentChar")
cmd := exec.Command("git", "config", "--get", "--null", "core.commentChar")
if output, err := cmd.Output(); err == nil && len(output) == 2 {
return output[0]
}
+3 -3
View File
@@ -6,10 +6,10 @@ import (
"strings"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/samber/lo"
)
type TodoLine struct {
@@ -26,11 +26,11 @@ func (self *TodoLine) ToString() string {
}
func TodoLinesToString(todoLines []TodoLine) string {
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
lines := lo.Map(todoLines, func(todoLine TodoLine, _ int) string {
return todoLine.ToString()
})
return strings.Join(slices.Reverse(lines), "")
return strings.Join(lo.Reverse(lines), "")
}
type ChangeTodoAction struct {
+12 -13
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"runtime/debug"
@@ -17,7 +18,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/env"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/logs/tail"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"gopkg.in/yaml.v3"
@@ -63,8 +63,17 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
log.Fatal(absRepoPath + " is not a valid git repository.")
}
cliArgs.WorkTree = absRepoPath
cliArgs.GitDir = filepath.Join(absRepoPath, ".git")
err = os.Chdir(absRepoPath)
if err != nil {
log.Fatalf("Failed to change directory to %s: %v", absRepoPath, err)
}
} else if cliArgs.WorkTree != "" {
env.SetWorkTreeEnv(cliArgs.WorkTree)
if err := os.Chdir(cliArgs.WorkTree); err != nil {
log.Fatalf("Failed to change directory to %s: %v", cliArgs.WorkTree, err)
}
}
if cliArgs.CustomConfigFile != "" {
@@ -75,10 +84,6 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
os.Setenv("CONFIG_DIR", cliArgs.UseConfigDir)
}
if cliArgs.WorkTree != "" {
env.SetGitWorkTreeEnv(cliArgs.WorkTree)
}
if cliArgs.GitDir != "" {
env.SetGitDirEnv(cliArgs.GitDir)
}
@@ -115,12 +120,6 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
os.Exit(0)
}
if cliArgs.WorkTree != "" {
if err := os.Chdir(cliArgs.WorkTree); err != nil {
log.Fatal(err.Error())
}
}
tempDir, err := os.MkdirTemp("", "lazygit-*")
if err != nil {
log.Fatal(err.Error())
@@ -277,7 +276,7 @@ func mergeBuildInfo(buildInfo *BuildInfo) {
}
func getGitVersionInfo() string {
cmd := secureexec.Command("git", "--version")
cmd := exec.Command("git", "--version")
stdout, _ := cmd.Output()
gitVersion := strings.Trim(strings.TrimPrefix(string(stdout), "git version "), " \r\n")
return gitVersion
+3 -3
View File
@@ -3,8 +3,8 @@ package app
import (
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/samber/lo"
)
type errorMapping struct {
@@ -18,7 +18,7 @@ func knownError(tr *i18n.TranslationSet, err error) (string, bool) {
knownErrorMessages := []string{tr.MinGitVersionError}
if slices.Contains(knownErrorMessages, errorMessage) {
if lo.Contains(knownErrorMessages, errorMessage) {
return errorMessage, true
}
@@ -29,7 +29,7 @@ func knownError(tr *i18n.TranslationSet, err error) (string, bool) {
},
}
if mapping, ok := slices.Find(mappings, func(mapping errorMapping) bool {
if mapping, ok := lo.Find(mappings, func(mapping errorMapping) bool {
return strings.Contains(errorMessage, mapping.originalError)
}); ok {
return mapping.newError, true
+4 -4
View File
@@ -15,7 +15,6 @@ import (
"strings"
"github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
@@ -23,6 +22,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/samber/lo"
"golang.org/x/exp/slices"
)
type bindingSection struct {
@@ -101,7 +101,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
"reflogCommits": tr.ReflogCommitsTitle,
"tags": tr.TagsTitle,
"commitFiles": tr.CommitFilesTitle,
"commitMessage": tr.CommitMessageTitle,
"commitMessage": tr.CommitSummaryTitle,
"commitDescription": tr.CommitDescriptionTitle,
"commits": tr.CommitsTitle,
"confirmation": tr.ConfirmationTitle,
@@ -129,7 +129,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*bindingSection {
excludedViews := []string{"stagingSecondary", "patchBuildingSecondary"}
bindingsToDisplay := slices.Filter(bindings, func(binding *types.Binding) bool {
bindingsToDisplay := lo.Filter(bindings, func(binding *types.Binding, _ int) bool {
if lo.Contains(excludedViews, binding.ViewName) {
return false
}
@@ -162,7 +162,7 @@ func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*b
return a.header.title < b.header.title
})
return slices.Map(bindingGroups, func(hb headerWithBindings) *bindingSection {
return lo.Map(bindingGroups, func(hb headerWithBindings, _ int) *bindingSection {
return &bindingSection{
title: hb.header.title,
bindings: hb.bindings,
+61 -106
View File
@@ -2,11 +2,13 @@ package commands
import (
"os"
"path"
"path/filepath"
"strings"
"github.com/go-errors/errors"
"github.com/sasha-s/go-deadlock"
"github.com/spf13/afero"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
@@ -38,6 +40,8 @@ type GitCommand struct {
WorkingTree *git_commands.WorkingTreeCommands
Bisect *git_commands.BisectCommands
Worktree *git_commands.WorktreeCommands
Version *git_commands.GitVersion
RepoPaths *git_commands.RepoPaths
Loaders Loaders
}
@@ -61,17 +65,49 @@ func NewGitCommand(
gitConfig git_config.IGitConfig,
syncMutex *deadlock.Mutex,
) (*GitCommand, error) {
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
return nil, err
currentPath, err := os.Getwd()
if err != nil {
return nil, utils.WrapError(err)
}
repo, err := setupRepository(gogit.PlainOpenWithOptions, gogit.PlainOpenOptions{DetectDotGit: false, EnableDotGitCommonDir: true}, cmn.Tr.GitconfigParseErr)
if err != nil {
return nil, err
// converting to forward slashes for the sake of windows (which uses backwards slashes). We want everything
// to have forward slashes internally
currentPath = filepath.ToSlash(currentPath)
gitDir := env.GetGitDirEnv()
if gitDir != "" {
// we've been given the git directory explicitly so no need to navigate to it
_, err := cmn.Fs.Stat(gitDir)
if err != nil {
return nil, utils.WrapError(err)
}
} else {
// we haven't been given the git dir explicitly so we assume it's in the current working directory as `.git/` (or an ancestor directory)
rootDirectory, err := findWorktreeRoot(cmn.Fs, currentPath)
if err != nil {
return nil, utils.WrapError(err)
}
currentPath = rootDirectory
err = os.Chdir(rootDirectory)
if err != nil {
return nil, utils.WrapError(err)
}
}
dotGitDir, err := findDotGitDir(os.Stat, os.ReadFile)
repoPaths, err := git_commands.GetRepoPaths(cmn.Fs, currentPath)
if err != nil {
return nil, errors.Errorf("Error getting repo paths: %v", err)
}
repository, err := gogit.PlainOpenWithOptions(
repoPaths.WorktreeGitDirPath(),
&gogit.PlainOpenOptions{DetectDotGit: false, EnableDotGitCommonDir: true},
)
if err != nil {
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
return nil, errors.New(cmn.Tr.GitconfigParseErr)
}
return nil, err
}
@@ -80,8 +116,8 @@ func NewGitCommand(
version,
osCommand,
gitConfig,
dotGitDir,
repo,
repoPaths,
repository,
syncMutex,
), nil
}
@@ -91,7 +127,7 @@ func NewGitCommandAux(
version *git_commands.GitVersion,
osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig,
dotGitDir string,
repoPaths *git_commands.RepoPaths,
repo *gogit.Repository,
syncMutex *deadlock.Mutex,
) *GitCommand {
@@ -104,9 +140,9 @@ func NewGitCommandAux(
// common ones are: cmn, osCommand, dotGitDir, configCommands
configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo)
fileLoader := git_commands.NewFileLoader(cmn, cmd, configCommands)
gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, repoPaths, repo, configCommands, syncMutex)
gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, dotGitDir, repo, configCommands, syncMutex)
fileLoader := git_commands.NewFileLoader(gitCommon, cmd, configCommands)
statusCommands := git_commands.NewStatusCommands(gitCommon)
flowCommands := git_commands.NewFlowCommands(gitCommon)
remoteCommands := git_commands.NewRemoteCommands(gitCommon)
@@ -133,10 +169,10 @@ func NewGitCommandAux(
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode, gitCommon)
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon)
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
worktreeLoader := git_commands.NewWorktreeLoader(cmn, cmd)
worktreeLoader := git_commands.NewWorktreeLoader(gitCommon)
stashLoader := git_commands.NewStashLoader(cmn, cmd)
tagLoader := git_commands.NewTagLoader(cmn, cmd)
@@ -159,6 +195,7 @@ func NewGitCommandAux(
Bisect: bisectCommands,
WorkingTree: workingTreeCommands,
Worktree: worktreeCommands,
Version: version,
Loaders: Loaders{
BranchLoader: branchLoader,
CommitFileLoader: commitFileLoader,
@@ -170,118 +207,36 @@ func NewGitCommandAux(
StashLoader: stashLoader,
TagLoader: tagLoader,
},
RepoPaths: repoPaths,
}
}
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
gitDir := env.GetGitDirEnv()
if gitDir != "" {
// we've been given the git directory explicitly so no need to navigate to it
_, err := stat(gitDir)
if err != nil {
return utils.WrapError(err)
}
return nil
}
// we haven't been given the git dir explicitly so we assume it's in the current working directory as `.git/` (or an ancestor directory)
// this returns the root of the current worktree. So if you start lazygit from within
// a subdirectory of the worktree, it will start in the context of the root of that worktree
func findWorktreeRoot(fs afero.Fs, currentPath string) (string, error) {
for {
_, err := stat(".git")
// we don't care if .git is a directory or a file: either is okay.
_, err := fs.Stat(path.Join(currentPath, ".git"))
if err == nil {
return nil
return currentPath, nil
}
if !os.IsNotExist(err) {
return utils.WrapError(err)
return "", utils.WrapError(err)
}
if err = chdir(".."); err != nil {
return utils.WrapError(err)
}
currentPath = path.Dir(currentPath)
currentPath, err := os.Getwd()
if err != nil {
return err
}
atRoot := currentPath == filepath.Dir(currentPath)
atRoot := currentPath == path.Dir(currentPath)
if atRoot {
// we should never really land here: the code that creates GitCommand should
// verify we're in a git directory
return errors.New("Must open lazygit in a git repository")
return "", errors.New("Must open lazygit in a git repository")
}
}
}
// resolvePath takes a path containing a symlink and returns the true path
func resolvePath(path string) (string, error) {
l, err := os.Lstat(path)
if err != nil {
return "", err
}
if l.Mode()&os.ModeSymlink == 0 {
return path, nil
}
return filepath.EvalSymlinks(path)
}
func setupRepository(openGitRepository func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error), options gogit.PlainOpenOptions, gitConfigParseErrorStr string) (*gogit.Repository, error) {
unresolvedPath := env.GetGitDirEnv()
if unresolvedPath == "" {
var err error
unresolvedPath, err = os.Getwd()
if err != nil {
return nil, err
}
}
path, err := resolvePath(unresolvedPath)
if err != nil {
return nil, err
}
repository, err := openGitRepository(path, &options)
if err != nil {
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
return nil, errors.New(gitConfigParseErrorStr)
}
return nil, err
}
return repository, err
}
func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filename string) ([]byte, error)) (string, error) {
if env.GetGitDirEnv() != "" {
return env.GetGitDirEnv(), nil
}
f, err := stat(".git")
if err != nil {
return "", err
}
if f.IsDir() {
return ".git", nil
}
fileBytes, err := readFile(".git")
if err != nil {
return "", err
}
fileContent := string(fileBytes)
if !strings.HasPrefix(fileContent, "gitdir: ") {
return "", errors.New(".git is a file which suggests we are in a submodule or a worktree but the file's contents do not contain a gitdir pointing to the actual .git directory")
}
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
}
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
return osCommand.Cmd.New(git_commands.NewGitCmd("rev-parse").Arg("--git-dir").ToArgv()).DontLog().Run()
}
+17 -4
View File
@@ -19,12 +19,16 @@ func NewBisectCommands(gitCommon *GitCommon) *BisectCommands {
// This command is pretty cheap to run so we're not storing the result anywhere.
// But if it becomes problematic we can chang that.
func (self *BisectCommands) GetInfo() *BisectInfo {
return self.GetInfoForGitDir(self.repoPaths.WorktreeGitDirPath())
}
func (self *BisectCommands) GetInfoForGitDir(gitDir string) *BisectInfo {
var err error
info := &BisectInfo{started: false, log: self.Log, newTerm: "bad", oldTerm: "good"}
// we return nil if we're not in a git bisect session.
// we know we're in a session by the presence of a .git/BISECT_START file
bisectStartPath := filepath.Join(self.dotGitDir, "BISECT_START")
bisectStartPath := filepath.Join(gitDir, "BISECT_START")
exists, err := self.os.FileExists(bisectStartPath)
if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error())
@@ -44,7 +48,7 @@ func (self *BisectCommands) GetInfo() *BisectInfo {
info.started = true
info.start = strings.TrimSpace(string(startContent))
termsContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_TERMS"))
termsContent, err := os.ReadFile(filepath.Join(gitDir, "BISECT_TERMS"))
if err != nil {
// old git versions won't have this file so we default to bad/good
} else {
@@ -53,7 +57,7 @@ func (self *BisectCommands) GetInfo() *BisectInfo {
info.oldTerm = splitContent[1]
}
bisectRefsDir := filepath.Join(self.dotGitDir, "refs", "bisect")
bisectRefsDir := filepath.Join(gitDir, "refs", "bisect")
files, err := os.ReadDir(bisectRefsDir)
if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error())
@@ -85,7 +89,7 @@ func (self *BisectCommands) GetInfo() *BisectInfo {
info.statusMap[sha] = status
}
currentContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_EXPECTED_REV"))
currentContent, err := os.ReadFile(filepath.Join(gitDir, "BISECT_EXPECTED_REV"))
if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error())
return info
@@ -121,6 +125,15 @@ func (self *BisectCommands) Start() error {
return self.cmd.New(cmdArgs).StreamOutput().Run()
}
func (self *BisectCommands) StartWithTerms(oldTerm string, newTerm string) error {
cmdArgs := NewGitCmd("bisect").Arg("start").
Arg("--term-old=" + oldTerm).
Arg("--term-new=" + newTerm).
ToArgv()
return self.cmd.New(cmdArgs).StreamOutput().Run()
}
// tells us whether we've found our problem commit(s). We return a string slice of
// commit sha's if we're done, and that slice may have more that one item if
// skipped commits are involved.
+2 -2
View File
@@ -2,7 +2,7 @@ package git_commands
import (
"github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/generics/slices"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
@@ -97,5 +97,5 @@ func (self *BisectInfo) Bisecting() bool {
return false
}
return slices.Contains(maps.Values(self.statusMap), BisectStatusOld)
return lo.Contains(maps.Values(self.statusMap), BisectStatusOld)
}
+15
View File
@@ -70,6 +70,21 @@ func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) {
}, nil
}
// CurrentBranchName get name of current branch
func (self *BranchCommands) CurrentBranchName() (string, error) {
cmdArgs := NewGitCmd("rev-parse").
Arg("--abbrev-ref").
Arg("--verify").
Arg("HEAD").
ToArgv()
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
if err == nil {
return strings.TrimSpace(output), nil
}
return "", err
}
// Delete delete branch
func (self *BranchCommands) Delete(branch string, force bool) error {
cmdArgs := NewGitCmd("branch").
+22 -26
View File
@@ -2,18 +2,17 @@ package git_commands
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/go-git/v5/config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"golang.org/x/exp/slices"
)
// context:
@@ -76,20 +75,26 @@ outer:
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = slices.Remove(branches, j)
branches = utils.Remove(branches, j)
continue outer
}
}
}
branches = slices.Prepend(branches, branchesWithRecency...)
// Sort branches that don't have a recency value alphabetically
// (we're really doing this for the sake of deterministic behaviour across git versions)
slices.SortFunc(branches, func(a *models.Branch, b *models.Branch) bool {
return a.Name < b.Name
})
branches = utils.Prepend(branches, branchesWithRecency...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = slices.Move(branches, i, 0)
branches = utils.Move(branches, i, 0)
break
}
}
@@ -98,7 +103,7 @@ outer:
if err != nil {
return nil, err
}
branches = slices.Prepend(branches, &models.Branch{Name: info.RefName, DisplayName: info.DisplayName, Head: true, DetachedHead: info.DetachedHead, Recency: " *"})
branches = utils.Prepend(branches, &models.Branch{Name: info.RefName, DisplayName: info.DisplayName, Head: true, DetachedHead: info.DetachedHead, Recency: " *"})
}
configBranches, err := self.config.Branches()
@@ -118,11 +123,6 @@ outer:
}
func (self *BranchLoader) obtainBranches() []*models.Branch {
currentDir, err := os.Getwd()
if err != nil {
panic(err)
}
output, err := self.getRawBranches()
if err != nil {
panic(err)
@@ -131,7 +131,7 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
return slices.FilterMap(outputLines, func(line string) (*models.Branch, bool) {
return lo.FilterMap(outputLines, func(line string, _ int) (*models.Branch, bool) {
if line == "" {
return nil, false
}
@@ -144,7 +144,7 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
return nil, false
}
return obtainBranch(split, currentDir), true
return obtainBranch(split), true
})
}
@@ -171,33 +171,29 @@ var branchFields = []string{
"upstream:short",
"upstream:track",
"subject",
fmt.Sprintf("objectname:short=%d", utils.COMMIT_HASH_SHORT_SIZE),
"worktreepath",
"objectname",
}
// Obtain branch information from parsed line output of getRawBranches()
func obtainBranch(split []string, currentDir string) *models.Branch {
func obtainBranch(split []string) *models.Branch {
headMarker := split[0]
fullName := split[1]
upstreamName := split[2]
track := split[3]
subject := split[4]
commitHash := split[5]
branchDir := split[6]
checkedOutByOtherWorktree := len(branchDir) > 0 && branchDir != currentDir
name := strings.TrimPrefix(fullName, "heads/")
pushables, pullables, gone := parseUpstreamInfo(upstreamName, track)
return &models.Branch{
Name: name,
Pushables: pushables,
Pullables: pullables,
UpstreamGone: gone,
Head: headMarker == "*",
Subject: subject,
CommitHash: commitHash,
CheckedOutByOtherWorktree: checkedOutByOtherWorktree,
Name: name,
Pushables: pushables,
Pullables: pullables,
UpstreamGone: gone,
Head: headMarker == "*",
Subject: subject,
CommitHash: commitHash,
}
}
@@ -81,7 +81,7 @@ func TestObtainBranch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
branch := obtainBranch(s.input, "current-dir")
branch := obtainBranch(s.input)
assert.EqualValues(t, s.expectedBranch, branch)
})
}
+7 -16
View File
@@ -50,13 +50,13 @@ func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars [
Run()
}
func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
messageArgs := self.commitMessageArgs(message)
func (self *CommitCommands) CommitCmdObj(summary string, description string) oscommands.ICmdObj {
messageArgs := self.commitMessageArgs(summary, description)
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
cmdArgs := NewGitCmd("commit").
ArgIf(skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix), "--no-verify").
ArgIf(skipHookPrefix != "" && strings.HasPrefix(summary, skipHookPrefix), "--no-verify").
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
Arg(messageArgs...).
ToArgv()
@@ -69,8 +69,8 @@ func (self *CommitCommands) RewordLastCommitInEditorCmdObj() oscommands.ICmdObj
}
// RewordLastCommit rewords the topmost commit with the given message
func (self *CommitCommands) RewordLastCommit(message string) error {
messageArgs := self.commitMessageArgs(message)
func (self *CommitCommands) RewordLastCommit(summary string, description string) error {
messageArgs := self.commitMessageArgs(summary, description)
cmdArgs := NewGitCmd("commit").
Arg("--allow-empty", "--amend", "--only").
@@ -80,9 +80,8 @@ func (self *CommitCommands) RewordLastCommit(message string) error {
return self.cmd.New(cmdArgs).Run()
}
func (self *CommitCommands) commitMessageArgs(message string) []string {
msg, description, _ := strings.Cut(message, "\n")
args := []string{"-m", msg}
func (self *CommitCommands) commitMessageArgs(summary string, description string) []string {
args := []string{"-m", summary}
if description != "" {
args = append(args, "-m", description)
@@ -108,14 +107,6 @@ func (self *CommitCommands) signoffFlag() string {
}
}
// Get the subject of the HEAD commit
func (self *CommitCommands) GetHeadCommitMessage() (string, error) {
cmdArgs := NewGitCmd("log").Arg("-1", "--pretty=%s").ToArgv()
message, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
return strings.TrimSpace(message), err
}
func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) {
cmdArgs := NewGitCmd("rev-list").
Arg("--format=%B", "--max-count=1", commitSha).
@@ -3,7 +3,6 @@ package git_commands
import (
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
@@ -52,7 +51,7 @@ func getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
}
// typical result looks like 'A my_file' meaning my_file was added
return slices.Map(lo.Chunk(lines, 2), func(chunk []string) *models.CommitFile {
return lo.Map(lo.Chunk(lines, 2), func(chunk []string, _ int) *models.CommitFile {
return &models.CommitFile{
ChangeStatus: chunk[0],
Name: chunk[1],
+77 -32
View File
@@ -8,13 +8,14 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
@@ -45,7 +46,6 @@ type CommitLoader struct {
func NewCommitLoader(
cmn *common.Common,
cmd oscommands.ICmdObjBuilder,
dotGitDir string,
getRebaseMode func() (enums.RebaseMode, error),
gitCommon *GitCommon,
) *CommitLoader {
@@ -55,7 +55,6 @@ func NewCommitLoader(
getRebaseMode: getRebaseMode,
readFile: os.ReadFile,
walkFiles: filepath.Walk,
dotGitDir: dotGitDir,
mainBranches: nil,
GitCommon: gitCommon,
}
@@ -84,31 +83,60 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
commits = append(commits, rebasingCommits...)
}
wg := sync.WaitGroup{}
wg.Add(2)
var logErr error
go utils.Safe(func() {
defer wg.Done()
logErr = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line)
commits = append(commits, commit)
return false, nil
})
})
var ancestor string
go utils.Safe(func() {
defer wg.Done()
ancestor = self.getMergeBase(opts.RefName)
})
passedFirstPushedCommit := false
// I can get this before
firstPushedCommit, err := self.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line)
wg.Wait()
if logErr != nil {
return nil, logErr
}
for _, commit := range commits {
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]models.CommitStatus{true: models.StatusUnpushed, false: models.StatusPushed}[!passedFirstPushedCommit]
commits = append(commits, commit)
return false, nil
})
if err != nil {
return nil, err
if passedFirstPushedCommit {
commit.Status = models.StatusPushed
} else {
commit.Status = models.StatusUnpushed
}
}
if len(commits) == 0 {
return commits, nil
}
commits = self.setCommitMergedStatuses(opts.RefName, commits)
if ancestor != "" {
commits = setCommitMergedStatuses(ancestor, commits)
}
return commits, nil
}
@@ -204,7 +232,7 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
return nil, nil
}
commitShas := slices.FilterMap(commits, func(commit *models.Commit) (string, bool) {
commitShas := lo.FilterMap(commits, func(commit *models.Commit, _ int) (string, bool) {
return commit.Sha, commit.Sha != ""
})
@@ -268,7 +296,7 @@ func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*mo
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
rewrittenCount := 0
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-apply/rewritten"))
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply/rewritten"))
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
@@ -276,7 +304,7 @@ func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*models.Commit{}
err = self.walkFiles(filepath.Join(self.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
err = self.walkFiles(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply"), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
@@ -317,7 +345,7 @@ func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"))
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
@@ -350,7 +378,7 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
// Command does not have a commit associated, skip
continue
}
commits = slices.Prepend(commits, &models.Commit{
commits = utils.Prepend(commits, &models.Commit{
Sha: t.Commit,
Name: t.Msg,
Status: models.StatusRebasing,
@@ -362,7 +390,7 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
}
func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string {
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/done"))
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/done"))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading rebase-merge/done: %s", err.Error()))
return ""
@@ -375,7 +403,7 @@ func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string {
}
amendFileExists := false
if _, err := os.Stat(filepath.Join(self.dotGitDir, "rebase-merge/amend")); err == nil {
if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend")); err == nil {
amendFileExists = true
}
@@ -464,14 +492,11 @@ func (self *CommitLoader) commitFromPatch(content string) *models.Commit {
}
}
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) []*models.Commit {
ancestor := self.getMergeBase(refName)
if ancestor == "" {
return commits
}
func setCommitMergedStatuses(ancestor string, commits []*models.Commit) []*models.Commit {
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
// some commits aren't really commits and don't have sha's, such as the update-ref todo
if commit.Sha != "" && strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != models.StatusPushed && commit.Status != models.StatusUnpushed {
@@ -510,13 +535,25 @@ func (self *CommitLoader) getMergeBase(refName string) string {
}
func (self *CommitLoader) getExistingMainBranches() []string {
return lo.FilterMap(self.UserConfig.Git.MainBranches,
func(branchName string, _ int) (string, bool) {
var existingBranches []string
var wg sync.WaitGroup
mainBranches := self.UserConfig.Git.MainBranches
existingBranches = make([]string, len(mainBranches))
for i, branchName := range mainBranches {
wg.Add(1)
i := i
branchName := branchName
go utils.Safe(func() {
defer wg.Done()
// Try to determine upstream of local main branch
if ref, err := self.cmd.New(
NewGitCmd("rev-parse").Arg("--symbolic-full-name", branchName+"@{u}").ToArgv(),
).DontLog().RunWithOutput(); err == nil {
return strings.TrimSpace(ref), true
existingBranches[i] = strings.TrimSpace(ref)
return
}
// If this failed, a local branch for this main branch doesn't exist or it
@@ -525,7 +562,8 @@ func (self *CommitLoader) getExistingMainBranches() []string {
if err := self.cmd.New(
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
).DontLog().Run(); err == nil {
return ref, true
existingBranches[i] = ref
return
}
// If this failed as well, try if we have the main branch as a local
@@ -535,11 +573,18 @@ func (self *CommitLoader) getExistingMainBranches() []string {
if err := self.cmd.New(
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
).DontLog().Run(); err == nil {
return ref, true
existingBranches[i] = ref
}
return "", false
})
}
wg.Wait()
existingBranches = lo.Filter(existingBranches, func(branch string, _ int) bool {
return branch != ""
})
return existingBranches
}
func ignoringWarnings(commandOutput string) string {
@@ -1,12 +1,12 @@
package git_commands
import (
"errors"
"path/filepath"
"strings"
"testing"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
@@ -506,3 +506,50 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
})
}
}
func TestCommitLoader_setCommitMergedStatuses(t *testing.T) {
type scenario struct {
testName string
commits []*models.Commit
ancestor string
expectedCommits []*models.Commit
}
scenarios := []scenario{
{
testName: "basic",
commits: []*models.Commit{
{Sha: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
{Sha: "67890", Name: "2", Action: models.ActionNone, Status: models.StatusPushed},
{Sha: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusPushed},
},
ancestor: "67890",
expectedCommits: []*models.Commit{
{Sha: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
{Sha: "67890", Name: "2", Action: models.ActionNone, Status: models.StatusMerged},
{Sha: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusMerged},
},
},
{
testName: "with update-ref",
commits: []*models.Commit{
{Sha: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
{Sha: "", Name: "", Action: todo.UpdateRef, Status: models.StatusNone},
{Sha: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusPushed},
},
ancestor: "deadbeef",
expectedCommits: []*models.Commit{
{Sha: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
{Sha: "", Name: "", Action: todo.UpdateRef, Status: models.StatusNone},
{Sha: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusPushed},
},
},
}
for _, scenario := range scenarios {
t.Run(scenario.testName, func(t *testing.T) {
expectedCommits := setCommitMergedStatuses(scenario.ancestor, scenario.commits)
assert.Equal(t, scenario.expectedCommits, expectedCommits)
})
}
}
+17 -12
View File
@@ -10,20 +10,23 @@ import (
func TestCommitRewordCommit(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
input string
testName string
runner *oscommands.FakeCmdObjRunner
summary string
description string
}
scenarios := []scenario{
{
"Single line reword",
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil),
"test",
"",
},
{
"Multi line reword",
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test", "-m", "line 2\nline 3"}, "", nil),
"test\nline 2\nline 3",
"test",
"line 2\nline 3",
},
}
for _, s := range scenarios {
@@ -31,7 +34,7 @@ func TestCommitRewordCommit(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner})
assert.NoError(t, instance.RewordLastCommit(s.input))
assert.NoError(t, instance.RewordLastCommit(s.summary, s.description))
s.runner.CheckForMissingCalls()
})
}
@@ -50,7 +53,8 @@ func TestCommitResetToCommit(t *testing.T) {
func TestCommitCommitCmdObj(t *testing.T) {
type scenario struct {
testName string
message string
summary string
description string
configSignoff bool
configSkipHookPrefix string
expectedArgs []string
@@ -59,35 +63,36 @@ func TestCommitCommitCmdObj(t *testing.T) {
scenarios := []scenario{
{
testName: "Commit",
message: "test",
summary: "test",
configSignoff: false,
configSkipHookPrefix: "",
expectedArgs: []string{"commit", "-m", "test"},
},
{
testName: "Commit with --no-verify flag",
message: "WIP: test",
summary: "WIP: test",
configSignoff: false,
configSkipHookPrefix: "WIP",
expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"},
},
{
testName: "Commit with multiline message",
message: "line1\nline2",
summary: "line1",
description: "line2",
configSignoff: false,
configSkipHookPrefix: "",
expectedArgs: []string{"commit", "-m", "line1", "-m", "line2"},
},
{
testName: "Commit with signoff",
message: "test",
summary: "test",
configSignoff: true,
configSkipHookPrefix: "",
expectedArgs: []string{"commit", "--signoff", "-m", "test"},
},
{
testName: "Commit with signoff and no-verify",
message: "WIP: test",
summary: "WIP: test",
configSignoff: true,
configSkipHookPrefix: "WIP",
expectedArgs: []string{"commit", "--no-verify", "--signoff", "-m", "WIP: test"},
@@ -104,7 +109,7 @@ func TestCommitCommitCmdObj(t *testing.T) {
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expectedArgs, "", nil)
instance := buildCommitCommands(commonDeps{userConfig: userConfig, runner: runner})
assert.NoError(t, instance.CommitCmdObj(s.message).Run())
assert.NoError(t, instance.CommitCmdObj(s.summary, s.description).Run())
runner.CheckForMissingCalls()
})
}
+3 -3
View File
@@ -12,7 +12,7 @@ type GitCommon struct {
version *GitVersion
cmd oscommands.ICmdObjBuilder
os *oscommands.OSCommand
dotGitDir string
repoPaths *RepoPaths
repo *gogit.Repository
config *ConfigCommands
// mutex for doing things like push/pull/fetch
@@ -24,7 +24,7 @@ func NewGitCommon(
version *GitVersion,
cmd oscommands.ICmdObjBuilder,
osCommand *oscommands.OSCommand,
dotGitDir string,
repoPaths *RepoPaths,
repo *gogit.Repository,
config *ConfigCommands,
syncMutex *deadlock.Mutex,
@@ -34,7 +34,7 @@ func NewGitCommon(
version: version,
cmd: cmd,
os: osCommand,
dotGitDir: dotGitDir,
repoPaths: repoPaths,
repo: repo,
config: config,
syncMutex: syncMutex,
+4
View File
@@ -107,3 +107,7 @@ func (self *ConfigCommands) GetCoreCommentChar() byte {
return '#'
}
func (self *ConfigCommands) GetRebaseUpdateRefs() bool {
return self.gitConfig.GetBool("rebase.updateRefs")
}
+16 -9
View File
@@ -11,6 +11,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spf13/afero"
)
type commonDeps struct {
@@ -20,9 +21,10 @@ type commonDeps struct {
gitConfig *git_config.FakeGitConfig
getenv func(string) string
removeFile func(string) error
dotGitDir string
common *common.Common
cmd *oscommands.CmdObjBuilder
fs afero.Fs
repoPaths *RepoPaths
}
func buildGitCommon(deps commonDeps) *GitCommon {
@@ -33,6 +35,16 @@ func buildGitCommon(deps commonDeps) *GitCommon {
gitCommon.Common = utils.NewDummyCommonWithUserConfig(deps.userConfig)
}
if deps.fs != nil {
gitCommon.Fs = deps.fs
}
if deps.repoPaths != nil {
gitCommon.repoPaths = deps.repoPaths
} else {
gitCommon.repoPaths = MockRepoPaths(".git")
}
runner := deps.runner
if runner == nil {
runner = oscommands.NewFakeRunner(nil)
@@ -81,11 +93,6 @@ func buildGitCommon(deps commonDeps) *GitCommon {
TempDir: os.TempDir(),
})
gitCommon.dotGitDir = deps.dotGitDir
if gitCommon.dotGitDir == "" {
gitCommon.dotGitDir = ".git"
}
return gitCommon
}
@@ -96,7 +103,7 @@ func buildRepo() *gogit.Repository {
}
func buildFileLoader(gitCommon *GitCommon) *FileLoader {
return NewFileLoader(gitCommon.Common, gitCommon.cmd, gitCommon.config)
return NewFileLoader(gitCommon, gitCommon.cmd, gitCommon.config)
}
func buildSubmoduleCommands(deps commonDeps) *SubmoduleCommands {
@@ -118,7 +125,7 @@ func buildWorkingTreeCommands(deps commonDeps) *WorkingTreeCommands {
return NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader)
}
func buildPatchCommands(deps commonDeps) *PatchCommands {
func buildPatchCommands(deps commonDeps) *PatchCommands { //nolint:golint,unused
gitCommon := buildGitCommon(deps)
rebaseCommands := buildRebaseCommands(deps)
commitCommands := buildCommitCommands(deps)
@@ -132,7 +139,7 @@ func buildPatchCommands(deps commonDeps) *PatchCommands {
return NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
}
func buildStatusCommands(deps commonDeps) *StatusCommands {
func buildStatusCommands(deps commonDeps) *StatusCommands { //nolint:golint,unused
gitCommon := buildGitCommon(deps)
return NewStatusCommands(gitCommon)
+11
View File
@@ -131,6 +131,17 @@ func (self *FileCommands) GetEditAtLineAndWaitCmdStr(filename string, lineNumber
return cmdStr
}
func (self *FileCommands) GetOpenDirInEditorCmdStr(path string) string {
template := config.GetOpenDirInEditorTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
templateValues := map[string]string{
"dir": self.cmd.Quote(path),
}
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
return cmdStr
}
func (self *FileCommands) guessDefaultEditor() string {
// Try to query a few places where editors get configured
editor := self.config.GetCoreEditor()
+24 -5
View File
@@ -2,11 +2,11 @@ package git_commands
import (
"fmt"
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type FileLoaderConfig interface {
@@ -14,15 +14,15 @@ type FileLoaderConfig interface {
}
type FileLoader struct {
*common.Common
*GitCommon
cmd oscommands.ICmdObjBuilder
config FileLoaderConfig
getFileType func(string) string
}
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader {
func NewFileLoader(gitCommon *GitCommon, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader {
return &FileLoader{
Common: cmn,
GitCommon: gitCommon,
cmd: cmd,
getFileType: oscommands.FileType,
config: config,
@@ -58,13 +58,32 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
Name: status.Name,
PreviousName: status.PreviousName,
DisplayString: status.StatusString,
Type: self.getFileType(status.Name),
}
models.SetStatusFields(file, status.Change)
files = append(files, file)
}
// Go through the files to see if any of these files are actually worktrees
// so that we can render them correctly
worktreePaths := linkedWortkreePaths(self.Fs, self.repoPaths.RepoGitDirPath())
for _, file := range files {
for _, worktreePath := range worktreePaths {
absFilePath, err := filepath.Abs(file.Name)
if err != nil {
self.Log.Error(err)
continue
}
if absFilePath == worktreePath {
file.IsWorktree = true
// `git status` renders this worktree as a folder with a trailing slash but we'll represent it as a singular worktree
// If we include the slash, it will be rendered as a folder with a null file inside.
file.Name = strings.TrimSuffix(file.Name, "/")
break
}
}
}
return files
}
+1 -11
View File
@@ -5,7 +5,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
@@ -41,7 +40,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt",
Type: "file",
ShortStatus: "MM",
},
{
@@ -54,7 +52,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "A file3.txt",
Type: "file",
ShortStatus: "A ",
},
{
@@ -67,7 +64,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt",
Type: "file",
ShortStatus: "AM",
},
{
@@ -80,7 +76,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt",
Type: "file",
ShortStatus: "??",
},
{
@@ -93,7 +88,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt",
Type: "file",
ShortStatus: "UU",
},
},
@@ -113,7 +107,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM a\nb.txt",
Type: "file",
ShortStatus: "MM",
},
},
@@ -137,7 +130,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "R before1.txt -> after1.txt",
Type: "file",
ShortStatus: "R ",
},
{
@@ -151,7 +143,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "RM before2.txt -> after2.txt",
Type: "file",
ShortStatus: "RM",
},
},
@@ -174,7 +165,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? a -> b.txt",
Type: "file",
ShortStatus: "??",
},
},
@@ -187,7 +177,7 @@ func TestFileGetStatusFiles(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
loader := &FileLoader{
Common: utils.NewDummyCommon(),
GitCommon: buildGitCommon(commonDeps{}),
cmd: cmd,
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"},
getFileType: func(string) string { return "file" },
@@ -1,6 +1,8 @@
package git_commands
import "strings"
import (
"strings"
)
// convenience struct for building git commands. Especially useful when
// including conditional args
@@ -42,9 +44,34 @@ func (self *GitCommandBuilder) Config(value string) *GitCommandBuilder {
return self
}
func (self *GitCommandBuilder) RepoPath(value string) *GitCommandBuilder {
// the -C arg will make git do a `cd` to the directory before doing anything else
func (self *GitCommandBuilder) Dir(path string) *GitCommandBuilder {
// repo path comes before the command
self.args = append([]string{"-C", value}, self.args...)
self.args = append([]string{"-C", path}, self.args...)
return self
}
// Note, you may prefer to use the Dir method instead of this one
func (self *GitCommandBuilder) Worktree(path string) *GitCommandBuilder {
// worktree arg comes before the command
self.args = append([]string{"--work-tree", path}, self.args...)
return self
}
// Note, you may prefer to use the Dir method instead of this one
func (self *GitCommandBuilder) GitDir(path string) *GitCommandBuilder {
// git dir arg comes before the command
self.args = append([]string{"--git-dir", path}, self.args...)
return self
}
func (self *GitCommandBuilder) GitDirIf(condition bool, path string) *GitCommandBuilder {
if condition {
return self.GitDir(path)
}
return self
}
@@ -45,7 +45,7 @@ func TestGitCommandBuilder(t *testing.T) {
expected: []string{"git", "-c", "user.email=bar", "-c", "user.name=foo", "push"},
},
{
input: NewGitCmd("push").RepoPath("a/b/c").ToArgv(),
input: NewGitCmd("push").Dir("a/b/c").ToArgv(),
expected: []string{"git", "-C", "a/b/c", "push"},
},
}
+8 -7
View File
@@ -1,7 +1,6 @@
package git_commands
import (
"fmt"
"path/filepath"
"time"
@@ -11,7 +10,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type PatchCommands struct {
@@ -81,7 +79,7 @@ func (self *PatchCommands) applyPatchFile(filepath string, opts ApplyPatchOpts)
}
func (self *PatchCommands) SaveTemporaryPatch(patch string) (string, error) {
filepath := filepath.Join(self.os.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
filepath := filepath.Join(self.os.GetTempDir(), self.repoPaths.RepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
self.Log.Infof("saving temporary patch to %s", filepath)
if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
return "", err
@@ -274,7 +272,12 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
return self.rebase.ContinueRebase()
}
func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx int) error {
func (self *PatchCommands) PullPatchIntoNewCommit(
commits []*models.Commit,
commitIdx int,
commitSummary string,
commitDescription string,
) error {
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx, false); err != nil {
return err
}
@@ -300,9 +303,7 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
return err
}
head_message, _ := self.commit.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
if err := self.commit.CommitCmdObj(new_message).Run(); err != nil {
if err := self.commit.CommitCmdObj(commitSummary, commitDescription).Run(); err != nil {
return err
}
-68
View File
@@ -1,68 +0,0 @@
package git_commands
import (
"fmt"
"os"
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
func TestPatchApplyPatch(t *testing.T) {
type scenario struct {
testName string
opts ApplyPatchOpts
runner *oscommands.FakeCmdObjRunner
test func(error)
}
// expectedArgs excludes the last argument which is an indeterminate filename
expectFn := func(expectedArgs []string, errToReturn error) func(cmdObj oscommands.ICmdObj) (string, error) {
return func(cmdObj oscommands.ICmdObj) (string, error) {
args := cmdObj.Args()
assert.Equal(t, len(args), len(expectedArgs)+1, fmt.Sprintf("unexpected command: %s", cmdObj.ToString()))
filename := args[len(args)-1]
content, err := os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, "test", string(content))
return "", errToReturn
}
}
scenarios := []scenario{
{
testName: "valid case",
opts: ApplyPatchOpts{Cached: true},
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn([]string{"git", "apply", "--cached"}, nil)),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "command returns error",
opts: ApplyPatchOpts{Cached: true},
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn([]string{"git", "apply", "--cached"}, errors.New("error"))),
test: func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildPatchCommands(commonDeps{runner: s.runner})
s.test(instance.ApplyPatch("test", s.opts))
s.runner.CheckForMissingCalls()
})
}
}
+60 -12
View File
@@ -7,7 +7,6 @@ import (
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/go-errors/errors"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/app/daemon"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
@@ -35,10 +34,10 @@ func NewRebaseCommands(
}
}
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, message string) error {
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, summary string, description string) error {
if models.IsHeadCommit(commits, index) {
// we've selected the top commit so no rebase is required
return self.commit.RewordLastCommit(message)
return self.commit.RewordLastCommit(summary, description)
}
err := self.BeginInteractiveRebaseForCommit(commits, index, false)
@@ -47,7 +46,7 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, me
}
// now the selected commit should be our head so we'll amend it with the new message
err = self.commit.RewordLastCommit(message)
err = self.commit.RewordLastCommit(summary, description)
if err != nil {
return err
}
@@ -105,7 +104,13 @@ func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int)
sha := commits[index].Sha
self.os.LogCommand(fmt.Sprintf("Moving TODO down: %s", utils.ShortSha(sha)), false)
msg := utils.ResolvePlaceholderString(
self.Tr.Log.MoveCommitDown,
map[string]string{
"shortSha": utils.ShortSha(sha),
},
)
self.os.LogCommand(msg, false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot,
@@ -119,7 +124,13 @@ func (self *RebaseCommands) MoveCommitUp(commits []*models.Commit, index int) er
sha := commits[index].Sha
self.os.LogCommand(fmt.Sprintf("Moving TODO up: %s", utils.ShortSha(sha)), false)
msg := utils.ResolvePlaceholderString(
self.Tr.Log.MoveCommitUp,
map[string]string{
"shortSha": utils.ShortSha(sha),
},
)
self.os.LogCommand(msg, false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot,
@@ -150,15 +161,37 @@ func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index in
}
func (self *RebaseCommands) EditRebase(branchRef string) error {
self.os.LogCommand(fmt.Sprintf("Beginning interactive rebase at '%s'", branchRef), false)
msg := utils.ResolvePlaceholderString(
self.Tr.Log.EditRebase,
map[string]string{
"ref": branchRef,
},
)
self.os.LogCommand(msg, false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: branchRef,
instruction: daemon.NewInsertBreakInstruction(),
}).Run()
}
func (self *RebaseCommands) EditRebaseFromBaseCommit(targetBranchName string, baseCommit string) error {
msg := utils.ResolvePlaceholderString(
self.Tr.Log.EditRebaseFromBaseCommit,
map[string]string{
"baseCommit": baseCommit,
"targetBranchName": targetBranchName,
},
)
self.os.LogCommand(msg, false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseCommit,
onto: targetBranchName,
instruction: daemon.NewInsertBreakInstruction(),
}).Run()
}
func logTodoChanges(changes []daemon.ChangeTodoAction) string {
changeTodoStr := strings.Join(slices.Map(changes, func(c daemon.ChangeTodoAction) string {
changeTodoStr := strings.Join(lo.Map(changes, func(c daemon.ChangeTodoAction, _ int) string {
return fmt.Sprintf("%s:%s", c.Sha, c.NewAction)
}), "\n")
return fmt.Sprintf("Changing TODO actions: %s", changeTodoStr)
@@ -166,6 +199,7 @@ func logTodoChanges(changes []daemon.ChangeTodoAction) string {
type PrepareInteractiveRebaseCommandOpts struct {
baseShaOrRoot string
onto string
instruction daemon.Instruction
overrideEditor bool
keepCommitsThatBecomeEmpty bool
@@ -184,6 +218,7 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteract
ArgIf(opts.keepCommitsThatBecomeEmpty && !self.version.IsOlderThan(2, 26, 0), "--empty=keep").
Arg("--no-autosquash").
ArgIf(!self.version.IsOlderThan(2, 22, 0), "--rebase-merges").
ArgIf(opts.onto != "", "--onto", opts.onto).
Arg(opts.baseShaOrRoot).
ToArgv()
@@ -243,18 +278,18 @@ func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) e
// EditRebaseTodo sets the action for a given rebase commit in the git-rebase-todo file
func (self *RebaseCommands) EditRebaseTodo(commit *models.Commit, action todo.TodoCommand) error {
return utils.EditRebaseTodo(
filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action, self.config.GetCoreCommentChar())
filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action, self.config.GetCoreCommentChar())
}
// MoveTodoDown moves a rebase todo item down by one position
func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo")
return utils.MoveTodoDown(fileName, commit.Sha, commit.Action, self.config.GetCoreCommentChar())
}
// MoveTodoDown moves a rebase todo item down by one position
func (self *RebaseCommands) MoveTodoUp(commit *models.Commit) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo")
return utils.MoveTodoUp(fileName, commit.Sha, commit.Action, self.config.GetCoreCommentChar())
}
@@ -307,6 +342,13 @@ func (self *RebaseCommands) RebaseBranch(branchName string) error {
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{baseShaOrRoot: branchName}).Run()
}
func (self *RebaseCommands) RebaseBranchFromBaseCommit(targetBranchName string, baseCommit string) error {
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseCommit,
onto: targetBranchName,
}).Run()
}
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
cmdArgs := NewGitCmd(commandType).Arg("--" + command).ToArgv()
@@ -395,7 +437,13 @@ func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
commitLines := lo.Map(commits, func(commit *models.Commit, _ int) string {
return fmt.Sprintf("%s %s", utils.ShortSha(commit.Sha), commit.Name)
})
self.os.LogCommand(fmt.Sprintf("Cherry-picking commits:\n%s", strings.Join(commitLines, "\n")), false)
msg := utils.ResolvePlaceholderString(
self.Tr.Log.CherryPickCommits,
map[string]string{
"commitLines": strings.Join(commitLines, "\n"),
},
)
self.os.LogCommand(msg, false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: "HEAD",
+4 -4
View File
@@ -79,7 +79,7 @@ func TestRebaseRebaseBranch(t *testing.T) {
// environment variables that suppress an interactive editor
func TestRebaseSkipEditorCommand(t *testing.T) {
cmdArgs := []string{"git", "blah"}
runner := oscommands.NewFakeRunner(t).ExpectFunc(func(cmdObj oscommands.ICmdObj) (string, error) {
runner := oscommands.NewFakeRunner(t).ExpectFunc("matches editor env var", func(cmdObj oscommands.ICmdObj) bool {
assert.EqualValues(t, cmdArgs, cmdObj.Args())
envVars := cmdObj.GetEnvVars()
for _, regexStr := range []string{
@@ -94,11 +94,11 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
return regexp.MustCompile(regexStr).MatchString(envVar)
})
if !foundMatch {
t.Errorf("expected environment variable %s to be set", regexStr)
return false
}
}
return "", nil
})
return true
}, "", nil)
instance := buildRebaseCommands(commonDeps{runner: runner})
err := instance.runSkipEditorCommand(instance.cmd.New(cmdArgs))
assert.NoError(t, err)
@@ -35,34 +35,19 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
ToArgv()
cmdObj := self.cmd.New(cmdArgs).DontLog()
onlyObtainedNewReflogCommits := false
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
fields := strings.SplitN(line, "\x00", 4)
if len(fields) <= 3 {
commit, ok := self.parseLine(line)
if !ok {
return false, nil
}
unixTimestamp, _ := strconv.Atoi(fields[1])
parentHashes := fields[3]
parents := []string{}
if len(parentHashes) > 0 {
parents = strings.Split(parentHashes, " ")
}
commit := &models.Commit{
Sha: fields[0],
Name: fields[2],
UnixTimestamp: int64(unixTimestamp),
Status: models.StatusReflog,
Parents: parents,
}
// note that the unix timestamp here is the timestamp of the COMMIT, not the reflog entry itself,
// so two consecutive reflog entries may have both the same SHA and therefore same timestamp.
// We use the reflog message to disambiguate, and fingers crossed that we never see the same of those
// twice in a row. Reason being that it would mean we'd be erroneously exiting early.
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp && commit.Name == lastReflogCommit.Name {
if lastReflogCommit != nil && self.sameReflogCommit(commit, lastReflogCommit) {
onlyObtainedNewReflogCommits = true
// after this point we already have these reflogs loaded so we'll simply return the new ones
return true, nil
@@ -77,3 +62,30 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
return commits, onlyObtainedNewReflogCommits, nil
}
func (self *ReflogCommitLoader) sameReflogCommit(a *models.Commit, b *models.Commit) bool {
return a.Sha == b.Sha && a.UnixTimestamp == b.UnixTimestamp && a.Name == b.Name
}
func (self *ReflogCommitLoader) parseLine(line string) (*models.Commit, bool) {
fields := strings.SplitN(line, "\x00", 4)
if len(fields) <= 3 {
return nil, false
}
unixTimestamp, _ := strconv.Atoi(fields[1])
parentHashes := fields[3]
parents := []string{}
if len(parentHashes) > 0 {
parents = strings.Split(parentHashes, " ")
}
return &models.Commit{
Sha: fields[0],
Name: fields[2],
UnixTimestamp: int64(unixTimestamp),
Status: models.StatusReflog,
Parents: parents,
}, true
}
+61 -19
View File
@@ -1,15 +1,16 @@
package git_commands
import (
"fmt"
"regexp"
"strings"
"sync"
"github.com/jesseduffield/generics/slices"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"golang.org/x/exp/slices"
)
type RemoteLoader struct {
@@ -31,29 +32,31 @@ func NewRemoteLoader(
}
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
cmdArgs := NewGitCmd("branch").Arg("-r").ToArgv()
remoteBranchesStr, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
wg := sync.WaitGroup{}
wg.Add(1)
var remoteBranchesByRemoteName map[string][]*models.RemoteBranch
var remoteBranchesErr error
go utils.Safe(func() {
defer wg.Done()
remoteBranchesByRemoteName, remoteBranchesErr = self.getRemoteBranchesByRemoteName()
})
goGitRemotes, err := self.getGoGitRemotes()
if err != nil {
return nil, err
}
// first step is to get our remotes from go-git
remotes := slices.Map(goGitRemotes, func(goGitRemote *gogit.Remote) *models.Remote {
remoteName := goGitRemote.Config().Name
wg.Wait()
re := regexp.MustCompile(fmt.Sprintf(`(?m)^\s*%s\/([\S]+)`, regexp.QuoteMeta(remoteName)))
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
branches := slices.Map(matches, func(match []string) *models.RemoteBranch {
return &models.RemoteBranch{
Name: match[1],
RemoteName: remoteName,
}
})
if remoteBranchesErr != nil {
return nil, remoteBranchesErr
}
remotes := lo.Map(goGitRemotes, func(goGitRemote *gogit.Remote, _ int) *models.Remote {
remoteName := goGitRemote.Config().Name
branches := remoteBranchesByRemoteName[remoteName]
return &models.Remote{
Name: goGitRemote.Config().Name,
@@ -76,3 +79,42 @@ func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
return remotes, nil
}
func (self *RemoteLoader) getRemoteBranchesByRemoteName() (map[string][]*models.RemoteBranch, error) {
remoteBranchesByRemoteName := make(map[string][]*models.RemoteBranch)
cmdArgs := NewGitCmd("branch").Arg("-r").ToArgv()
err := self.cmd.New(cmdArgs).DontLog().RunAndProcessLines(func(line string) (bool, error) {
// excluding lines like 'origin/HEAD -> origin/master' (there will be a separate
// line for 'origin/master')
if strings.Contains(line, "->") {
return false, nil
}
line = strings.TrimSpace(line)
split := strings.SplitN(line, "/", 2)
if len(split) != 2 {
return false, nil
}
remoteName := split[0]
name := split[1]
_, ok := remoteBranchesByRemoteName[remoteName]
if !ok {
remoteBranchesByRemoteName[remoteName] = []*models.RemoteBranch{}
}
remoteBranchesByRemoteName[remoteName] = append(remoteBranchesByRemoteName[remoteName],
&models.RemoteBranch{
Name: name,
RemoteName: remoteName,
})
return false, nil
})
if err != nil {
return nil, err
}
return remoteBranchesByRemoteName, nil
}
+279
View File
@@ -0,0 +1,279 @@
package git_commands
import (
"fmt"
ioFs "io/fs"
"os"
"path"
"path/filepath"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/samber/lo"
"github.com/spf13/afero"
)
type RepoPaths struct {
currentPath string
worktreePath string
worktreeGitDirPath string
repoPath string
repoGitDirPath string
repoName string
}
// Current working directory of the program. Currently, this will always
// be the same as WorktreePath(), but in future we may support running
// lazygit from inside a subdirectory of the worktree.
func (self *RepoPaths) CurrentPath() string {
return self.currentPath
}
// Path to the current worktree. If we're in the main worktree, this will
// be the same as RepoPath()
func (self *RepoPaths) WorktreePath() string {
return self.worktreePath
}
// Path of the worktree's git dir.
// If we're in the main worktree, this will be the .git dir under the RepoPath().
// If we're in a linked worktree, it will be the directory pointed at by the worktree's .git file
func (self *RepoPaths) WorktreeGitDirPath() string {
return self.worktreeGitDirPath
}
// Path of the repo. If we're in a the main worktree, this will be the same as WorktreePath()
// If we're in a bare repo, it will be the parent folder of the bare repo
func (self *RepoPaths) RepoPath() string {
return self.repoPath
}
// path of the git-dir for the repo.
// If this is a bare repo, it will be the location of the bare repo
// If this is a non-bare repo, it will be the location of the .git dir in
// the main worktree.
func (self *RepoPaths) RepoGitDirPath() string {
return self.repoGitDirPath
}
// Name of the repo. Basename of the folder containing the repo.
func (self *RepoPaths) RepoName() string {
return self.repoName
}
// Returns the repo paths for a typical repo
func MockRepoPaths(currentPath string) *RepoPaths {
return &RepoPaths{
currentPath: currentPath,
worktreePath: currentPath,
worktreeGitDirPath: path.Join(currentPath, ".git"),
repoPath: currentPath,
repoGitDirPath: path.Join(currentPath, ".git"),
repoName: "lazygit",
}
}
func GetRepoPaths(
fs afero.Fs,
currentPath string,
) (*RepoPaths, error) {
return getRepoPathsAux(afero.NewOsFs(), resolveSymlink, currentPath)
}
func getRepoPathsAux(
fs afero.Fs,
resolveSymlinkFn func(string) (string, error),
currentPath string,
) (*RepoPaths, error) {
worktreePath := currentPath
repoGitDirPath, repoPath, err := getCurrentRepoGitDirPath(fs, resolveSymlinkFn, currentPath)
if err != nil {
return nil, errors.Errorf("failed to get repo git dir path: %v", err)
}
var worktreeGitDirPath string
if env.GetWorkTreeEnv() != "" {
// This env is set when you pass --work-tree to lazygit. In that case,
// we're not dealing with a linked work-tree, we're dealing with a 'specified'
// worktree (for lack of a better term). In this case, the worktree has no
// .git file and it just contains a bunch of files: it has no idea it's
// pointed to by a bare repo. As such it does not have its own git dir within
// the bare repo's git dir. Instead, we just use the bare repo's git dir.
worktreeGitDirPath = repoGitDirPath
} else {
var err error
worktreeGitDirPath, err = getWorktreeGitDirPath(fs, currentPath)
if err != nil {
return nil, errors.Errorf("failed to get worktree git dir path: %v", err)
}
}
repoName := path.Base(repoPath)
return &RepoPaths{
currentPath: currentPath,
worktreePath: worktreePath,
worktreeGitDirPath: worktreeGitDirPath,
repoPath: repoPath,
repoGitDirPath: repoGitDirPath,
repoName: repoName,
}, nil
}
// Returns the path of the git-dir for the worktree. For linked worktrees, the worktree has
// a .git file that points to the git-dir (which itself lives in the git-dir
// of the repo)
func getWorktreeGitDirPath(fs afero.Fs, worktreePath string) (string, error) {
// if .git is a file, we're in a linked worktree, otherwise we're in
// the main worktree
dotGitPath := path.Join(worktreePath, ".git")
gitFileInfo, err := fs.Stat(dotGitPath)
if err != nil {
return "", err
}
if gitFileInfo.IsDir() {
return dotGitPath, nil
}
return linkedWorktreeGitDirPath(fs, worktreePath)
}
func linkedWorktreeGitDirPath(fs afero.Fs, worktreePath string) (string, error) {
dotGitPath := path.Join(worktreePath, ".git")
gitFileContents, err := afero.ReadFile(fs, dotGitPath)
if err != nil {
return "", err
}
// The file will have `gitdir: /path/to/.git/worktrees/<worktree-name>`
gitDirLine := lo.Filter(strings.Split(string(gitFileContents), "\n"), func(line string, _ int) bool {
return strings.HasPrefix(line, "gitdir: ")
})
if len(gitDirLine) == 0 {
return "", errors.New(fmt.Sprintf("%s is a file which suggests we are in a submodule or a worktree but the file's contents do not contain a gitdir pointing to the actual .git directory", dotGitPath))
}
gitDir := strings.TrimPrefix(gitDirLine[0], "gitdir: ")
// For windows support
gitDir = filepath.ToSlash(gitDir)
return gitDir, nil
}
func getCurrentRepoGitDirPath(
fs afero.Fs,
resolveSymlinkFn func(string) (string, error),
currentPath string,
) (string, string, error) {
var unresolvedGitPath string
if env.GetGitDirEnv() != "" {
unresolvedGitPath = env.GetGitDirEnv()
} else {
unresolvedGitPath = path.Join(currentPath, ".git")
}
gitPath, err := resolveSymlinkFn(unresolvedGitPath)
if err != nil {
return "", "", err
}
// check if .git is a file or a directory
gitFileInfo, err := fs.Stat(gitPath)
if err != nil {
return "", "", err
}
if gitFileInfo.IsDir() {
// must be in the main worktree
return gitPath, path.Dir(gitPath), nil
}
// either in a submodule, or worktree
worktreeGitPath, err := linkedWorktreeGitDirPath(fs, currentPath)
if err != nil {
return "", "", errors.Errorf("could not find git dir for %s: %v", currentPath, err)
}
_, err = fs.Stat(worktreeGitPath)
if err != nil {
if os.IsNotExist(err) {
// hardcoding error to get around windows-specific error message
return "", "", errors.Errorf("could not find git dir for %s. %s does not exist", currentPath, worktreeGitPath)
}
return "", "", errors.Errorf("could not find git dir for %s: %v", currentPath, err)
}
// confirm whether the next directory up is the worktrees directory
parent := path.Dir(worktreeGitPath)
if path.Base(parent) == "worktrees" {
gitDirPath := path.Dir(parent)
return gitDirPath, path.Dir(gitDirPath), nil
}
// Unlike worktrees, submodules can be nested arbitrarily deep, so we check
// if the `modules` directory is anywhere up the chain.
if strings.Contains(worktreeGitPath, "/modules/") {
// For submodules, we just return the path directly
return worktreeGitPath, currentPath, nil
}
// If this error causes issues, we could relax the constraint and just always
// return the path
return "", "", errors.Errorf("could not find git dir for %s: path is not under `worktrees` or `modules` directories", currentPath)
}
// takes a path containing a symlink and returns the true path
func resolveSymlink(path string) (string, error) {
l, err := os.Lstat(path)
if err != nil {
return "", err
}
if l.Mode()&os.ModeSymlink == 0 {
return path, nil
}
return filepath.EvalSymlinks(path)
}
// Returns the paths of linked worktrees
func linkedWortkreePaths(fs afero.Fs, repoGitDirPath string) []string {
result := []string{}
// For each directory in this path we're going to cat the `gitdir` file and append its contents to our result
// That file points us to the `.git` file in the worktree.
worktreeGitDirsPath := path.Join(repoGitDirPath, "worktrees")
// ensure the directory exists
_, err := fs.Stat(worktreeGitDirsPath)
if err != nil {
return result
}
_ = afero.Walk(fs, worktreeGitDirsPath, func(currPath string, info ioFs.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
return nil
}
gitDirPath := path.Join(currPath, "gitdir")
gitDirBytes, err := afero.ReadFile(fs, gitDirPath)
if err != nil {
// ignoring error
return nil
}
trimmedGitDir := strings.TrimSpace(string(gitDirBytes))
// removing the .git part
worktreeDir := path.Dir(trimmedGitDir)
result = append(result, worktreeDir)
return nil
})
return result
}
@@ -0,0 +1,145 @@
package git_commands
import (
"testing"
"github.com/go-errors/errors"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)
func mockResolveSymlinkFn(p string) (string, error) { return p, nil }
type Scenario struct {
Name string
BeforeFunc func(fs afero.Fs)
Path string
Expected *RepoPaths
Err error
}
func TestGetRepoPathsAux(t *testing.T) {
scenarios := []Scenario{
{
Name: "typical case",
BeforeFunc: func(fs afero.Fs) {
// setup for main worktree
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
},
Path: "/path/to/repo",
Expected: &RepoPaths{
currentPath: "/path/to/repo",
worktreePath: "/path/to/repo",
worktreeGitDirPath: "/path/to/repo/.git",
repoPath: "/path/to/repo",
repoGitDirPath: "/path/to/repo/.git",
repoName: "repo",
},
Err: nil,
},
{
Name: "linked worktree",
BeforeFunc: func(fs afero.Fs) {
// setup for linked worktree
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/worktree1", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo/worktree1/.git", []byte("gitdir: /path/to/repo/.git/worktrees/worktree1"), 0o644)
},
Path: "/path/to/repo/worktree1",
Expected: &RepoPaths{
currentPath: "/path/to/repo/worktree1",
worktreePath: "/path/to/repo/worktree1",
worktreeGitDirPath: "/path/to/repo/.git/worktrees/worktree1",
repoPath: "/path/to/repo",
repoGitDirPath: "/path/to/repo/.git",
repoName: "repo",
},
Err: nil,
},
{
Name: "worktree .git file missing gitdir directive",
BeforeFunc: func(fs afero.Fs) {
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/worktree2", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo/worktree2/.git", []byte("blah"), 0o644)
},
Path: "/path/to/repo/worktree2",
Expected: nil,
Err: errors.New("failed to get repo git dir path: could not find git dir for /path/to/repo/worktree2: /path/to/repo/worktree2/.git is a file which suggests we are in a submodule or a worktree but the file's contents do not contain a gitdir pointing to the actual .git directory"),
},
{
Name: "worktree .git file gitdir directive points to a non-existing directory",
BeforeFunc: func(fs afero.Fs) {
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/worktree2", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo/worktree2/.git", []byte("gitdir: /nonexistant"), 0o644)
},
Path: "/path/to/repo/worktree2",
Expected: nil,
Err: errors.New("failed to get repo git dir path: could not find git dir for /path/to/repo/worktree2. /nonexistant does not exist"),
},
{
Name: "submodule",
BeforeFunc: func(fs afero.Fs) {
_ = fs.MkdirAll("/path/to/repo/.git/modules/submodule1", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo/submodule1/.git", []byte("gitdir: /path/to/repo/.git/modules/submodule1"), 0o644)
},
Path: "/path/to/repo/submodule1",
Expected: &RepoPaths{
currentPath: "/path/to/repo/submodule1",
worktreePath: "/path/to/repo/submodule1",
worktreeGitDirPath: "/path/to/repo/.git/modules/submodule1",
repoPath: "/path/to/repo/submodule1",
repoGitDirPath: "/path/to/repo/.git/modules/submodule1",
repoName: "submodule1",
},
Err: nil,
},
{
Name: "submodule in nested directory",
BeforeFunc: func(fs afero.Fs) {
_ = fs.MkdirAll("/path/to/repo/.git/modules/my/submodule1", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo/my/submodule1/.git", []byte("gitdir: /path/to/repo/.git/modules/my/submodule1"), 0o644)
},
Path: "/path/to/repo/my/submodule1",
Expected: &RepoPaths{
currentPath: "/path/to/repo/my/submodule1",
worktreePath: "/path/to/repo/my/submodule1",
worktreeGitDirPath: "/path/to/repo/.git/modules/my/submodule1",
repoPath: "/path/to/repo/my/submodule1",
repoGitDirPath: "/path/to/repo/.git/modules/my/submodule1",
repoName: "submodule1",
},
Err: nil,
},
{
Name: "submodule git dir not under .git/modules",
BeforeFunc: func(fs afero.Fs) {
_ = fs.MkdirAll("/random/submodule1", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo/my/submodule1/.git", []byte("gitdir: /random/submodule1"), 0o644)
},
Path: "/path/to/repo/my/submodule1",
Expected: nil,
Err: errors.New("failed to get repo git dir path: could not find git dir for /path/to/repo/my/submodule1: path is not under `worktrees` or `modules` directories"),
},
}
for _, s := range scenarios {
s := s
t.Run(s.Name, func(t *testing.T) {
fs := afero.NewMemMapFs()
// prepare the filesystem for the scenario
s.BeforeFunc(fs)
// run the function with the scenario path
repoPaths, err := getRepoPathsAux(fs, mockResolveSymlinkFn, s.Path)
// check the error and the paths
if s.Err != nil {
assert.Error(t, err)
assert.EqualError(t, err, s.Err.Error())
} else {
assert.Nil(t, err)
assert.Equal(t, s.Expected, repoPaths)
}
})
}
}
+2 -2
View File
@@ -5,11 +5,11 @@ import (
"strconv"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type StashLoader struct {
@@ -69,7 +69,7 @@ func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
cmdArgs := NewGitCmd("stash").Arg("list", "-z", "--pretty=%gs").ToArgv()
rawString, _ := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
return slices.MapWithIndex(utils.SplitNul(rawString), func(line string, index int) *models.StashEntry {
return lo.Map(utils.SplitNul(rawString), func(line string, index int) *models.StashEntry {
return self.stashEntryFromLine(line, index)
})
}
+15 -3
View File
@@ -1,6 +1,7 @@
package git_commands
import (
"os"
"path/filepath"
"strconv"
"strings"
@@ -24,14 +25,14 @@ func NewStatusCommands(
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (self *StatusCommands) RebaseMode() (enums.RebaseMode, error) {
exists, err := self.os.FileExists(filepath.Join(self.dotGitDir, "rebase-apply"))
exists, err := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply"))
if err != nil {
return enums.REBASE_MODE_NONE, err
}
if exists {
return enums.REBASE_MODE_NORMAL, nil
}
exists, err = self.os.FileExists(filepath.Join(self.dotGitDir, "rebase-merge"))
exists, err = self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge"))
if exists {
return enums.REBASE_MODE_INTERACTIVE, err
} else {
@@ -69,5 +70,16 @@ func IsBareRepo(osCommand *oscommands.OSCommand) (bool, error) {
// IsInMergeState states whether we are still mid-merge
func (self *StatusCommands) IsInMergeState() (bool, error) {
return self.os.FileExists(filepath.Join(self.dotGitDir, "MERGE_HEAD"))
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "MERGE_HEAD"))
}
// Full ref (e.g. "refs/heads/mybranch") of the branch that is currently
// being rebased, or empty string when we're not in a rebase
func (self *StatusCommands) BranchBeingRebased() string {
for _, dir := range []string{"rebase-merge", "rebase-apply"} {
if bytesContent, err := os.ReadFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), dir, "head-name")); err == nil {
return strings.TrimSpace(string(bytesContent))
}
}
return ""
}
+4 -2
View File
@@ -82,7 +82,7 @@ func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
}
cmdArgs := NewGitCmd("stash").
RepoPath(submodule.Path).
Dir(submodule.Path).
Arg("--include-untracked").
ToArgv()
@@ -139,7 +139,9 @@ func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
self.Log.Error(err)
}
return os.RemoveAll(filepath.Join(self.dotGitDir, "modules", submodule.Path))
// We may in fact want to use the repo's git dir path but git docs say not to
// mix submodules and worktrees anyway.
return os.RemoveAll(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "modules", submodule.Path))
}
func (self *SubmoduleCommands) Add(name string, path string, url string) error {
+8 -1
View File
@@ -82,6 +82,7 @@ type PullOptions struct {
RemoteName string
BranchName string
FastForwardOnly bool
WorktreeGitDir string
}
func (self *SyncCommands) Pull(task gocui.Task, opts PullOptions) error {
@@ -90,6 +91,7 @@ func (self *SyncCommands) Pull(task gocui.Task, opts PullOptions) error {
ArgIf(opts.FastForwardOnly, "--ff-only").
ArgIf(opts.RemoteName != "", opts.RemoteName).
ArgIf(opts.BranchName != "", opts.BranchName).
GitDirIf(opts.WorktreeGitDir != "", opts.WorktreeGitDir).
ToArgv()
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
@@ -97,7 +99,12 @@ func (self *SyncCommands) Pull(task gocui.Task, opts PullOptions) error {
return self.cmd.New(cmdArgs).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run()
}
func (self *SyncCommands) FastForward(task gocui.Task, branchName string, remoteName string, remoteBranchName string) error {
func (self *SyncCommands) FastForward(
task gocui.Task,
branchName string,
remoteName string,
remoteBranchName string,
) error {
cmdArgs := NewGitCmd("fetch").
Arg(remoteName).
Arg(remoteBranchName + ":" + branchName).
+15 -3
View File
@@ -12,16 +12,19 @@ func NewTagCommands(gitCommon *GitCommon) *TagCommands {
}
}
func (self *TagCommands) CreateLightweight(tagName string, ref string) error {
cmdArgs := NewGitCmd("tag").Arg("--", tagName).
func (self *TagCommands) CreateLightweight(tagName string, ref string, force bool) error {
cmdArgs := NewGitCmd("tag").
ArgIf(force, "--force").
Arg("--", tagName).
ArgIf(len(ref) > 0, ref).
ToArgv()
return self.cmd.New(cmdArgs).Run()
}
func (self *TagCommands) CreateAnnotated(tagName, ref, msg string) error {
func (self *TagCommands) CreateAnnotated(tagName, ref, msg string, force bool) error {
cmdArgs := NewGitCmd("tag").Arg(tagName).
ArgIf(force, "--force").
ArgIf(len(ref) > 0, ref).
Arg("-m", msg).
ToArgv()
@@ -29,6 +32,15 @@ func (self *TagCommands) CreateAnnotated(tagName, ref, msg string) error {
return self.cmd.New(cmdArgs).Run()
}
func (self *TagCommands) HasTag(tagName string) bool {
cmdArgs := NewGitCmd("show-ref").
Arg("--tags", "--quiet", "--verify", "--").
Arg("refs/tags/" + tagName).
ToArgv()
return self.cmd.New(cmdArgs).Run() == nil
}
func (self *TagCommands) Delete(tagName string) error {
cmdArgs := NewGitCmd("tag").Arg("-d", tagName).
ToArgv()
+2 -2
View File
@@ -3,11 +3,11 @@ package git_commands
import (
"regexp"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type TagLoader struct {
@@ -38,7 +38,7 @@ func (self *TagLoader) GetTags() ([]*models.Tag, error) {
lineRegex := regexp.MustCompile(`^([^\s]+)(\s+)?(.*)$`)
tags := slices.Map(split, func(line string) *models.Tag {
tags := lo.Map(split, func(line string, _ int) *models.Tag {
matches := lineRegex.FindStringSubmatch(line)
tagName := matches[1]
message := ""
+41 -29
View File
@@ -1,11 +1,7 @@
package git_commands
import (
"errors"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
@@ -20,10 +16,30 @@ func NewWorktreeCommands(gitCommon *GitCommon) *WorktreeCommands {
}
}
func (self *WorktreeCommands) New(worktreePath string, committish string) error {
cmdArgs := NewGitCmd("worktree").Arg("add", worktreePath, committish).ToArgv()
type NewWorktreeOpts struct {
// required. The path of the new worktree.
Path string
// required. The base branch/ref.
Base string
return self.cmd.New(cmdArgs).Run()
// if true, ends up with a detached head
Detach bool
// optional. if empty, and if detach is false, we will checkout the base
Branch string
}
func (self *WorktreeCommands) New(opts NewWorktreeOpts) error {
if opts.Detach && opts.Branch != "" {
panic("cannot specify branch when detaching")
}
cmdArgs := NewGitCmd("worktree").Arg("add").
ArgIf(opts.Detach, "--detach").
ArgIf(opts.Branch != "", "-b", opts.Branch).
Arg(opts.Path, opts.Base)
return self.cmd.New(cmdArgs.ToArgv()).Run()
}
func (self *WorktreeCommands) Delete(worktreePath string, force bool) error {
@@ -32,31 +48,27 @@ func (self *WorktreeCommands) Delete(worktreePath string, force bool) error {
return self.cmd.New(cmdArgs).Run()
}
func (self *WorktreeCommands) IsCurrentWorktree(w *models.Worktree) bool {
return IsCurrentWorktree(w)
func (self *WorktreeCommands) Detach(worktreePath string) error {
cmdArgs := NewGitCmd("checkout").Arg("--detach").GitDir(filepath.Join(worktreePath, ".git")).ToArgv()
return self.cmd.New(cmdArgs).Run()
}
func IsCurrentWorktree(w *models.Worktree) bool {
pwd, err := os.Getwd()
if err != nil {
log.Fatalln(err.Error())
}
return EqualPath(pwd, w.Path)
}
func (self *WorktreeCommands) IsWorktreePathMissing(w *models.Worktree) bool {
if _, err := os.Stat(w.Path); err != nil {
if errors.Is(err, fs.ErrNotExist) {
return true
func WorktreeForBranch(branch *models.Branch, worktrees []*models.Worktree) (*models.Worktree, bool) {
for _, worktree := range worktrees {
if worktree.Branch == branch.Name {
return worktree, true
}
log.Fatalln(fmt.Errorf("failed to check if worktree path `%s` exists\n%w", w.Path, err).Error())
}
return false
return nil, false
}
// checks if two paths are equal
// TODO: support relative paths
func EqualPath(a string, b string) bool {
return a == b
func CheckedOutByOtherWorktree(branch *models.Branch, worktrees []*models.Worktree) bool {
worktree, ok := WorktreeForBranch(branch, worktrees)
if !ok {
return false
}
return !worktree.IsCurrent
}
+102 -24
View File
@@ -1,52 +1,74 @@
package git_commands
import (
"os"
iofs "io/fs"
"path/filepath"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/spf13/afero"
)
type WorktreeLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
*GitCommon
}
func NewWorktreeLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *WorktreeLoader {
return &WorktreeLoader{
Common: common,
cmd: cmd,
}
func NewWorktreeLoader(gitCommon *GitCommon) *WorktreeLoader {
return &WorktreeLoader{GitCommon: gitCommon}
}
func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
cmdArgs := NewGitCmd("worktree").Arg("list", "--porcelain", "-z").ToArgv()
currentRepoPath := self.repoPaths.RepoPath()
worktreePath := self.repoPaths.WorktreePath()
cmdArgs := NewGitCmd("worktree").Arg("list", "--porcelain").ToArgv()
worktreesOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
splitLines := strings.Split(worktreesOutput, "\x00")
splitLines := strings.Split(
utils.NormalizeLinefeeds(worktreesOutput), "\n",
)
var worktrees []*models.Worktree
var current *models.Worktree
for _, splitLine := range splitLines {
// worktrees are defined over multiple lines and are separated by blank lines
// so if we reach a blank line we're done with the current worktree
if len(splitLine) == 0 && current != nil {
worktrees = append(worktrees, current)
current = nil
continue
}
// ignore bare repo (not sure why it's even appearing in this list: it's not a worktree)
if splitLine == "bare" {
current = nil
continue
}
if strings.HasPrefix(splitLine, "worktree ") {
path := strings.SplitN(splitLine, " ", 2)[1]
isMain := path == currentRepoPath
isCurrent := path == worktreePath
isPathMissing := self.pathExists(path)
var gitDir string
gitDir, err := getWorktreeGitDirPath(self.Fs, path)
if err != nil {
self.Log.Warnf("Could not find git dir for worktree %s: %v", path, err)
}
current = &models.Worktree{
IsMain: len(worktrees) == 0,
Path: path,
IsMain: isMain,
IsCurrent: isCurrent,
IsPathMissing: isPathMissing,
Path: path,
GitDir: gitDir,
}
} else if strings.HasPrefix(splitLine, "branch ") {
branch := strings.SplitN(splitLine, " ", 2)[1]
@@ -59,26 +81,82 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
}))
for index, worktree := range worktrees {
worktree.NameField = names[index]
}
pwd, err := os.Getwd()
if err != nil {
return nil, err
worktree.Name = names[index]
}
// move current worktree to the top
for i, worktree := range worktrees {
if EqualPath(worktree.Path, pwd) {
if worktree.IsCurrent {
worktrees = append(worktrees[:i], worktrees[i+1:]...)
worktrees = append([]*models.Worktree{worktree}, worktrees...)
break
}
}
// Some worktrees are on a branch but are mid-rebase, and in those cases,
// `git worktree list` will not show the branch name. We can get the branch
// name from the `rebase-merge/head-name` file (if it exists) in the folder
// for the worktree in the parent repo's .git/worktrees folder.
for _, worktree := range worktrees {
// No point checking if we already have a branch name
if worktree.Branch != "" {
continue
}
// If we couldn't find the git directory, we can't find the branch name
if worktree.GitDir == "" {
continue
}
rebasedBranch, ok := self.rebasedBranch(worktree)
if ok {
worktree.Branch = rebasedBranch
continue
}
bisectedBranch, ok := self.bisectedBranch(worktree)
if ok {
worktree.Branch = bisectedBranch
continue
}
}
return worktrees, nil
}
func (self *WorktreeLoader) pathExists(path string) bool {
if _, err := self.Fs.Stat(path); err != nil {
if errors.Is(err, iofs.ErrNotExist) {
return true
}
self.Log.Errorf("failed to check if worktree path `%s` exists\n%v", path, err)
return false
}
return false
}
func (self *WorktreeLoader) rebasedBranch(worktree *models.Worktree) (string, bool) {
for _, dir := range []string{"rebase-merge", "rebase-apply"} {
if bytesContent, err := afero.ReadFile(self.Fs, filepath.Join(worktree.GitDir, dir, "head-name")); err == nil {
headName := strings.TrimSpace(string(bytesContent))
shortHeadName := strings.TrimPrefix(headName, "refs/heads/")
return shortHeadName, true
}
}
return "", false
}
func (self *WorktreeLoader) bisectedBranch(worktree *models.Worktree) (string, bool) {
bisectStartPath := filepath.Join(worktree.GitDir, "BISECT_START")
startContent, err := afero.ReadFile(self.Fs, bisectStartPath)
if err != nil {
return "", false
}
return strings.TrimSpace(string(startContent)), true
}
type pathWithIndexT struct {
path string
index int
@@ -3,9 +3,195 @@ package git_commands
import (
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)
func TestGetWorktrees(t *testing.T) {
type scenario struct {
testName string
repoPaths *RepoPaths
before func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs)
expectedWorktrees []*models.Worktree
expectedErr string
}
scenarios := []scenario{
{
testName: "Single worktree (main)",
repoPaths: &RepoPaths{
repoPath: "/path/to/repo",
worktreePath: "/path/to/repo",
},
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) {
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
`worktree /path/to/repo
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
branch refs/heads/mybranch
`,
nil)
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
},
expectedWorktrees: []*models.Worktree{
{
IsMain: true,
IsCurrent: true,
Path: "/path/to/repo",
IsPathMissing: false,
GitDir: "/path/to/repo/.git",
Branch: "mybranch",
Name: "repo",
},
},
expectedErr: "",
},
{
testName: "Multiple worktrees (main + linked)",
repoPaths: &RepoPaths{
repoPath: "/path/to/repo",
worktreePath: "/path/to/repo",
},
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) {
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
`worktree /path/to/repo
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
branch refs/heads/mybranch
worktree /path/to/repo-worktree
HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de
branch refs/heads/mybranch-worktree
`,
nil)
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
_ = fs.MkdirAll("/path/to/repo-worktree", 0o755)
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/repo-worktree", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo-worktree/.git", []byte("gitdir: /path/to/repo/.git/worktrees/repo-worktree"), 0o755)
},
expectedWorktrees: []*models.Worktree{
{
IsMain: true,
IsCurrent: true,
Path: "/path/to/repo",
IsPathMissing: false,
GitDir: "/path/to/repo/.git",
Branch: "mybranch",
Name: "repo",
},
{
IsMain: false,
IsCurrent: false,
Path: "/path/to/repo-worktree",
IsPathMissing: false,
GitDir: "/path/to/repo/.git/worktrees/repo-worktree",
Branch: "mybranch-worktree",
Name: "repo-worktree",
},
},
expectedErr: "",
},
{
testName: "Worktree missing path",
repoPaths: &RepoPaths{
repoPath: "/path/to/repo",
worktreePath: "/path/to/repo",
},
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) {
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
`worktree /path/to/worktree
HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de
branch refs/heads/missingbranch
`,
nil)
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
},
expectedWorktrees: []*models.Worktree{
{
IsMain: false,
IsCurrent: false,
Path: "/path/to/worktree",
IsPathMissing: true,
GitDir: "",
Branch: "missingbranch",
Name: "worktree",
},
},
expectedErr: "",
},
{
testName: "In linked worktree",
repoPaths: &RepoPaths{
repoPath: "/path/to/repo",
worktreePath: "/path/to/repo-worktree",
},
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs) {
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
`worktree /path/to/repo
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
branch refs/heads/mybranch
worktree /path/to/repo-worktree
HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de
branch refs/heads/mybranch-worktree
`,
nil)
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
_ = fs.MkdirAll("/path/to/repo-worktree", 0o755)
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/repo-worktree", 0o755)
_ = afero.WriteFile(fs, "/path/to/repo-worktree/.git", []byte("gitdir: /path/to/repo/.git/worktrees/repo-worktree"), 0o755)
},
expectedWorktrees: []*models.Worktree{
{
IsMain: false,
IsCurrent: true,
Path: "/path/to/repo-worktree",
IsPathMissing: false,
GitDir: "/path/to/repo/.git/worktrees/repo-worktree",
Branch: "mybranch-worktree",
Name: "repo-worktree",
},
{
IsMain: true,
IsCurrent: false,
Path: "/path/to/repo",
IsPathMissing: false,
GitDir: "/path/to/repo/.git",
Branch: "mybranch",
Name: "repo",
},
},
expectedErr: "",
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
runner := oscommands.NewFakeRunner(t)
fs := afero.NewMemMapFs()
s.before(runner, fs)
loader := &WorktreeLoader{
GitCommon: buildGitCommon(commonDeps{runner: runner, fs: fs, repoPaths: s.repoPaths}),
}
worktrees, err := loader.GetWorktrees()
if s.expectedErr != "" {
assert.EqualError(t, errors.New(s.expectedErr), err.Error())
} else {
assert.NoError(t, err)
assert.EqualValues(t, worktrees, s.expectedWorktrees)
}
})
}
}
func TestGetUniqueNamesFromPaths(t *testing.T) {
for _, scenario := range []struct {
input []string
+2 -4
View File
@@ -7,8 +7,6 @@ import (
"os/exec"
"strings"
"syscall"
"github.com/jesseduffield/lazygit/pkg/secureexec"
)
// including license from https://github.com/tcnksm/go-gitconfig because this file is an adaptation of that repo's code
@@ -55,10 +53,10 @@ func runGitConfigCmd(cmd *exec.Cmd) (string, error) {
func getGitConfigCmd(key string) *exec.Cmd {
gitArgs := []string{"config", "--get", "--null", key}
return secureexec.Command("git", gitArgs...)
return exec.Command("git", gitArgs...)
}
func getGitConfigGeneralCmd(args string) *exec.Cmd {
gitArgs := append([]string{"config"}, strings.Split(args, " ")...)
return secureexec.Command("git", gitArgs...)
return exec.Command("git", gitArgs...)
}
+42 -273
View File
@@ -1,305 +1,74 @@
package commands
import (
"fmt"
"os"
"testing"
"time"
"github.com/go-errors/errors"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)
type fileInfoMock struct {
name string
size int64
fileMode os.FileMode
fileModTime time.Time
isDir bool
sys interface{}
}
// Name is a function.
func (f fileInfoMock) Name() string {
return f.name
}
// Size is a function.
func (f fileInfoMock) Size() int64 {
return f.size
}
// Mode is a function.
func (f fileInfoMock) Mode() os.FileMode {
return f.fileMode
}
// ModTime is a function.
func (f fileInfoMock) ModTime() time.Time {
return f.fileModTime
}
// IsDir is a function.
func (f fileInfoMock) IsDir() bool {
return f.isDir
}
// Sys is a function.
func (f fileInfoMock) Sys() interface{} {
return f.sys
}
// TestNavigateToRepoRootDirectory is a function.
func TestNavigateToRepoRootDirectory(t *testing.T) {
func TestFindWorktreeRoot(t *testing.T) {
type scenario struct {
testName string
stat func(string) (os.FileInfo, error)
chdir func(string) error
test func(error)
testName string
currentPath string
before func(fs afero.Fs)
expectedPath string
expectedErr string
}
scenarios := []scenario{
{
"Navigate to git repository",
func(string) (os.FileInfo, error) {
return fileInfoMock{isDir: true}, nil
},
func(string) error {
return nil
},
func(err error) {
assert.NoError(t, err)
testName: "at root of worktree",
currentPath: "/path/to/repo",
before: func(fs afero.Fs) {
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
},
expectedPath: "/path/to/repo",
expectedErr: "",
},
{
"An error occurred when getting path information",
func(string) (os.FileInfo, error) {
return nil, fmt.Errorf("An error occurred")
},
func(string) error {
return nil
},
func(err error) {
assert.Error(t, err)
assert.EqualError(t, err, "An error occurred")
testName: "inside worktree",
currentPath: "/path/to/repo/subdir",
before: func(fs afero.Fs) {
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
_ = fs.MkdirAll("/path/to/repo/subdir", 0o755)
},
expectedPath: "/path/to/repo",
expectedErr: "",
},
{
"An error occurred when trying to move one path backward",
func(string) (os.FileInfo, error) {
return nil, os.ErrNotExist
},
func(string) error {
return fmt.Errorf("An error occurred")
},
func(err error) {
assert.Error(t, err)
assert.EqualError(t, err, "An error occurred")
testName: "not in a git repo",
currentPath: "/path/to/dir",
before: func(fs afero.Fs) {},
expectedPath: "",
expectedErr: "Must open lazygit in a git repository",
},
{
testName: "In linked worktree",
currentPath: "/path/to/worktree",
before: func(fs afero.Fs) {
_ = fs.MkdirAll("/path/to/worktree", 0o755)
_ = afero.WriteFile(fs, "/path/to/worktree/.git", []byte("blah"), 0o755)
},
expectedPath: "/path/to/worktree",
expectedErr: "",
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.test(navigateToRepoRootDirectory(s.stat, s.chdir))
})
}
}
fs := afero.NewMemMapFs()
s.before(fs)
// TestSetupRepository is a function.
func TestSetupRepository(t *testing.T) {
type scenario struct {
testName string
openGitRepository func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error)
errorStr string
options gogit.PlainOpenOptions
test func(*gogit.Repository, error)
}
scenarios := []scenario{
{
"A gitconfig parsing error occurred",
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
return nil, fmt.Errorf(`unquoted '\' must be followed by new line`)
},
"error translated",
gogit.PlainOpenOptions{},
func(r *gogit.Repository, err error) {
assert.Error(t, err)
assert.EqualError(t, err, "error translated")
},
},
{
"A gogit error occurred",
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
return nil, fmt.Errorf("Error from inside gogit")
},
"",
gogit.PlainOpenOptions{},
func(r *gogit.Repository, err error) {
assert.Error(t, err)
assert.EqualError(t, err, "Error from inside gogit")
},
},
{
"Setup done properly",
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
assert.NoError(t, os.RemoveAll("/tmp/lazygit-test"))
r, err := gogit.PlainInit("/tmp/lazygit-test", false)
assert.NoError(t, err)
return r, nil
},
"",
gogit.PlainOpenOptions{},
func(r *gogit.Repository, err error) {
assert.NoError(t, err)
assert.NotNil(t, r)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.test(setupRepository(s.openGitRepository, s.options, s.errorStr))
})
}
}
// TestNewGitCommand is a function.
func TestNewGitCommand(t *testing.T) {
actual, err := os.Getwd()
assert.NoError(t, err)
defer func() {
assert.NoError(t, os.Chdir(actual))
}()
type scenario struct {
testName string
setup func()
test func(*GitCommand, error)
}
scenarios := []scenario{
{
"An error occurred, folder doesn't contains a git repository",
func() {
assert.NoError(t, os.Chdir("/tmp"))
},
func(gitCmd *GitCommand, err error) {
assert.Error(t, err)
assert.Regexp(t, `Must open lazygit in a git repository`, err.Error())
},
},
{
"New GitCommand object created",
func() {
assert.NoError(t, os.RemoveAll("/tmp/lazygit-test"))
_, err := gogit.PlainInit("/tmp/lazygit-test", false)
root, err := findWorktreeRoot(fs, s.currentPath)
if s.expectedErr != "" {
assert.EqualError(t, errors.New(s.expectedErr), err.Error())
} else {
assert.NoError(t, err)
assert.NoError(t, os.Chdir("/tmp/lazygit-test"))
},
func(gitCmd *GitCommand, err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.setup()
s.test(
NewGitCommand(utils.NewDummyCommon(),
&git_commands.GitVersion{},
oscommands.NewDummyOSCommand(),
git_config.NewFakeGitConfig(nil),
&deadlock.Mutex{},
))
})
}
}
func TestFindDotGitDir(t *testing.T) {
type scenario struct {
testName string
stat func(string) (os.FileInfo, error)
readFile func(filename string) ([]byte, error)
test func(string, error)
}
scenarios := []scenario{
{
".git is a directory",
func(dotGit string) (os.FileInfo, error) {
assert.Equal(t, ".git", dotGit)
return os.Stat("testdata/a_dir")
},
func(dotGit string) ([]byte, error) {
assert.Fail(t, "readFile should not be called if .git is a directory")
return nil, nil
},
func(gitDir string, err error) {
assert.NoError(t, err)
assert.Equal(t, ".git", gitDir)
},
},
{
".git is a file",
func(dotGit string) (os.FileInfo, error) {
assert.Equal(t, ".git", dotGit)
return os.Stat("testdata/a_file")
},
func(dotGit string) ([]byte, error) {
assert.Equal(t, ".git", dotGit)
return []byte("gitdir: blah\n"), nil
},
func(gitDir string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah", gitDir)
},
},
{
"os.Stat returns an error",
func(dotGit string) (os.FileInfo, error) {
assert.Equal(t, ".git", dotGit)
return nil, errors.New("error")
},
func(dotGit string) ([]byte, error) {
assert.Fail(t, "readFile should not be called os.Stat returns an error")
return nil, nil
},
func(gitDir string, err error) {
assert.Error(t, err)
},
},
{
"readFile returns an error",
func(dotGit string) (os.FileInfo, error) {
assert.Equal(t, ".git", dotGit)
return os.Stat("testdata/a_file")
},
func(dotGit string) ([]byte, error) {
return nil, errors.New("error")
},
func(gitDir string, err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.test(findDotGitDir(s.stat, s.readFile))
assert.Equal(t, s.expectedPath, root)
}
})
}
}
@@ -8,9 +8,10 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
"github.com/jesseduffield/generics/slices"
"golang.org/x/exp/slices"
)
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, gitea, etc.
@@ -111,7 +112,7 @@ func (self *HostingServiceMgr) getCandidateServiceDomains() []ServiceDomain {
serviceDefinition, ok := serviceDefinitionByProvider[provider]
if !ok {
providerNames := slices.Map(serviceDefinitions, func(serviceDefinition ServiceDefinition) string {
providerNames := lo.Map(serviceDefinitions, func(serviceDefinition ServiceDefinition, _ int) string {
return serviceDefinition.provider
})
+13
View File
@@ -0,0 +1,13 @@
package models
import "fmt"
// A commit author
type Author struct {
Name string
Email string
}
func (self *Author) Combined() string {
return fmt.Sprintf("%s <%s>", self.Name, self.Email)
}
-2
View File
@@ -26,8 +26,6 @@ type Branch struct {
Subject string
// commit hash
CommitHash string
CheckedOutByOtherWorktree bool
}
func (b *Branch) FullRefName() string {
+3 -1
View File
@@ -18,8 +18,10 @@ type File struct {
HasMergeConflicts bool
HasInlineMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
// If true, this must be a worktree folder
IsWorktree bool
}
// sometimes we need to deal with either a node (which contains a file) or an actual file
+18 -12
View File
@@ -4,14 +4,28 @@ package models
type Worktree struct {
// if false, this is a linked worktree
IsMain bool
Path string
// if true, this is the worktree that is currently checked out
IsCurrent bool
// path to the directory of the worktree i.e. the directory that contains all the user's files
Path string
// if true, the path is not found
IsPathMissing bool
// path of the git directory for this worktree. The equivalent of the .git directory
// in the main worktree. For linked worktrees this would be <repo_path>/.git/worktrees/<name>
GitDir string
// If the worktree has a branch checked out, this field will be set to the branch name.
// A branch is considered 'checked out' if:
// * the worktree is directly on the branch
// * the worktree is mid-rebase on the branch
// * the worktree is mid-bisect on the branch
Branch string
// based on the path, but uniquified
NameField string
// based on the path, but uniquified. Not the same name that git uses in the worktrees/ folder (no good reason for this,
// I just prefer my naming convention better)
Name string
}
func (w *Worktree) RefName() string {
return w.Name()
return w.Name
}
func (w *Worktree) ID() string {
@@ -21,11 +35,3 @@ func (w *Worktree) ID() string {
func (w *Worktree) Description() string {
return w.RefName()
}
func (w *Worktree) Name() string {
return w.NameField
}
func (w *Worktree) Main() bool {
return w.IsMain
}
+11 -6
View File
@@ -24,6 +24,9 @@ type ICmdObj interface {
AddEnvVars(...string) ICmdObj
GetEnvVars() []string
// sets the working directory
SetWd(string) ICmdObj
// runs the command and returns an error if any
Run() error
// runs the command and returns the output as a string, and an error if any
@@ -70,10 +73,6 @@ type ICmdObj interface {
}
type CmdObj struct {
// the secureexec package will swap out the first arg with the full path to the binary,
// so we store these args separately so that ToString() will output the original
args []string
cmd *exec.Cmd
runner ICmdObjRunner
@@ -118,7 +117,7 @@ func (self *CmdObj) GetCmd() *exec.Cmd {
func (self *CmdObj) ToString() string {
// if a given arg contains a space, we need to wrap it in quotes
quotedArgs := lo.Map(self.args, func(arg string, _ int) string {
quotedArgs := lo.Map(self.cmd.Args, func(arg string, _ int) string {
if strings.Contains(arg, " ") {
return `"` + arg + `"`
}
@@ -129,7 +128,7 @@ func (self *CmdObj) ToString() string {
}
func (self *CmdObj) Args() []string {
return self.args
return self.cmd.Args
}
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
@@ -142,6 +141,12 @@ func (self *CmdObj) GetEnvVars() []string {
return self.cmd.Env
}
func (self *CmdObj) SetWd(wd string) ICmdObj {
self.cmd.Dir = wd
return self
}
func (self *CmdObj) DontLog() ICmdObj {
self.dontLog = true
return self
+2 -3
View File
@@ -3,9 +3,9 @@ package oscommands
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/mgutz/str"
)
@@ -27,11 +27,10 @@ type CmdObjBuilder struct {
var _ ICmdObjBuilder = &CmdObjBuilder{}
func (self *CmdObjBuilder) New(args []string) ICmdObj {
cmd := secureexec.Command(args[0], args[1:]...)
cmd := exec.Command(args[0], args[1:]...)
cmd.Env = os.Environ()
return &CmdObj{
args: args,
cmd: cmd,
runner: self.runner,
}
+25 -3
View File
@@ -6,6 +6,7 @@ import (
"io"
"regexp"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
@@ -102,10 +103,14 @@ func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
self.logCmdObj(cmdObj)
}
t := time.Now()
output, err := sanitisedCommandOutput(cmdObj.GetCmd().CombinedOutput())
if err != nil {
self.log.WithField("command", cmdObj.ToString()).Error(output)
}
self.log.Infof("%s (%s)", cmdObj.ToString(), time.Since(t))
return output, err
}
@@ -116,12 +121,15 @@ func (self *cmdObjRunner) RunWithOutputsAux(cmdObj ICmdObj) (string, string, err
self.logCmdObj(cmdObj)
}
t := time.Now()
var outBuffer, errBuffer bytes.Buffer
cmd := cmdObj.GetCmd()
cmd.Stdout = &outBuffer
cmd.Stderr = &errBuffer
err := cmd.Run()
self.log.Infof("%s (%s)", cmdObj.ToString(), time.Since(t))
stdout := outBuffer.String()
stderr, err := sanitisedCommandOutput(errBuffer.Bytes(), err)
if err != nil {
@@ -144,6 +152,7 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
t := time.Now()
cmd := cmdObj.GetCmd()
stdoutPipe, err := cmd.StdoutPipe()
@@ -171,6 +180,8 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
_ = cmd.Wait()
self.log.Infof("%s (%s)", cmdObj.ToString(), time.Since(t))
return nil
}
@@ -237,9 +248,14 @@ func (self *cmdObjRunner) runAndStreamAux(
}
}()
t := time.Now()
onRun(handler, cmdWriter)
err = cmd.Wait()
self.log.Infof("%s (%s)", cmdObj.ToString(), time.Since(t))
if err != nil {
errStr := stderr.String()
if errStr != "" {
@@ -312,7 +328,9 @@ func (self *cmdObjRunner) runAndDetectCredentialRequest(
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
self.processOutput(tr, handler.stdinPipe, promptUserForCredential, cmdObj.GetTask())
go utils.Safe(func() {
self.processOutput(tr, handler.stdinPipe, promptUserForCredential, cmdObj.GetTask())
})
})
}
@@ -331,9 +349,13 @@ func (self *cmdObjRunner) processOutput(
askFor, ok := checkForCredentialRequest(newBytes)
if ok {
responseChan := promptUserForCredential(askFor)
task.Pause()
if task != nil {
task.Pause()
}
toInput := <-responseChan
task.Continue()
if task != nil {
task.Continue()
}
// If the return data is empty we don't write anything to stdin
if toInput != "" {
_, _ = writer.Write([]byte(toInput))
+2 -1
View File
@@ -27,7 +27,8 @@ func TestCmdObjToString(t *testing.T) {
}
for _, scenario := range scenarios {
cmdObj := &CmdObj{args: scenario.cmdArgs}
cmd := exec.Command(scenario.cmdArgs[0], scenario.cmdArgs[1:]...)
cmdObj := &CmdObj{cmd: cmd}
actual := cmdObj.ToString()
if actual != scenario.expected {
t.Errorf("Expected %s, got %s", quote(scenario.expected), quote(actual))
+8 -3
View File
@@ -3,7 +3,6 @@ package oscommands
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
)
@@ -106,7 +105,7 @@ func CopyDir(src string, dst string) (err error) {
return //nolint: nakedret
}
entries, err := ioutil.ReadDir(src)
entries, err := os.ReadDir(src)
if err != nil {
return //nolint: nakedret
}
@@ -121,8 +120,14 @@ func CopyDir(src string, dst string) (err error) {
return //nolint: nakedret
}
} else {
var info os.FileInfo
info, err = entry.Info()
if err != nil {
return //nolint: nakedret
}
// Skip symlinks.
if entry.Mode()&os.ModeSymlink != 0 {
if info.Mode()&os.ModeSymlink != 0 {
continue
}
+76 -53
View File
@@ -3,21 +3,37 @@ package oscommands
import (
"bufio"
"fmt"
"regexp"
"runtime"
"strings"
"sync"
"testing"
"github.com/go-errors/errors"
"github.com/stretchr/testify/assert"
"github.com/samber/lo"
"golang.org/x/exp/slices"
)
// for use in testing
type FakeCmdObjRunner struct {
t *testing.T
expectedCmds []func(ICmdObj) (string, error)
expectedCmdIndex int
t *testing.T
// commands can be run in any order; mimicking the concurrent behaviour of
// production code.
expectedCmds []CmdObjMatcher
invokedCmdIndexes []int
mutex sync.Mutex
}
type CmdObjMatcher struct {
description string
// returns true if the matcher matches the command object
test func(ICmdObj) bool
// output of the command
output string
// error of the command
err error
}
var _ ICmdObjRunner = &FakeCmdObjRunner{}
@@ -26,23 +42,40 @@ func NewFakeRunner(t *testing.T) *FakeCmdObjRunner { //nolint:thelper
return &FakeCmdObjRunner{t: t}
}
func (self *FakeCmdObjRunner) remainingExpectedCmds() []CmdObjMatcher {
return lo.Filter(self.expectedCmds, func(_ CmdObjMatcher, i int) bool {
return !lo.Contains(self.invokedCmdIndexes, i)
})
}
func (self *FakeCmdObjRunner) Run(cmdObj ICmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if self.expectedCmdIndex > len(self.expectedCmds)-1 {
self.mutex.Lock()
defer self.mutex.Unlock()
if len(self.remainingExpectedCmds()) == 0 {
self.t.Errorf("ran too many commands. Unexpected command: `%s`", cmdObj.ToString())
return "", errors.New("ran too many commands")
}
expectedCmd := self.expectedCmds[self.expectedCmdIndex]
output, err := expectedCmd(cmdObj)
for i := range self.expectedCmds {
if lo.Contains(self.invokedCmdIndexes, i) {
continue
}
expectedCmd := self.expectedCmds[i]
matched := expectedCmd.test(cmdObj)
if matched {
self.invokedCmdIndexes = append(self.invokedCmdIndexes, i)
return expectedCmd.output, expectedCmd.err
}
}
self.expectedCmdIndex++
return output, err
self.t.Errorf("Unexpected command: `%s`", cmdObj.ToString())
return "", nil
}
func (self *FakeCmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error) {
@@ -72,63 +105,53 @@ func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(lin
return nil
}
func (self *FakeCmdObjRunner) ExpectFunc(fn func(cmdObj ICmdObj) (string, error)) *FakeCmdObjRunner {
self.expectedCmds = append(self.expectedCmds, fn)
func (self *FakeCmdObjRunner) ExpectFunc(description string, fn func(cmdObj ICmdObj) bool, output string, err error) *FakeCmdObjRunner {
self.mutex.Lock()
defer self.mutex.Unlock()
return self
}
func (self *FakeCmdObjRunner) Expect(expectedCmdStr string, output string, err error) *FakeCmdObjRunner {
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
cmdStr := cmdObj.ToString()
assert.Equal(self.t, expectedCmdStr, cmdStr, fmt.Sprintf("expected command %d to be %s, but was %s", self.expectedCmdIndex+1, expectedCmdStr, cmdStr))
return output, err
self.expectedCmds = append(self.expectedCmds, CmdObjMatcher{
test: fn,
output: output,
err: err,
description: description,
})
return self
}
func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
args := cmdObj.GetCmd().Args
if runtime.GOOS == "windows" {
// thanks to the secureexec package, the first arg is something like
// '"C:\\Program Files\\Git\\mingw64\\bin\\<command>.exe"
// on windows so we'll just ensure it contains our program
assert.Contains(self.t, args[0], expectedArgs[0])
} else {
// first arg is the program name
assert.Equal(self.t, expectedArgs[0], args[0])
}
assert.EqualValues(self.t, expectedArgs[1:], args[1:], fmt.Sprintf("command %d did not match expectation", self.expectedCmdIndex+1))
return output, err
})
description := fmt.Sprintf("matches args %s", strings.Join(expectedArgs, " "))
self.ExpectFunc(description, func(cmdObj ICmdObj) bool {
return slices.Equal(expectedArgs, cmdObj.GetCmd().Args)
}, output, err)
return self
}
func (self *FakeCmdObjRunner) ExpectGitArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
// first arg is 'git' on unix and something like '"C:\\Program Files\\Git\\mingw64\\bin\\git.exe" on windows so we'll just ensure it ends in either 'git' or 'git.exe'
re := regexp.MustCompile(`git(\.exe)?$`)
args := cmdObj.GetCmd().Args
if !re.MatchString(args[0]) {
self.t.Errorf("expected first arg to end in .git or .git.exe but was %s", args[0])
}
assert.EqualValues(self.t, expectedArgs, args[1:], fmt.Sprintf("command %d did not match expectation", self.expectedCmdIndex+1))
return output, err
})
description := fmt.Sprintf("matches git args %s", strings.Join(expectedArgs, " "))
self.ExpectFunc(description, func(cmdObj ICmdObj) bool {
return slices.Equal(expectedArgs, cmdObj.GetCmd().Args[1:])
}, output, err)
return self
}
func (self *FakeCmdObjRunner) CheckForMissingCalls() {
if self.expectedCmdIndex < len(self.expectedCmds) {
self.t.Errorf("expected command %d to be called, but was not", self.expectedCmdIndex+1)
self.mutex.Lock()
defer self.mutex.Unlock()
remaining := self.remainingExpectedCmds()
if len(remaining) > 0 {
self.t.Errorf(
"expected %d more command(s) to be run. Remaining commands:\n%s",
len(remaining),
strings.Join(
lo.Map(remaining, func(cmdObj CmdObjMatcher, _ int) string {
return cmdObj.description
}),
"\n",
),
)
}
}
+46 -7
View File
@@ -13,7 +13,6 @@ import (
"github.com/samber/lo"
"github.com/atotto/clipboard"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/kill"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
@@ -118,7 +117,15 @@ func (c *OSCommand) Quote(message string) string {
// AppendLineToFile adds a new line in file
func (c *OSCommand) AppendLineToFile(filename, line string) error {
c.LogCommand(fmt.Sprintf("Appending '%s' to file '%s'", line, filename), false)
msg := utils.ResolvePlaceholderString(
c.Tr.Log.AppendingLineToFile,
map[string]string{
"line": line,
"filename": filename,
},
)
c.LogCommand(msg, false)
f, err := os.OpenFile(filename, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0o600)
if err != nil {
return utils.WrapError(err)
@@ -155,7 +162,13 @@ func (c *OSCommand) AppendLineToFile(filename, line string) error {
// CreateFileWithContent creates a file with the given content
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
c.LogCommand(fmt.Sprintf("Creating file '%s'", path), false)
msg := utils.ResolvePlaceholderString(
c.Tr.Log.CreateFileWithContent,
map[string]string{
"path": path,
},
)
c.LogCommand(msg, false)
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
c.Log.Error(err)
return err
@@ -171,7 +184,13 @@ func (c *OSCommand) CreateFileWithContent(path string, content string) error {
// Remove removes a file or directory at the specified path
func (c *OSCommand) Remove(filename string) error {
c.LogCommand(fmt.Sprintf("Removing '%s'", filename), false)
msg := utils.ResolvePlaceholderString(
c.Tr.Log.Remove,
map[string]string{
"filename": filename,
},
)
c.LogCommand(msg, false)
err := os.RemoveAll(filename)
return utils.WrapError(err)
}
@@ -189,7 +208,7 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
func (c *OSCommand) PipeCommands(cmdObjs ...ICmdObj) error {
cmds := slices.Map(cmdObjs, func(cmdObj ICmdObj) *exec.Cmd {
cmds := lo.Map(cmdObjs, func(cmdObj ICmdObj, _ int) *exec.Cmd {
return cmdObj.GetCmd()
})
@@ -266,12 +285,32 @@ func PrepareForChildren(cmd *exec.Cmd) {
func (c *OSCommand) CopyToClipboard(str string) error {
escaped := strings.Replace(str, "\n", "\\n", -1)
truncated := utils.TruncateWithEllipsis(escaped, 40)
c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", truncated), false)
msg := utils.ResolvePlaceholderString(
c.Tr.Log.CopyToClipboard,
map[string]string{
"str": truncated,
},
)
c.LogCommand(msg, false)
if c.UserConfig.OS.CopyToClipboardCmd != "" {
cmdStr := utils.ResolvePlaceholderString(c.UserConfig.OS.CopyToClipboardCmd, map[string]string{
"text": c.Cmd.Quote(str),
})
return c.Cmd.NewShell(cmdStr).Run()
}
return clipboard.WriteAll(str)
}
func (c *OSCommand) RemoveFile(path string) error {
c.LogCommand(fmt.Sprintf("Deleting path '%s'", path), false)
msg := utils.ResolvePlaceholderString(
c.Tr.Log.RemoveFile,
map[string]string{
"path": path,
},
)
c.LogCommand(msg, false)
return c.removeFileFn(path)
}
+5 -8
View File
@@ -6,7 +6,6 @@ package oscommands
import (
"testing"
"github.com/cli/safeexec"
"github.com/go-errors/errors"
"github.com/stretchr/testify/assert"
)
@@ -20,13 +19,11 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
test func(error)
}
fullCmdPath, _ := safeexec.LookPath("cmd")
scenarios := []scenario{
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "test"}, "", errors.New("error")),
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
@@ -34,7 +31,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "test"}, "", nil),
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -42,7 +39,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "filename with spaces"}, "", nil),
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -50,7 +47,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "let's_test_with_single_quote",
runner: NewFakeRunner(t).
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
ExpectArgs([]string{"cmd", "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -58,7 +55,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "$USER.txt",
runner: NewFakeRunner(t).
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "$USER.txt"}, "", nil),
ExpectArgs([]string{"cmd", "/c", "start", "", "$USER.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
+2 -3
View File
@@ -5,7 +5,6 @@ import (
"strings"
"github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/generics/slices"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
@@ -209,10 +208,10 @@ func (p *PatchBuilder) renderEachFilePatch(plain bool) []string {
filenames := maps.Keys(p.fileInfoMap)
sort.Strings(filenames)
patches := slices.Map(filenames, func(filename string) string {
patches := lo.Map(filenames, func(filename string, _ int) string {
return p.RenderPatchForFile(filename, plain, false)
})
output := slices.Filter(patches, func(patch string) bool {
output := lo.Filter(patches, func(patch string, _ int) bool {
return patch != ""
})
+4
View File
@@ -4,6 +4,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
"github.com/spf13/afero"
)
// Commonly used things wrapped into one struct for convenience when passing it around
@@ -12,4 +13,7 @@ type Common struct {
Tr *i18n.TranslationSet
UserConfig *config.UserConfig
Debug bool
// for interacting with the filesystem. We use afero rather than the default
// `os` package for the sake of mocking the filesystem in tests
Fs afero.Fs
}
+4 -3
View File
@@ -8,7 +8,7 @@ import (
"github.com/OpenPeeDeeP/xdg"
"github.com/jesseduffield/lazygit/pkg/utils/yaml_utils"
"gopkg.in/yaml.v3"
yaml "github.com/jesseduffield/yaml"
)
// AppConfig contains the base configuration fields required for lazygit.
@@ -318,8 +318,9 @@ type AppState struct {
StartupPopupVersion int
// these are for custom commands typed in directly, not for custom commands in the lazygit config
CustomCommandsHistory []string
HideCommandLog bool
CustomCommandsHistory []string
HideCommandLog bool
IgnoreWhitespaceInDiffView bool
}
func getDefaultAppState() *AppState {
+2 -3
View File
@@ -1,18 +1,17 @@
package config
import (
"io/ioutil"
"os"
"strings"
)
func isWSL() bool {
data, err := ioutil.ReadFile("/proc/sys/kernel/osrelease")
data, err := os.ReadFile("/proc/sys/kernel/osrelease")
return err == nil && strings.Contains(string(data), "microsoft")
}
func isContainer() bool {
data, err := ioutil.ReadFile("/proc/1/cgroup")
data, err := os.ReadFile("/proc/1/cgroup")
if strings.Contains(string(data), "docker") ||
strings.Contains(string(data), "/lxc/") ||
+1 -1
View File
@@ -1,7 +1,7 @@
package config
import (
"gopkg.in/yaml.v3"
yaml "github.com/jesseduffield/yaml"
)
// NewDummyAppConfig creates a new dummy AppConfig for testing
+16
View File
@@ -28,10 +28,20 @@ func GetEditAtLineAndWaitTemplate(osConfig *OSConfig, guessDefaultEditor func()
return template
}
func GetOpenDirInEditorTemplate(osConfig *OSConfig, guessDefaultEditor func() string) string {
preset := getPreset(osConfig, guessDefaultEditor)
template := osConfig.OpenDirInEditor
if template == "" {
template = preset.openDirInEditorTemplate
}
return template
}
type editPreset struct {
editTemplate string
editAtLineTemplate string
editAtLineAndWaitTemplate string
openDirInEditorTemplate string
editInTerminal bool
}
@@ -48,30 +58,35 @@ func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset
editTemplate: "hx -- {{filename}}",
editAtLineTemplate: "hx -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "hx -- {{filename}}:{{line}}",
openDirInEditorTemplate: "hx -- {{dir}}",
editInTerminal: 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}}",
editInTerminal: false,
},
"sublime": {
editTemplate: "subl -- {{filename}}",
editAtLineTemplate: "subl -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "subl --wait -- {{filename}}:{{line}}",
openDirInEditorTemplate: "subl -- {{dir}}",
editInTerminal: false,
},
"bbedit": {
editTemplate: "bbedit -- {{filename}}",
editAtLineTemplate: "bbedit +{{line}} -- {{filename}}",
editAtLineAndWaitTemplate: "bbedit +{{line}} --wait -- {{filename}}",
openDirInEditorTemplate: "bbedit -- {{dir}}",
editInTerminal: false,
},
"xcode": {
editTemplate: "xed -- {{filename}}",
editAtLineTemplate: "xed --line {{line}} -- {{filename}}",
editAtLineAndWaitTemplate: "xed --line {{line}} --wait -- {{filename}}",
openDirInEditorTemplate: "xed -- {{dir}}",
editInTerminal: false,
},
}
@@ -107,6 +122,7 @@ func standardTerminalEditorPreset(editor string) *editPreset {
editTemplate: editor + " -- {{filename}}",
editAtLineTemplate: editor + " +{{line}} -- {{filename}}",
editAtLineAndWaitTemplate: editor + " +{{line}} -- {{filename}}",
openDirInEditorTemplate: editor + " -- {{dir}}",
editInTerminal: true,
}
}
+65 -45
View File
@@ -27,36 +27,36 @@ type RefresherConfig struct {
}
type GuiConfig struct {
AuthorColors map[string]string `yaml:"authorColors"`
BranchColors map[string]string `yaml:"branchColors"`
ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"`
MouseEvents bool `yaml:"mouseEvents"`
SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"`
SidePanelWidth float64 `yaml:"sidePanelWidth"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Language string `yaml:"language"`
TimeFormat string `yaml:"timeFormat"`
ShortTimeFormat string `yaml:"shortTimeFormat"`
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
ShowListFooter bool `yaml:"showListFooter"`
ShowFileTree bool `yaml:"showFileTree"`
ShowRandomTip bool `yaml:"showRandomTip"`
ShowCommandLog bool `yaml:"showCommandLog"`
ShowBottomLine bool `yaml:"showBottomLine"`
ShowIcons bool `yaml:"showIcons"`
NerdFontsVersion string `yaml:"nerdFontsVersion"`
ShowBranchCommitHash bool `yaml:"showBranchCommitHash"`
ExperimentalShowBranchHeads bool `yaml:"experimentalShowBranchHeads"`
CommandLogSize int `yaml:"commandLogSize"`
SplitDiff string `yaml:"splitDiff"`
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
WindowSize string `yaml:"windowSize"`
Border string `yaml:"border"`
AuthorColors map[string]string `yaml:"authorColors"`
BranchColors map[string]string `yaml:"branchColors"`
ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"`
MouseEvents bool `yaml:"mouseEvents"`
SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"`
SidePanelWidth float64 `yaml:"sidePanelWidth"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Language string `yaml:"language"`
TimeFormat string `yaml:"timeFormat"`
ShortTimeFormat string `yaml:"shortTimeFormat"`
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
ShowListFooter bool `yaml:"showListFooter"`
ShowFileTree bool `yaml:"showFileTree"`
ShowRandomTip bool `yaml:"showRandomTip"`
ShowCommandLog bool `yaml:"showCommandLog"`
ShowBottomLine bool `yaml:"showBottomLine"`
ShowIcons bool `yaml:"showIcons"`
NerdFontsVersion string `yaml:"nerdFontsVersion"`
ShowBranchCommitHash bool `yaml:"showBranchCommitHash"`
CommandLogSize int `yaml:"commandLogSize"`
SplitDiff string `yaml:"splitDiff"`
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
WindowSize string `yaml:"windowSize"`
Border string `yaml:"border"`
AnimateExplosion bool `yaml:"animateExplosion"`
}
type ThemeConfig struct {
@@ -68,6 +68,8 @@ type ThemeConfig struct {
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
CherryPickedCommitBgColor []string `yaml:"cherryPickedCommitBgColor"`
CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor"`
MarkedBaseCommitBgColor []string `yaml:"markedBaseCommitBgColor"`
MarkedBaseCommitFgColor []string `yaml:"markedBaseCommitFgColor"`
UnstagedChangesColor []string `yaml:"unstagedChangesColor"`
DefaultFgColor []string `yaml:"defaultFgColor"`
}
@@ -132,6 +134,7 @@ type KeybindingConfig struct {
Status KeybindingStatusConfig `yaml:"status"`
Files KeybindingFilesConfig `yaml:"files"`
Branches KeybindingBranchesConfig `yaml:"branches"`
Worktrees KeybindingWorktreesConfig `yaml:"worktrees"`
Commits KeybindingCommitsConfig `yaml:"commits"`
Stash KeybindingStashConfig `yaml:"stash"`
CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"`
@@ -246,6 +249,10 @@ type KeybindingBranchesConfig struct {
FetchRemote string `yaml:"fetchRemote"`
}
type KeybindingWorktreesConfig struct {
ViewWorktreeOptions string `yaml:"viewWorktreeOptions"`
}
type KeybindingCommitsConfig struct {
SquashDown string `yaml:"squashDown"`
RenameCommit string `yaml:"renameCommit"`
@@ -263,6 +270,7 @@ type KeybindingCommitsConfig struct {
CherryPickCopy string `yaml:"cherryPickCopy"`
CherryPickCopyRange string `yaml:"cherryPickCopyRange"`
PasteCommits string `yaml:"pasteCommits"`
MarkCommitAsBaseForRebase string `yaml:"markCommitAsBaseForRebase"`
CreateTag string `yaml:"tagCommit"`
CheckoutCommit string `yaml:"checkoutCommit"`
ResetCherryPick string `yaml:"resetCherryPick"`
@@ -313,6 +321,9 @@ type OSConfig struct {
// Pointer to bool so that we can distinguish unset (nil) from false.
EditInTerminal *bool `yaml:"editInTerminal,omitempty"`
// For opening a directory in an editor
OpenDirInEditor string `yaml:"openDirInEditor,omitempty"`
// A built-in preset that sets all of the above settings. Supported presets
// are defined in the getPreset function in editor_presets.go.
EditPreset string `yaml:"editPreset,omitempty"`
@@ -346,6 +357,9 @@ type OSConfig struct {
// OpenLinkCommand is the command for opening a link
// Deprecated: use OpenLink instead.
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
// CopyToClipboardCmd is the command for copying to clipboard
CopyToClipboardCmd string `yaml:"copyToClipboardCmd,omitempty"`
}
type CustomCommandAfterHook struct {
@@ -422,24 +436,26 @@ func GetDefaultConfig() *UserConfig {
SelectedRangeBgColor: []string{"blue"},
CherryPickedCommitBgColor: []string{"cyan"},
CherryPickedCommitFgColor: []string{"blue"},
MarkedBaseCommitBgColor: []string{"yellow"},
MarkedBaseCommitFgColor: []string{"blue"},
UnstagedChangesColor: []string{"red"},
DefaultFgColor: []string{"default"},
},
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,
ShowListFooter: true,
ShowCommandLog: true,
ShowBottomLine: true,
ShowFileTree: true,
ShowRandomTip: true,
ShowIcons: false,
NerdFontsVersion: "",
ExperimentalShowBranchHeads: false,
ShowBranchCommitHash: false,
CommandLogSize: 8,
SplitDiff: "auto",
SkipRewordInEditorWarning: false,
Border: "single",
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,
ShowListFooter: true,
ShowCommandLog: true,
ShowBottomLine: true,
ShowFileTree: true,
ShowRandomTip: true,
ShowIcons: false,
NerdFontsVersion: "",
ShowBranchCommitHash: false,
CommandLogSize: 8,
SplitDiff: "auto",
SkipRewordInEditorWarning: false,
Border: "single",
AnimateExplosion: true,
},
Git: GitConfig{
Paging: PagingConfig{
@@ -584,6 +600,9 @@ func GetDefaultConfig() *UserConfig {
SetUpstream: "u",
FetchRemote: "f",
},
Worktrees: KeybindingWorktreesConfig{
ViewWorktreeOptions: "w",
},
Commits: KeybindingCommitsConfig{
SquashDown: "s",
RenameCommit: "r",
@@ -601,6 +620,7 @@ func GetDefaultConfig() *UserConfig {
CherryPickCopy: "c",
CherryPickCopyRange: "C",
PasteCommits: "v",
MarkCommitAsBaseForRebase: "B",
CreateTag: "T",
CheckoutCommit: "<space>",
ResetCherryPick: "<c-R>",
+6 -6
View File
@@ -10,19 +10,19 @@ func GetGitDirEnv() string {
return os.Getenv("GIT_DIR")
}
func GetGitWorkTreeEnv() string {
return os.Getenv("GIT_WORK_TREE")
}
func SetGitDirEnv(value string) {
os.Setenv("GIT_DIR", value)
}
func SetGitWorkTreeEnv(value string) {
func GetWorkTreeEnv() string {
return os.Getenv("GIT_WORK_TREE")
}
func SetWorkTreeEnv(value string) {
os.Setenv("GIT_WORK_TREE", value)
}
func UnsetGitDirEnvs() {
func UnsetGitLocationEnvVars() {
_ = os.Unsetenv("GIT_DIR")
_ = os.Unsetenv("GIT_WORK_TREE")
}
+3 -3
View File
@@ -4,9 +4,9 @@ import (
"errors"
"sync"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
@@ -134,7 +134,7 @@ func (self *ContextMgr) pushToContextStack(c types.Context) ([]types.Context, ty
(topContext.GetKind() == types.MAIN_CONTEXT && c.GetKind() == types.MAIN_CONTEXT) {
contextsToDeactivate = append(contextsToDeactivate, topContext)
_, self.ContextStack = slices.Pop(self.ContextStack)
_, self.ContextStack = utils.Pop(self.ContextStack)
}
self.ContextStack = append(self.ContextStack, c)
@@ -154,7 +154,7 @@ func (self *ContextMgr) Pop() error {
}
var currentContext types.Context
currentContext, self.ContextStack = slices.Pop(self.ContextStack)
currentContext, self.ContextStack = utils.Pop(self.ContextStack)
newContext := self.ContextStack[len(self.ContextStack)-1]
+5
View File
@@ -31,6 +31,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
c.Modes().Diffing.Ref,
c.Tr,
c.UserConfig,
c.Model().Worktrees,
)
}
@@ -82,3 +83,7 @@ func (self *BranchesContext) GetDiffTerminals() []string {
}
return nil
}
func (self *BranchesContext) ShowBranchHeadsInSubCommits() bool {
return true
}
+2 -2
View File
@@ -1,12 +1,12 @@
package context
import (
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
type CommitFilesContext struct {
@@ -34,7 +34,7 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
}
lines := presentation.RenderCommitFileTree(viewModel, c.Modes().Diffing.Ref, c.Git().Patch.PatchBuilder)
return slices.Map(lines, func(line string) []string {
return lo.Map(lines, func(line string, _ int) []string {
return []string{line}
})
}
+12 -5
View File
@@ -31,7 +31,7 @@ type CommitMessageViewModel struct {
// the full preserved message (combined summary and description)
preservedMessage string
// invoked when pressing enter in the commit message panel
onConfirm func(string) error
onConfirm func(string, string) error
// The message typed in before cycling through history
// We store this separately to 'preservedMessage' because 'preservedMessage'
@@ -88,15 +88,22 @@ func (self *CommitMessageContext) SetHistoryMessage(message string) {
self.viewModel.historyMessage = message
}
func (self *CommitMessageContext) OnConfirm(message string) error {
return self.viewModel.onConfirm(message)
func (self *CommitMessageContext) OnConfirm(summary string, description string) error {
return self.viewModel.onConfirm(summary, description)
}
func (self *CommitMessageContext) SetPanelState(index int, title string, preserveMessage bool, onConfirm func(string) error) {
func (self *CommitMessageContext) SetPanelState(
index int,
summaryTitle string,
descriptionTitle string,
preserveMessage bool,
onConfirm func(string, string) error,
) {
self.viewModel.selectedindex = index
self.viewModel.preserveMessage = preserveMessage
self.viewModel.onConfirm = onConfirm
self.GetView().Title = title
self.GetView().Title = summaryTitle
self.c.Views().CommitDescription.Title = descriptionTitle
}
func (self *CommitMessageContext) RenderCommitLength() {
+1 -1
View File
@@ -124,10 +124,10 @@ func (self *ContextTree) Flatten() []types.Context {
self.Status,
self.Snake,
self.Submodules,
self.Worktrees,
self.Files,
self.SubCommits,
self.Remotes,
self.Worktrees,
self.RemoteBranches,
self.Tags,
self.Branches,
+27 -12
View File
@@ -1,7 +1,11 @@
package context
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sahilm/fuzzy"
"github.com/samber/lo"
"github.com/sasha-s/go-deadlock"
)
@@ -53,6 +57,21 @@ func (self *FilteredList[T]) UnfilteredLen() int {
return len(self.getList())
}
type fuzzySource[T any] struct {
list []T
getFilterFields func(T) []string
}
var _ fuzzy.Source = &fuzzySource[string]{}
func (self *fuzzySource[T]) String(i int) string {
return strings.Join(self.getFilterFields(self.list[i]), " ")
}
func (self *fuzzySource[T]) Len() int {
return len(self.list)
}
func (self *FilteredList[T]) applyFilter() {
self.mutex.Lock()
defer self.mutex.Unlock()
@@ -60,20 +79,16 @@ func (self *FilteredList[T]) applyFilter() {
if self.filter == "" {
self.filteredIndices = nil
} else {
self.filteredIndices = []int{}
for i, item := range self.getList() {
for _, field := range self.getFilterFields(item) {
if self.match(field, self.filter) {
self.filteredIndices = append(self.filteredIndices, i)
break
}
}
source := &fuzzySource[T]{
list: self.getList(),
getFilterFields: self.getFilterFields,
}
}
}
func (self *FilteredList[T]) match(haystack string, needle string) bool {
return utils.CaseAwareContains(haystack, needle)
matches := fuzzy.FindFrom(self.filter, source)
self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int {
return match.Index
})
}
}
func (self *FilteredList[T]) UnfilteredIndex(index int) int {
+14 -3
View File
@@ -14,7 +14,7 @@ type ListContextTrait struct {
list types.IList
getDisplayStrings func(startIdx int, length int) [][]string
// Alignment for each column. If nil, the default is left alignment
columnAlignments []utils.Alignment
getColumnAlignments func() []utils.Alignment
// Some contexts, like the commit context, will highlight the path from the selected commit
// to its parents, because it's ambiguous otherwise. For these, we need to refresh the viewport
// so that we show the highlighted path.
@@ -31,7 +31,14 @@ func (self *ListContextTrait) GetList() types.IList {
}
func (self *ListContextTrait) FocusLine() {
self.GetViewTrait().FocusPoint(self.list.GetSelectedLineIdx())
// Doing this at the end of the layout function because we need the view to be
// resized before we focus the line, otherwise if we're in accordion mode
// the view could be squashed and won't how to adjust the cursor/origin
self.c.AfterLayout(func() error {
self.GetViewTrait().FocusPoint(self.list.GetSelectedLineIdx())
return nil
})
self.setFooter()
if self.refreshViewportOnChange {
@@ -75,9 +82,13 @@ func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) error
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
func (self *ListContextTrait) HandleRender() error {
self.list.RefreshSelectedIdx()
var columnAlignments []utils.Alignment
if self.getColumnAlignments != nil {
columnAlignments = self.getColumnAlignments()
}
content := utils.RenderDisplayStrings(
self.getDisplayStrings(0, self.list.Len()),
self.columnAlignments,
columnAlignments,
)
self.GetViewTrait().SetContent(content)
self.c.Render()
+5
View File
@@ -38,13 +38,18 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
}
showYouAreHereLabel := c.Model().WorkingTreeStateAtLastCommitRefresh == enums.REBASE_MODE_REBASING
showBranchMarkerForHeadCommit := c.Git().Config.GetRebaseUpdateRefs()
return presentation.GetCommitListDisplayStrings(
c.Common,
c.Model().Commits,
c.Model().Branches,
c.Model().CheckedOutBranch,
showBranchMarkerForHeadCommit,
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().CherryPicking.SelectedShaSet(),
c.Modes().Diffing.Ref,
c.Modes().MarkedBaseCommit.GetSha(),
c.UserConfig.Gui.TimeFormat,
c.UserConfig.Gui.ShortTimeFormat,
time.Now(),

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