mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-02 12:06:48 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6705428a8d | ||
|
|
bbd17abc43 | ||
|
|
12ed50464b | ||
|
|
57991c1da8 | ||
|
|
eecb59dd8a | ||
|
|
b8fcaf01b8 | ||
|
|
c80b0e1910 | ||
|
|
f250dfb8de | ||
|
|
f3466e2525 | ||
|
|
7a24c5628a | ||
|
|
cabcd54508 | ||
|
|
a4f43cb275 | ||
|
|
caa8c921e6 | ||
|
|
48305c18f7 | ||
|
|
ffda51014d | ||
|
|
61822b73f0 | ||
|
|
df805f3a1a | ||
|
|
0249d4c8ab | ||
|
|
4f959da9f8 | ||
|
|
da5789ba5f | ||
|
|
bf13e0bc6a | ||
|
|
5686e63af3 | ||
|
|
1fbfefa625 | ||
|
|
01b63a25ba | ||
|
|
43efdded4a | ||
|
|
e67bc45d48 | ||
|
|
a0ec22c251 | ||
|
|
3bd8a923ee | ||
|
|
47f1d847f1 | ||
|
|
04bacbf9ce | ||
|
|
26e58b8def | ||
|
|
5d30409f33 | ||
|
|
450239d5c8 | ||
|
|
c3081ef02e | ||
|
|
636b94cf22 | ||
|
|
1a880f916c | ||
|
|
a27db87fb6 | ||
|
|
f6d13330dd | ||
|
|
5321101276 | ||
|
|
50b2aa5843 | ||
|
|
b02441cdca | ||
|
|
0c0f95168c | ||
|
|
200b34ff28 | ||
|
|
144565ee03 | ||
|
|
ef1da6f704 | ||
|
|
4e497eef8a | ||
|
|
e6bd9d0ae6 | ||
|
|
1028f8efb8 | ||
|
|
c16c9f982f | ||
|
|
252dda5220 | ||
|
|
a6a68778ea | ||
|
|
223978eb82 | ||
|
|
e8d365f1aa | ||
|
|
a9f9dee30d | ||
|
|
5f4be3bfb7 | ||
|
|
02611dad7c | ||
|
|
a199ed1396 | ||
|
|
8cf617b683 | ||
|
|
a400ef0079 | ||
|
|
4e3d09e9d8 | ||
|
|
e295477951 | ||
|
|
7137196788 | ||
|
|
66caa25dcd | ||
|
|
6ca627d9d7 | ||
|
|
e63abf89db | ||
|
|
28aa26f30a | ||
|
|
18e5b0a650 | ||
|
|
13c21365c0 | ||
|
|
8d834e2eab | ||
|
|
722cc85508 | ||
|
|
0f1f455edb | ||
|
|
e27bc15bbd | ||
|
|
1037371a44 | ||
|
|
97aa7a04e6 | ||
|
|
5844ec6eb2 | ||
|
|
cb0c8f39bf | ||
|
|
5b109b2dd6 | ||
|
|
9d202cf9ea | ||
|
|
4cfa6e0c98 | ||
|
|
14eb4c29ca | ||
|
|
d87bdaf94d | ||
|
|
80e2e76f60 | ||
|
|
326e7e8716 | ||
|
|
159bbb0825 | ||
|
|
ac9b830bf1 | ||
|
|
6402c830d5 | ||
|
|
3ff1be0788 | ||
|
|
0496e3af50 | ||
|
|
bb64e3ccb6 | ||
|
|
4d0eaea6fc | ||
|
|
8bbfd4b9b9 | ||
|
|
3302a090f9 | ||
|
|
829aa3c6af | ||
|
|
b353d2a55a | ||
|
|
2b43f5b592 | ||
|
|
9e90d5ee62 | ||
|
|
7c2063c7a9 | ||
|
|
da24deb881 | ||
|
|
aa68885215 | ||
|
|
f65166ae91 | ||
|
|
30868eead8 | ||
|
|
4bf11eae4b | ||
|
|
9d88d6a44e | ||
|
|
483195110a | ||
|
|
ecc70f4764 | ||
|
|
bd1e34b39d | ||
|
|
7495854243 | ||
|
|
eaaa937315 | ||
|
|
2174762315 | ||
|
|
3577808a14 | ||
|
|
e4362eea60 | ||
|
|
4e21a096b9 | ||
|
|
98e4cb733f | ||
|
|
fbb8b2e17e | ||
|
|
acfa024915 | ||
|
|
338064ac2c | ||
|
|
ec51efc82c | ||
|
|
2b399a3c36 | ||
|
|
60887eddd0 | ||
|
|
7b17f33e9e | ||
|
|
fa35f661d9 | ||
|
|
1a93b2324b | ||
|
|
b7b7c65999 | ||
|
|
2a2705dcc3 | ||
|
|
7b96615792 | ||
|
|
b97dd6bc3f | ||
|
|
6c0ee7df2b | ||
|
|
548d486aee | ||
|
|
74054c9d70 | ||
|
|
6cbc1e0ace | ||
|
|
98c19feccf | ||
|
|
e90a3dc666 | ||
|
|
0dab37a631 | ||
|
|
945b023eb5 | ||
|
|
1c91999f61 | ||
|
|
f14a3cdd46 | ||
|
|
c13c6356e3 | ||
|
|
108054efc6 | ||
|
|
27825eba9e | ||
|
|
4b35434eba | ||
|
|
05b3ae9524 | ||
|
|
b7d01d67a6 | ||
|
|
b350876f84 | ||
|
|
5ba8d42c80 | ||
|
|
9b88052a4e | ||
|
|
6b6d881624 | ||
|
|
bb4d03db1f | ||
|
|
687bae48ed | ||
|
|
ff465e2581 | ||
|
|
9c8f987934 | ||
|
|
d84ee606e8 | ||
|
|
3dc045b69c | ||
|
|
90db796f42 | ||
|
|
3e5024480d | ||
|
|
4b3262ab3e | ||
|
|
362678e2ef | ||
|
|
542525743c | ||
|
|
8af8f7754b | ||
|
|
e1eb95b2b3 | ||
|
|
b210b4363d | ||
|
|
cd36e95a82 | ||
|
|
1a73697546 | ||
|
|
37f835244d | ||
|
|
94fc4d7eb4 | ||
|
|
682e54222c | ||
|
|
89ba3f2b1a | ||
|
|
fa9d75835c | ||
|
|
9e3e1a7b3a | ||
|
|
0236682882 | ||
|
|
058699cb71 |
@@ -0,0 +1,39 @@
|
||||
name: Close Issues
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
close_issue:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.issue.pull_request == null && startsWith(github.event.comment.body, '/close') }}
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const trustedUsers = ['ChrisMcD1', 'jesseduffield', 'stefanhaller']
|
||||
const commenter = context.payload.comment.user.login
|
||||
|
||||
console.log(`Commenter: ${commenter}`)
|
||||
|
||||
if (!trustedUsers.includes(commenter)) {
|
||||
console.log(`User ${commenter} is not trusted. Ignoring.`)
|
||||
return
|
||||
}
|
||||
|
||||
const issueNumber = context.payload.issue.number
|
||||
const owner = context.repo.owner
|
||||
const repo = context.repo.repo
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
state: 'closed'
|
||||
})
|
||||
|
||||
console.log(`Closed issue #${issueNumber} by request from ${commenter}.`)
|
||||
+9
-1
@@ -337,6 +337,10 @@ git:
|
||||
# If true, periodically refresh files and submodules
|
||||
autoRefresh: true
|
||||
|
||||
# If not "none", lazygit will automatically fast-forward local branches to match their upstream after fetching. Applies to branches that are not the currently checked out branch, and only to those that are strictly behind their upstream (as opposed to diverged).
|
||||
# Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
|
||||
autoForwardBranches: onlyMainBranches
|
||||
|
||||
# If true, pass the --all arg to git fetch
|
||||
fetchAll: true
|
||||
|
||||
@@ -350,7 +354,8 @@ git:
|
||||
branchLogCmd: git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --
|
||||
|
||||
# Commands used to display git log of all branches in the main window, they will be cycled in order of appearance (array of strings)
|
||||
allBranchesLogCmds: []
|
||||
allBranchesLogCmds:
|
||||
- git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium
|
||||
|
||||
# If true, do not spawn a separate process when using GPG
|
||||
overrideGpg: false
|
||||
@@ -501,6 +506,7 @@ keybinding:
|
||||
- "3"
|
||||
- "4"
|
||||
- "5"
|
||||
focusMainView: "0"
|
||||
nextMatch: "n"
|
||||
prevMatch: "N"
|
||||
startSearch: /
|
||||
@@ -510,6 +516,7 @@ keybinding:
|
||||
goInto: <enter>
|
||||
confirm: <enter>
|
||||
confirmInEditor: <a-enter>
|
||||
confirmInEditor-alt: <c-s>
|
||||
remove: d
|
||||
new: "n"
|
||||
edit: e
|
||||
@@ -582,6 +589,7 @@ keybinding:
|
||||
rebaseBranch: r
|
||||
renameBranch: R
|
||||
mergeIntoCurrentBranch: M
|
||||
moveCommitsToNewBranch: "N"
|
||||
viewGitFlowOptions: i
|
||||
fastForward: f
|
||||
createTag: T
|
||||
|
||||
@@ -14,7 +14,7 @@ customCommands:
|
||||
- key: 'C'
|
||||
context: 'global'
|
||||
command: "git commit"
|
||||
subprocess: true
|
||||
output: terminal
|
||||
- key: 'n'
|
||||
context: 'localBranches'
|
||||
prompts:
|
||||
@@ -53,13 +53,11 @@ For a given custom command, here are the allowed fields:
|
||||
| key | The key to trigger the command. Use a single letter or one of the values from [here](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md). Custom commands without a key specified can be triggered by selecting them from the keybindings (`?`) menu | no |
|
||||
| command | The command to run (using Go template syntax for placeholder values) | yes |
|
||||
| context | The context in which to listen for the key (see [below](#contexts)) | yes |
|
||||
| subprocess | Whether you want the command to run in a subprocess (e.g. if the command requires user input) | no |
|
||||
| prompts | A list of prompts that will request user input before running the final command | no |
|
||||
| loadingText | Text to display while waiting for command to finish | no |
|
||||
| description | Label for the custom command when displayed in the keybindings menu | no |
|
||||
| stream | Whether you want to stream the command's output to the Command Log panel | no |
|
||||
| showOutput | Whether you want to show the command's output in a popup within Lazygit | no |
|
||||
| outputTitle | The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title. | no |
|
||||
| output | Where the output of the command should go. 'none' discards it, 'terminal' suspends lazygit and runs the command in the terminal (useful for commands that require user input), 'log' streams it to the command log, 'logWithPty' is like 'log' but runs the command in a pseudo terminal (can be useful for commands that produce colored output when the output is a terminal), and 'popup' shows it in a popup. | no |
|
||||
| outputTitle | The title to display in the popup panel if output is set to 'popup'. If left unset, the command will be used as the title. | no |
|
||||
| after | Actions to take after the command has completed | no |
|
||||
|
||||
Here are the options for the `after` key:
|
||||
@@ -365,7 +363,7 @@ If you use the commandMenu property, none of the other properties except key and
|
||||
|
||||
## Debugging
|
||||
|
||||
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `showOutput: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved.
|
||||
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `output: popup` so that it doesn't actually execute the command but you can see how the placeholders were resolved.
|
||||
|
||||
## More Examples
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` ` `` | Toggle file tree view | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Commit summary
|
||||
@@ -108,10 +109,14 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
@@ -153,6 +158,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` f `` | Fetch | Fetch changes from remote. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Local branches
|
||||
@@ -163,6 +169,9 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` i `` | Show git-flow options | |
|
||||
| `` <space> `` | Checkout | Checkout selected item. |
|
||||
| `` n `` | New branch | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` o `` | Create pull request | |
|
||||
| `` O `` | View create pull request options | |
|
||||
| `` <c-y> `` | Copy pull request URL to clipboard | |
|
||||
@@ -178,6 +187,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` R `` | Rename branch | |
|
||||
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -204,6 +214,9 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
|-----|--------|-------------|
|
||||
| `` mouse wheel down (fn+up) `` | Scroll down | |
|
||||
| `` mouse wheel up (fn+down) `` | Scroll up | |
|
||||
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Main panel (patch building)
|
||||
|
||||
@@ -259,11 +272,15 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -282,6 +299,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` s `` | Sort order | |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -297,6 +315,14 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` f `` | Fetch | Fetch updates from the remote repository. This retrieves new commits and branches without merging them into your local branches. |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Secondary
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Stash
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -306,6 +332,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` d `` | Drop | Remove the stash entry from the stash list. |
|
||||
| `` n `` | New branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` r `` | Rename stash | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -329,11 +356,15 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
@@ -363,6 +394,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
@@ -51,6 +51,14 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` ] `` | 次のタブ | |
|
||||
| `` [ `` | 前のタブ | |
|
||||
|
||||
## Secondary
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | パネルを切り替え | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | 検索を開始 | |
|
||||
|
||||
## Stash
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -60,6 +68,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` d `` | Drop | Remove the stash entry from the stash list. |
|
||||
| `` n `` | 新しいブランチを作成 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` r `` | Stashを変更 | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -73,11 +82,15 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` y `` | コミットの情報をコピー | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | ブラウザでコミットを開く | |
|
||||
| `` n `` | コミットにブランチを作成 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | コミットをコピー (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | 検索を開始 | |
|
||||
@@ -123,10 +136,14 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | コミットの情報をコピー | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | ブラウザでコミットを開く | |
|
||||
| `` n `` | コミットにブランチを作成 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | コミットをコピー (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | 検索を開始 | |
|
||||
@@ -148,6 +165,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` ` `` | ファイルツリーの表示を切り替え | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | 検索を開始 | |
|
||||
|
||||
## コミットメッセージ
|
||||
@@ -192,6 +210,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` P `` | タグをpush | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | コミットを閲覧 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -226,6 +245,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` f `` | Fetch | Fetch changes from remote. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | 検索を開始 | |
|
||||
|
||||
## ブランチ
|
||||
@@ -236,6 +256,9 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` i `` | Show git-flow options | |
|
||||
| `` <space> `` | チェックアウト | Checkout selected item. |
|
||||
| `` n `` | 新しいブランチを作成 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` o `` | Pull Requestを作成 | |
|
||||
| `` O `` | View create pull request options | |
|
||||
| `` <c-y> `` | Pull RequestのURLをクリップボードにコピー | |
|
||||
@@ -251,6 +274,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` R `` | ブランチ名を変更 | |
|
||||
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | コミットを閲覧 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -277,6 +301,9 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
|-----|--------|-------------|
|
||||
| `` mouse wheel down (fn+up) `` | 下にスクロール | |
|
||||
| `` mouse wheel up (fn+down) `` | 上にスクロール | |
|
||||
| `` <tab> `` | パネルを切り替え | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | 検索を開始 | |
|
||||
|
||||
## メインパネル (Patch Building)
|
||||
|
||||
@@ -348,6 +375,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` s `` | 並び替え | |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | コミットを閲覧 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -361,11 +389,15 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | コミットの情報をコピー | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | ブラウザでコミットを開く | |
|
||||
| `` n `` | コミットにブランチを作成 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | コミットをコピー (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | コミットを閲覧 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
@@ -60,15 +60,27 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | 브라우저에서 커밋 열기 | |
|
||||
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | Reset cherry-picked (copied) commits selection | |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Secondary
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## Stash
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -78,6 +90,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` d `` | Drop | Remove the stash entry from the stash list. |
|
||||
| `` n `` | 새 브랜치 생성 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` r `` | Rename stash | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View selected item's files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -91,11 +104,15 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | 브라우저에서 커밋 열기 | |
|
||||
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | Reset cherry-picked (copied) commits selection | |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View selected item's files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
@@ -140,6 +157,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
|-----|--------|-------------|
|
||||
| `` mouse wheel down (fn+up) `` | 아래로 스크롤 | |
|
||||
| `` mouse wheel up (fn+down) `` | 위로 스크롤 | |
|
||||
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## 메인 패널 (Patch Building)
|
||||
|
||||
@@ -186,6 +206,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` i `` | Git-flow 옵션 보기 | |
|
||||
| `` <space> `` | 체크아웃 | Checkout selected item. |
|
||||
| `` n `` | 새 브랜치 생성 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` o `` | 풀 리퀘스트 생성 | |
|
||||
| `` O `` | 풀 리퀘스트 생성 옵션 | |
|
||||
| `` <c-y> `` | 풀 리퀘스트 URL을 클립보드에 복사 | |
|
||||
@@ -201,6 +224,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` R `` | 브랜치 이름 변경 | |
|
||||
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -254,6 +278,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` s `` | Sort order | |
|
||||
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -289,10 +314,14 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | 브라우저에서 커밋 열기 | |
|
||||
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View selected item's files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
@@ -314,6 +343,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` ` `` | 파일 트리뷰로 전환 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## 커밋메시지
|
||||
@@ -334,6 +364,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` P `` | 태그를 push | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | 초기화 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -368,6 +399,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` f `` | Fetch | Fetch changes from remote. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## 확인 패널
|
||||
|
||||
@@ -81,6 +81,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` f `` | Fetch | Fetch changes from remote. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Bevestigingspaneel
|
||||
@@ -98,6 +99,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` i `` | Laat git-flow opties zien | |
|
||||
| `` <space> `` | Uitchecken | Checkout selected item. |
|
||||
| `` n `` | Nieuwe branch | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` o `` | Maak een pull-request | |
|
||||
| `` O `` | Bekijk opties voor pull-aanvraag | |
|
||||
| `` <c-y> `` | Kopieer de URL van het pull-verzoek naar het klembord | |
|
||||
@@ -113,6 +117,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` R `` | Hernoem branch | |
|
||||
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -141,6 +146,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` ` `` | Toggle bestandsboom weergave | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Commits
|
||||
@@ -174,10 +180,14 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Creëer nieuwe branch van commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk gecommite bestanden | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
@@ -212,6 +222,9 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
|-----|--------|-------------|
|
||||
| `` mouse wheel down (fn+up) `` | Scroll omlaag | |
|
||||
| `` mouse wheel up (fn+down) `` | Scroll omhoog | |
|
||||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Patch bouwen
|
||||
|
||||
@@ -237,11 +250,15 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Creëer nieuwe branch van commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -260,6 +277,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` s `` | Sort order | |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -275,6 +293,14 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` f `` | Fetch | Fetch remote |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Secondary
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Staging
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -306,6 +332,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` d `` | Laten vallen | Remove the stash entry from the stash list. |
|
||||
| `` n `` | Nieuwe branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` r `` | Rename stash | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk gecommite bestanden | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -329,11 +356,15 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Creëer nieuwe branch van commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk gecommite bestanden | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
@@ -363,6 +394,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
@@ -82,14 +82,26 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|
||||
| `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). |
|
||||
| `` o `` | Otwórz commit w przeglądarce | |
|
||||
| `` n `` | Utwórz nową gałąź z commita | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
|
||||
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Wyświetl pliki | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Dodatkowy
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Drzewa pracy
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -123,6 +135,9 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|
||||
| `` i `` | Pokaż opcje git-flow | |
|
||||
| `` <space> `` | Przełącz | Przełącz wybrany element. |
|
||||
| `` n `` | Nowa gałąź | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` o `` | Utwórz żądanie ściągnięcia | |
|
||||
| `` O `` | Zobacz opcje tworzenia pull requesta | |
|
||||
| `` <c-y> `` | Kopiuj adres URL żądania ściągnięcia do schowka | |
|
||||
@@ -138,6 +153,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|
||||
| `` R `` | Zmień nazwę gałęzi | |
|
||||
| `` u `` | Pokaż opcje upstream | Pokaż opcje dotyczące upstream gałęzi, np. ustawianie/usuwanie upstream i resetowanie do upstream. |
|
||||
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
@@ -156,6 +172,9 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|
||||
|-----|--------|-------------|
|
||||
| `` mouse wheel down (fn+up) `` | Przewiń w dół | |
|
||||
| `` mouse wheel up (fn+down) `` | Przewiń w górę | |
|
||||
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Panel główny (scalanie)
|
||||
|
||||
@@ -232,6 +251,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|
||||
| `` f `` | Pobierz | Pobierz zmiany ze zdalnego serwera. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Pliki commita
|
||||
@@ -251,6 +271,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|
||||
| `` ` `` | Przełącz widok drzewa plików | Przełącz widok plików między płaskim a drzewem. Płaski układ pokazuje wszystkie ścieżki plików na jednej liście, układ drzewa grupuje pliki według katalogów. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Podsumowanie commita
|
||||
@@ -269,11 +290,15 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|
||||
| `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). |
|
||||
| `` o `` | Otwórz commit w przeglądarce | |
|
||||
| `` n `` | Utwórz nową gałąź z commita | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
|
||||
| `` <c-r> `` | Resetuj wybrane (cherry-picked) commity | |
|
||||
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
@@ -287,6 +312,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|
||||
| `` d `` | Usuń | Usuń wpis schowka z listy schowka. |
|
||||
| `` n `` | Nowa gałąź | Utwórz nową gałąź z wybranego wpisu schowka. Działa poprzez przełączenie git na commit, na którym wpis schowka został utworzony, tworzenie nowej gałęzi z tego commita, a następnie zastosowanie wpisu schowka do nowej gałęzi jako dodatkowego commita. |
|
||||
| `` r `` | Zmień nazwę schowka | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Wyświetl pliki | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
@@ -310,11 +336,15 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|
||||
| `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). |
|
||||
| `` o `` | Otwórz commit w przeglądarce | |
|
||||
| `` n `` | Utwórz nową gałąź z commita | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
|
||||
| `` <c-r> `` | Resetuj wybrane (cherry-picked) commity | |
|
||||
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Wyświetl pliki | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
@@ -344,6 +374,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|
||||
| `` P `` | Wyślij tag | Wyślij wybrany tag do zdalnego. Zostaniesz poproszony o wybranie zdalnego. |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
@@ -373,6 +404,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|
||||
| `` s `` | Kolejność sortowania | |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
||||
@@ -83,6 +83,7 @@ Veja a documentação:
|
||||
| `` f `` | Buscar | Buscar alterações do controle remoto. |
|
||||
| `` - `` | Recolher todos os arquivos | Recolher todos os diretórios na árvore de arquivos |
|
||||
| `` = `` | Expandir todos os arquivos | Expandir todos os diretórios na árvore do arquivo |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Branches locais
|
||||
@@ -93,6 +94,9 @@ Veja a documentação:
|
||||
| `` i `` | Show git-flow options | |
|
||||
| `` <space> `` | Verificar | Checar item selecionado |
|
||||
| `` n `` | Nova branch | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` o `` | Create pull request | |
|
||||
| `` O `` | View create pull request options | |
|
||||
| `` <c-y> `` | Copiar URL do pull request para área de transferência | |
|
||||
@@ -108,6 +112,7 @@ Veja a documentação:
|
||||
| `` R `` | Rename branch | |
|
||||
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
|
||||
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -126,6 +131,7 @@ Veja a documentação:
|
||||
| `` s `` | Sort order | |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -147,6 +153,7 @@ Veja a documentação:
|
||||
| `` ` `` | Alternar exibição de árvore de arquivo | Alternar a visualização de arquivo entre layout plano e layout de árvore. Layout plano mostra todos os caminhos de arquivo em uma única lista, layout de árvore agrupa arquivos por diretório. |
|
||||
| `` - `` | Recolher todos os arquivos | Recolher todos os diretórios na árvore de arquivos |
|
||||
| `` = `` | Expandir todos os arquivos | Expandir todos os diretórios na árvore do arquivo |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Commits
|
||||
@@ -180,10 +187,14 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
|
||||
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Ver arquivos | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
@@ -206,6 +217,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -224,6 +236,9 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
|-----|--------|-------------|
|
||||
| `` mouse wheel down (fn+up) `` | Rolar para baixo | |
|
||||
| `` mouse wheel up (fn+down) `` | Rolar para cima | |
|
||||
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Painel Principal (preparação)
|
||||
|
||||
@@ -289,11 +304,15 @@ Veja a documentação:
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
|
||||
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -309,6 +328,14 @@ Veja a documentação:
|
||||
| `` f `` | Buscar | Fetch updates from the remote repository. This retrieves new commits and branches without merging them into your local branches. |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Secundário
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Stash
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -318,6 +345,7 @@ Veja a documentação:
|
||||
| `` d `` | Descartar | Remova a entrada do stash da lista de armazenamento. |
|
||||
| `` n `` | Nova branch | Criar um novo ramo a partir da entrada de lixo selecionada. Isso funciona verificando o commit do qual a entrada de lixo foi criada, criar um novo branch a partir desse commit e, em seguida, aplicar a entrada de lixo ao novo branch como um commit adicional. |
|
||||
| `` r `` | Renomear o stasj | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Ver arquivos | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -341,11 +369,15 @@ Veja a documentação:
|
||||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
|
||||
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Ver arquivos | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
@@ -61,6 +61,14 @@ _Связки клавиш_
|
||||
| `` d `` | Remove | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Вторичный
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Главная панель (Индексирование)
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -89,6 +97,9 @@ _Связки клавиш_
|
||||
|-----|--------|-------------|
|
||||
| `` mouse wheel down (fn+up) `` | Прокрутить вниз | |
|
||||
| `` mouse wheel up (fn+down) `` | Прокрутить вверх | |
|
||||
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Главная панель (Слияние)
|
||||
|
||||
@@ -130,11 +141,15 @@ _Связки клавиш_
|
||||
| `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Открыть коммит в браузере | |
|
||||
| `` n `` | Создать новую ветку с этого коммита | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | Сбросить отобранную (скопированную | cherry-picked) выборку коммитов | |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -170,10 +185,14 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Открыть коммит в браузере | |
|
||||
| `` n `` | Создать новую ветку с этого коммита | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Найти | |
|
||||
@@ -186,6 +205,9 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` i `` | Показать параметры git-flow | |
|
||||
| `` <space> `` | Переключить | Checkout selected item. |
|
||||
| `` n `` | Новая ветка | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` o `` | Создать запрос на принятие изменений | |
|
||||
| `` O `` | Создать параметры запроса принятие изменений | |
|
||||
| `` <c-y> `` | Скопировать URL запроса на принятие изменений в буфер обмена | |
|
||||
@@ -201,6 +223,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` R `` | Переименовать ветку | |
|
||||
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -229,11 +252,15 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Открыть коммит в браузере | |
|
||||
| `` n `` | Создать новую ветку с этого коммита | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | Сбросить отобранную (скопированную | cherry-picked) выборку коммитов | |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Найти | |
|
||||
@@ -276,6 +303,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` ` `` | Переключить вид дерева файлов | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Статус
|
||||
@@ -299,6 +327,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` P `` | Отправить тег | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -317,6 +346,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` s `` | Порядок сортировки | |
|
||||
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
@@ -362,6 +392,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` f `` | Получить изменения | Fetch changes from remote. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Хранилище
|
||||
@@ -373,6 +404,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` d `` | Удалить припрятанные изменения из хранилища | Remove the stash entry from the stash list. |
|
||||
| `` n `` | Новая ветка | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` r `` | Переименовать хранилище | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
@@ -60,11 +60,15 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(例如,hash、URL、diff、消息、作者)。 |
|
||||
| `` o `` | 在浏览器中打开提交 | |
|
||||
| `` n `` | 从提交创建新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,你可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
|
||||
| `` <c-r> `` | 重置已拣选(复制)的提交 | |
|
||||
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
@@ -78,11 +82,15 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(例如,hash、URL、diff、消息、作者)。 |
|
||||
| `` o `` | 在浏览器中打开提交 | |
|
||||
| `` n `` | 从提交创建新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,你可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
|
||||
| `` <c-r> `` | 重置已拣选(复制)的提交 | |
|
||||
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 查看提交的文件 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
@@ -142,10 +150,14 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(例如,hash、URL、diff、消息、作者)。 |
|
||||
| `` o `` | 在浏览器中打开提交 | |
|
||||
| `` n `` | 从提交创建新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,你可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
|
||||
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 查看提交的文件 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
@@ -174,6 +186,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` ` `` | 切换文件树视图 | 在平铺部署与树布局之间切换文件视图。平铺布局在一个列表中展示所有文件路径,树布局则根据目录分组展示。 |
|
||||
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
|
||||
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 文件
|
||||
@@ -206,6 +219,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` f `` | 抓取 | 从远程获取变更 |
|
||||
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
|
||||
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 本地分支
|
||||
@@ -216,6 +230,9 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` i `` | 显示 git-flow 选项 | |
|
||||
| `` <space> `` | 检出 | 检出选中的项目 |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` o `` | 创建拉取请求 | |
|
||||
| `` O `` | 创建拉取请求选项 | |
|
||||
| `` <c-y> `` | 将拉取请求 URL 复制到剪贴板 | |
|
||||
@@ -231,6 +248,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` R `` | 重命名分支 | |
|
||||
| `` u `` | 查看上游选项 | 查看与分支上游相关的选项,例如设置/取消设置上游和重置为上游。 |
|
||||
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
@@ -261,10 +279,19 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` P `` | 推送标签 | 推送选择的标签到远端。你将在弹窗中选择一个远端。 |
|
||||
| `` g `` | 重置 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
## 次要
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 正在合并
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -309,6 +336,9 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
|-----|--------|-------------|
|
||||
| `` mouse wheel down (fn+up) `` | 向下滚动 | |
|
||||
| `` mouse wheel up (fn+down) `` | 向上滚动 | |
|
||||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 状态
|
||||
|
||||
@@ -344,6 +374,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` d `` | 删除 | 从贮藏列表中删除该贮藏项 |
|
||||
| `` n `` | 新分支 | 从选定的贮藏项创建一个新分支。这是通过 git 检查创建贮藏项的提交,从该提交创建一个新分支,然后将贮藏项作为附加提交应用到新分支来实现的。 |
|
||||
| `` r `` | 重命名贮藏 | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 查看提交的文件 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
@@ -373,6 +404,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` s `` | 排序 | |
|
||||
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
@@ -72,6 +72,9 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
|
||||
|-----|--------|-------------|
|
||||
| `` mouse wheel down (fn+up) `` | 向下捲動 | |
|
||||
| `` mouse wheel up (fn+down) `` | 向上捲動 | |
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 主面板(合併)
|
||||
|
||||
@@ -128,11 +131,15 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
|
||||
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | 在瀏覽器中開啟提交 | |
|
||||
| `` n `` | 從提交建立新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
@@ -192,10 +199,14 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | 在瀏覽器中開啟提交 | |
|
||||
| `` n `` | 從提交建立新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
@@ -224,6 +235,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` ` `` | 顯示檔案樹狀視圖 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 收藏 (Stash)
|
||||
@@ -235,6 +247,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` d `` | 捨棄 | Remove the stash entry from the stash list. |
|
||||
| `` n `` | 新分支 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` r `` | 重新命名收藏 | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
@@ -248,11 +261,15 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | 在瀏覽器中開啟提交 | |
|
||||
| `` n `` | 從提交建立新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <c-r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
@@ -265,6 +282,9 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` i `` | 顯示 git-flow 選項 | |
|
||||
| `` <space> `` | 檢出 | 檢出選定的項目。 |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
|
||||
|
||||
Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` o `` | 建立拉取請求 | |
|
||||
| `` O `` | 建立拉取請求選項 | |
|
||||
| `` <c-y> `` | 複製拉取請求的 URL 到剪貼板 | |
|
||||
@@ -280,6 +300,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` R `` | 重新命名分支 | |
|
||||
| `` u `` | 檢視遠端設定 | 檢視有關遠端分支的設定(例如重設至遠端) |
|
||||
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
@@ -295,6 +316,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` P `` | 推送標籤 | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | 重設 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
@@ -329,6 +351,15 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` f `` | 擷取 | 同步遠端異動 |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 次要
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 狀態
|
||||
@@ -373,6 +404,7 @@ If you would instead like to start an interactive rebase from the selected commi
|
||||
| `` s `` | 排序規則 | |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -13,9 +13,9 @@ require (
|
||||
github.com/go-errors/errors v1.5.1
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/generics v0.0.0-20250406224309-4f541cb84918
|
||||
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250408140206-7f1bb9232647
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e
|
||||
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
|
||||
@@ -33,11 +33,11 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/afero v1.9.5
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
github.com/stefanhaller/git-todo-parser v0.0.7-0.20240406123903-fd957137b6e2
|
||||
github.com/stefanhaller/git-todo-parser v0.0.7-0.20250429125209-dcf39e4641f5
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
golang.org/x/sync v0.13.0
|
||||
golang.org/x/sync v0.14.0
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
@@ -78,9 +78,9 @@ require (
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
|
||||
@@ -190,12 +190,12 @@ github.com/invopop/jsonschema v0.10.0 h1:c1ktzNLBun3LyQQhyty5WE3lulbOdIIyOVlkmDL
|
||||
github.com/invopop/jsonschema v0.10.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jesseduffield/generics v0.0.0-20250406224309-4f541cb84918 h1:meoUDZGF6jZAbhW5IBwj92mTqGmrOn+Cuu0jM7/aUcs=
|
||||
github.com/jesseduffield/generics v0.0.0-20250406224309-4f541cb84918/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
|
||||
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2PaiisXAC5sOjDPfMArSnbswDObtCssx+xn28edX4=
|
||||
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c/go.mod h1:F2fEBk0ddf6ixrBrJjY7phfQ3hL9rXG0uSjvwYe50bE=
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd h1:ViKj6qth8FgcIWizn9KiACWwPemWSymx62OPN0tHT+Q=
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd/go.mod h1:lRhCiBr6XjQrvcQVa+UYsy/99d3wMXn/a0nSQlhnhlA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250408140206-7f1bb9232647 h1:XpoXCyLGCB4mczPc1q/8dJefr6OB1xa7jyhH5a5eJqY=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250408140206-7f1bb9232647/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e h1:NuSXZHAUSo+dSAYArlaDpmKgxVpojuKvNcRkYJcQxkY=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
|
||||
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a h1:UDeJ3EBk04bXDLOPvuqM3on8HvyJfISw0+UMqW+0a4g=
|
||||
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a/go.mod h1:FSWDLKT0NQpntbDd1H3lbz51fhCVlMzy/J0S6nM727Q=
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
|
||||
@@ -286,8 +286,8 @@ 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/git-todo-parser v0.0.7-0.20240406123903-fd957137b6e2 h1:RTNWemd/9z9A5L/AggEP3OdhRz5dXETB/wdAlYF0SuM=
|
||||
github.com/stefanhaller/git-todo-parser v0.0.7-0.20240406123903-fd957137b6e2/go.mod h1:HFt9hGqMzgQ+gVxMKcvTvGaFz4Y0yYycqqAp2V3wcJY=
|
||||
github.com/stefanhaller/git-todo-parser v0.0.7-0.20250429125209-dcf39e4641f5 h1:ZCI0NPs0xXd00Ej9lX+wwbHjQDkstJa3kUbX7WCOF8I=
|
||||
github.com/stefanhaller/git-todo-parser v0.0.7-0.20250429125209-dcf39e4641f5/go.mod h1:HFt9hGqMzgQ+gVxMKcvTvGaFz4Y0yYycqqAp2V3wcJY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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=
|
||||
@@ -436,8 +436,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -490,8 +490,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -501,8 +501,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
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=
|
||||
@@ -517,8 +517,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
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=
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
@@ -33,7 +32,6 @@ const (
|
||||
|
||||
DaemonKindExitImmediately
|
||||
DaemonKindRemoveUpdateRefsForCopiedBranch
|
||||
DaemonKindCherryPick
|
||||
DaemonKindMoveTodosUp
|
||||
DaemonKindMoveTodosDown
|
||||
DaemonKindInsertBreak
|
||||
@@ -56,7 +54,6 @@ func getInstruction() Instruction {
|
||||
mapping := map[DaemonKind]func(string) Instruction{
|
||||
DaemonKindExitImmediately: deserializeInstruction[*ExitImmediatelyInstruction],
|
||||
DaemonKindRemoveUpdateRefsForCopiedBranch: deserializeInstruction[*RemoveUpdateRefsForCopiedBranchInstruction],
|
||||
DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction],
|
||||
DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction],
|
||||
DaemonKindDropMergeCommit: deserializeInstruction[*DropMergeCommitInstruction],
|
||||
DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction],
|
||||
@@ -180,39 +177,6 @@ func NewRemoveUpdateRefsForCopiedBranchInstruction() Instruction {
|
||||
return &RemoveUpdateRefsForCopiedBranchInstruction{}
|
||||
}
|
||||
|
||||
type CherryPickCommitsInstruction struct {
|
||||
Todo string
|
||||
}
|
||||
|
||||
func NewCherryPickCommitsInstruction(commits []*models.Commit) Instruction {
|
||||
todoLines := lo.Map(commits, func(commit *models.Commit, _ int) TodoLine {
|
||||
return TodoLine{
|
||||
Action: "pick",
|
||||
Commit: commit,
|
||||
}
|
||||
})
|
||||
|
||||
todo := TodoLinesToString(todoLines)
|
||||
|
||||
return &CherryPickCommitsInstruction{
|
||||
Todo: todo,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CherryPickCommitsInstruction) Kind() DaemonKind {
|
||||
return DaemonKindCherryPick
|
||||
}
|
||||
|
||||
func (self *CherryPickCommitsInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *CherryPickCommitsInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return utils.PrependStrToTodoFile(path, []byte(self.Todo))
|
||||
})
|
||||
}
|
||||
|
||||
type ChangeTodoActionsInstruction struct {
|
||||
Changes []ChangeTodoAction
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func (self *TodoLine) ToString() string {
|
||||
if self.Action == "break" {
|
||||
return self.Action + "\n"
|
||||
} else {
|
||||
return self.Action + " " + self.Commit.Hash + " " + self.Commit.Name + "\n"
|
||||
return self.Action + " " + self.Commit.Hash() + " " + self.Commit.Name + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ func NewGitCommandAux(
|
||||
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.WorkingTreeState, gitCommon)
|
||||
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
||||
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
|
||||
worktreeLoader := git_commands.NewWorktreeLoader(gitCommon)
|
||||
|
||||
@@ -30,11 +30,11 @@ func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuild
|
||||
|
||||
var defaultEnvVar = "GIT_OPTIONAL_LOCKS=0"
|
||||
|
||||
func (self *gitCmdObjBuilder) New(args []string) oscommands.ICmdObj {
|
||||
func (self *gitCmdObjBuilder) New(args []string) *oscommands.CmdObj {
|
||||
return self.innerBuilder.New(args).AddEnvVars(defaultEnvVar)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) NewShell(cmdStr string, shellFunctionsFile string) oscommands.ICmdObj {
|
||||
func (self *gitCmdObjBuilder) NewShell(cmdStr string, shellFunctionsFile string) *oscommands.CmdObj {
|
||||
return self.innerBuilder.NewShell(cmdStr, shellFunctionsFile).AddEnvVars(defaultEnvVar)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ type gitCmdObjRunner struct {
|
||||
innerRunner oscommands.ICmdObjRunner
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) Run(cmdObj oscommands.ICmdObj) error {
|
||||
func (self *gitCmdObjRunner) Run(cmdObj *oscommands.CmdObj) error {
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) {
|
||||
func (self *gitCmdObjRunner) RunWithOutput(cmdObj *oscommands.CmdObj) (string, error) {
|
||||
var output string
|
||||
var err error
|
||||
for i := 0; i < RetryCount; i++ {
|
||||
@@ -44,7 +44,7 @@ func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, e
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) RunWithOutputs(cmdObj oscommands.ICmdObj) (string, string, error) {
|
||||
func (self *gitCmdObjRunner) RunWithOutputs(cmdObj *oscommands.CmdObj) (string, string, error) {
|
||||
var stdout, stderr string
|
||||
var err error
|
||||
for i := 0; i < RetryCount; i++ {
|
||||
@@ -64,6 +64,6 @@ func (self *gitCmdObjRunner) RunWithOutputs(cmdObj oscommands.ICmdObj) (string,
|
||||
}
|
||||
|
||||
// Retry logic not implemented here, but these commands typically don't need to obtain a lock.
|
||||
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj *oscommands.CmdObj, onLine func(line string) (bool, error)) error {
|
||||
return self.innerRunner.RunAndProcessLines(cmdObj, onLine)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,15 @@ func (self *BranchCommands) NewWithoutTracking(name string, base string) error {
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// NewWithoutCheckout creates a new branch without checking it out
|
||||
func (self *BranchCommands) NewWithoutCheckout(name string, base string) error {
|
||||
cmdArgs := NewGitCmd("branch").
|
||||
Arg(name, base).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// CreateWithUpstream creates a new branch with a given upstream, but without
|
||||
// checking it out
|
||||
func (self *BranchCommands) CreateWithUpstream(name string, upstream string) error {
|
||||
@@ -145,7 +154,7 @@ func (self *BranchCommands) GetGraph(branchName string) (string, error) {
|
||||
return self.GetGraphCmdObj(branchName).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) GetGraphCmdObj(branchName string) oscommands.ICmdObj {
|
||||
func (self *BranchCommands) GetGraphCmdObj(branchName string) *oscommands.CmdObj {
|
||||
branchLogCmdTemplate := self.UserConfig().Git.BranchLogCmd
|
||||
templateValues := map[string]string{
|
||||
"branchName": self.cmd.Quote(branchName),
|
||||
@@ -246,22 +255,24 @@ func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error {
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) AllBranchesLogCmdObj() oscommands.ICmdObj {
|
||||
// Only choose between non-empty, non-identical commands
|
||||
candidates := lo.Uniq(lo.WithoutEmpty(append([]string{
|
||||
self.UserConfig().Git.AllBranchesLogCmd,
|
||||
},
|
||||
self.UserConfig().Git.AllBranchesLogCmds...,
|
||||
)))
|
||||
// Only choose between non-empty, non-identical commands
|
||||
func (self *BranchCommands) allBranchesLogCandidates() []string {
|
||||
return lo.Uniq(lo.WithoutEmpty(self.UserConfig().Git.AllBranchesLogCmds))
|
||||
}
|
||||
|
||||
n := len(candidates)
|
||||
func (self *BranchCommands) AllBranchesLogCmdObj() *oscommands.CmdObj {
|
||||
candidates := self.allBranchesLogCandidates()
|
||||
|
||||
i := self.allBranchesLogCmdIndex
|
||||
self.allBranchesLogCmdIndex = uint8((int(i) + 1) % n)
|
||||
|
||||
return self.cmd.New(str.ToArgv(candidates[i])).DontLog()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) RotateAllBranchesLogIdx() {
|
||||
n := len(self.allBranchesLogCandidates())
|
||||
i := self.allBranchesLogCmdIndex
|
||||
self.allBranchesLogCmdIndex = uint8((int(i) + 1) % n)
|
||||
}
|
||||
|
||||
func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *MainBranches) (bool, error) {
|
||||
branchesToCheckAgainst := []string{"HEAD"}
|
||||
if branch.RemoteBranchStoredLocally() {
|
||||
@@ -285,3 +296,11 @@ func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *
|
||||
|
||||
return stdout == "", nil
|
||||
}
|
||||
|
||||
func (self *BranchCommands) UpdateBranchRefs(updateCommands string) error {
|
||||
cmdArgs := NewGitCmd("update-ref").
|
||||
Arg("--stdin").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).SetStdin(updateCommands).Run()
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func (self *CommitCommands) ResetToCommit(hash string, strength string, envVars
|
||||
Run()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) CommitCmdObj(summary string, description string, forceSkipHooks bool) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) CommitCmdObj(summary string, description string, forceSkipHooks bool) *oscommands.CmdObj {
|
||||
messageArgs := self.commitMessageArgs(summary, description)
|
||||
skipHookPrefix := self.UserConfig().Git.SkipHookPrefix
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
@@ -97,16 +97,16 @@ func (self *CommitCommands) CommitCmdObj(summary string, description string, for
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *CommitCommands) RewordLastCommitInEditorCmdObj() oscommands.ICmdObj {
|
||||
func (self *CommitCommands) RewordLastCommitInEditorCmdObj() *oscommands.CmdObj {
|
||||
return self.cmd.New(NewGitCmd("commit").Arg("--allow-empty", "--amend", "--only").ToArgv())
|
||||
}
|
||||
|
||||
func (self *CommitCommands) RewordLastCommitInEditorWithMessageFileCmdObj(tmpMessageFile string) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) RewordLastCommitInEditorWithMessageFileCmdObj(tmpMessageFile string) *oscommands.CmdObj {
|
||||
return self.cmd.New(NewGitCmd("commit").
|
||||
Arg("--allow-empty", "--amend", "--only", "--edit", "--file="+tmpMessageFile).ToArgv())
|
||||
}
|
||||
|
||||
func (self *CommitCommands) CommitInEditorWithMessageFileCmdObj(tmpMessageFile string, forceSkipHooks bool) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) CommitInEditorWithMessageFileCmdObj(tmpMessageFile string, forceSkipHooks bool) *oscommands.CmdObj {
|
||||
return self.cmd.New(NewGitCmd("commit").
|
||||
ArgIf(forceSkipHooks, "--no-verify").
|
||||
Arg("--edit").
|
||||
@@ -116,7 +116,7 @@ func (self *CommitCommands) CommitInEditorWithMessageFileCmdObj(tmpMessageFile s
|
||||
}
|
||||
|
||||
// RewordLastCommit rewords the topmost commit with the given message
|
||||
func (self *CommitCommands) RewordLastCommit(summary string, description string) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) RewordLastCommit(summary string, description string) *oscommands.CmdObj {
|
||||
messageArgs := self.commitMessageArgs(summary, description)
|
||||
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
@@ -138,7 +138,7 @@ func (self *CommitCommands) commitMessageArgs(summary string, description string
|
||||
}
|
||||
|
||||
// runs git commit without the -m argument meaning it will invoke the user's editor
|
||||
func (self *CommitCommands) CommitEditorCmdObj() oscommands.ICmdObj {
|
||||
func (self *CommitCommands) CommitEditorCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
|
||||
ToArgv()
|
||||
@@ -246,7 +246,7 @@ func (self *CommitCommands) AmendHead() error {
|
||||
return self.AmendHeadCmdObj().Run()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
|
||||
func (self *CommitCommands) AmendHeadCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
Arg("--amend", "--no-edit", "--allow-empty").
|
||||
ToArgv()
|
||||
@@ -254,7 +254,7 @@ func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) *oscommands.CmdObj {
|
||||
contextSize := self.AppState.DiffContextSize
|
||||
|
||||
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
|
||||
@@ -278,22 +278,21 @@ func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) oscommand
|
||||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) ShowFileContentCmdObj(hash string, filePath string) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) ShowFileContentCmdObj(hash string, filePath string) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("show").
|
||||
Arg(fmt.Sprintf("%s:%s", hash, filePath)).
|
||||
ToArgv()
|
||||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
// Revert reverts the selected commit by hash
|
||||
func (self *CommitCommands) Revert(hash string) error {
|
||||
cmdArgs := NewGitCmd("revert").Arg(hash).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) RevertMerge(hash string, parentNumber int) error {
|
||||
cmdArgs := NewGitCmd("revert").Arg(hash, "-m", fmt.Sprintf("%d", parentNumber)).
|
||||
// Revert reverts the selected commits by hash. If isMerge is true, we'll pass -m 1
|
||||
// to say we want to revert the first parent of the merge commit, which is the one
|
||||
// people want in 99.9% of cases. In current git versions we could unconditionally
|
||||
// pass -m 1 even for non-merge commits, but older versions of git choke on it.
|
||||
func (self *CommitCommands) Revert(hashes []string, isMerge bool) error {
|
||||
cmdArgs := NewGitCmd("revert").
|
||||
ArgIf(isMerge, "-m", "1").
|
||||
Arg(hashes...).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -31,10 +30,10 @@ type CommitLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
|
||||
getRebaseMode func() (enums.RebaseMode, error)
|
||||
readFile func(filename string) ([]byte, error)
|
||||
walkFiles func(root string, fn filepath.WalkFunc) error
|
||||
dotGitDir string
|
||||
getWorkingTreeState func() models.WorkingTreeState
|
||||
readFile func(filename string) ([]byte, error)
|
||||
walkFiles func(root string, fn filepath.WalkFunc) error
|
||||
dotGitDir string
|
||||
*GitCommon
|
||||
}
|
||||
|
||||
@@ -42,16 +41,16 @@ type CommitLoader struct {
|
||||
func NewCommitLoader(
|
||||
cmn *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
getRebaseMode func() (enums.RebaseMode, error),
|
||||
getWorkingTreeState func() models.WorkingTreeState,
|
||||
gitCommon *GitCommon,
|
||||
) *CommitLoader {
|
||||
return &CommitLoader{
|
||||
Common: cmn,
|
||||
cmd: cmd,
|
||||
getRebaseMode: getRebaseMode,
|
||||
readFile: os.ReadFile,
|
||||
walkFiles: filepath.Walk,
|
||||
GitCommon: gitCommon,
|
||||
Common: cmn,
|
||||
cmd: cmd,
|
||||
getWorkingTreeState: getWorkingTreeState,
|
||||
readFile: os.ReadFile,
|
||||
walkFiles: filepath.Walk,
|
||||
GitCommon: gitCommon,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,20 +66,19 @@ type GetCommitsOptions struct {
|
||||
// If non-empty, show divergence from this ref (left-right log)
|
||||
RefToShowDivergenceFrom string
|
||||
MainBranches *MainBranches
|
||||
HashPool *utils.StringPool
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
|
||||
commits := []*models.Commit{}
|
||||
var rebasingCommits []*models.Commit
|
||||
|
||||
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
|
||||
var err error
|
||||
rebasingCommits, err = self.MergeRebasingCommits(commits)
|
||||
commits, err = self.MergeRebasingCommits(opts.HashPool, commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits = append(commits, rebasingCommits...)
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
@@ -92,7 +90,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
|
||||
defer wg.Done()
|
||||
|
||||
logErr = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
|
||||
commit := self.extractCommitFromLine(line, opts.RefToShowDivergenceFrom != "")
|
||||
commit := self.extractCommitFromLine(opts.HashPool, line, opts.RefToShowDivergenceFrom != "")
|
||||
commits = append(commits, commit)
|
||||
return false, nil
|
||||
})
|
||||
@@ -124,10 +122,10 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
|
||||
}
|
||||
|
||||
for _, commit := range commits {
|
||||
if commit.Hash == firstPushedCommit {
|
||||
if commit.Hash() == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
if commit.Status != models.StatusRebasing {
|
||||
if !commit.IsTODO() {
|
||||
if passedFirstPushedCommit {
|
||||
commit.Status = models.StatusPushed
|
||||
} else {
|
||||
@@ -162,7 +160,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
|
||||
func (self *CommitLoader) MergeRebasingCommits(hashPool *utils.StringPool, commits []*models.Commit) ([]*models.Commit, error) {
|
||||
// chances are we have as many commits as last time so we'll set the capacity to be the old length
|
||||
result := make([]*models.Commit, 0, len(commits))
|
||||
for i, commit := range commits {
|
||||
@@ -172,24 +170,26 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
|
||||
}
|
||||
}
|
||||
|
||||
rebaseMode, err := self.getRebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
workingTreeState := self.getWorkingTreeState()
|
||||
addConflictedRebasingCommit := true
|
||||
if workingTreeState.CherryPicking || workingTreeState.Reverting {
|
||||
sequencerCommits, err := self.getHydratedSequencerCommits(hashPool, workingTreeState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(sequencerCommits, result...)
|
||||
addConflictedRebasingCommit = false
|
||||
}
|
||||
|
||||
if rebaseMode == enums.REBASE_MODE_NONE {
|
||||
// not in rebase mode so return original commits
|
||||
return result, nil
|
||||
if workingTreeState.Rebasing {
|
||||
rebasingCommits, err := self.getHydratedRebasingCommits(hashPool, addConflictedRebasingCommit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rebasingCommits) > 0 {
|
||||
result = append(rebasingCommits, result...)
|
||||
}
|
||||
}
|
||||
|
||||
rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rebasingCommits) > 0 {
|
||||
result = append(rebasingCommits, result...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
|
||||
// then puts them into a commit object
|
||||
// example input:
|
||||
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
|
||||
func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool) *models.Commit {
|
||||
func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line string, showDivergence bool) *models.Commit {
|
||||
split := strings.SplitN(line, "\x00", 8)
|
||||
|
||||
hash := split[0]
|
||||
@@ -235,7 +235,7 @@ func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool
|
||||
parents = strings.Split(parentHashes, " ")
|
||||
}
|
||||
|
||||
return &models.Commit{
|
||||
return models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: hash,
|
||||
Name: message,
|
||||
Tags: tags,
|
||||
@@ -245,18 +245,40 @@ func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool
|
||||
AuthorEmail: authorEmail,
|
||||
Parents: parents,
|
||||
Divergence: divergence,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
|
||||
commits := self.getRebasingCommits(rebaseMode)
|
||||
func (self *CommitLoader) getHydratedRebasingCommits(hashPool *utils.StringPool, addConflictingCommit bool) ([]*models.Commit, error) {
|
||||
todoFileHasShortHashes := self.version.IsOlderThan(2, 25, 2)
|
||||
return self.getHydratedTodoCommits(hashPool, self.getRebasingCommits(hashPool, addConflictingCommit), todoFileHasShortHashes)
|
||||
}
|
||||
|
||||
if len(commits) == 0 {
|
||||
func (self *CommitLoader) getHydratedSequencerCommits(hashPool *utils.StringPool, workingTreeState models.WorkingTreeState) ([]*models.Commit, error) {
|
||||
commits := self.getSequencerCommits(hashPool)
|
||||
if len(commits) > 0 {
|
||||
// If we have any commits in .git/sequencer/todo, then the last one of
|
||||
// those is the conflicting one.
|
||||
commits[len(commits)-1].Status = models.StatusConflicted
|
||||
} else {
|
||||
// For single-commit cherry-picks and reverts, git apparently doesn't
|
||||
// use the sequencer; in that case, CHERRY_PICK_HEAD or REVERT_HEAD is
|
||||
// our conflicting commit, so synthesize it here.
|
||||
conflicedCommit := self.getConflictedSequencerCommit(hashPool, workingTreeState)
|
||||
if conflicedCommit != nil {
|
||||
commits = append(commits, conflicedCommit)
|
||||
}
|
||||
}
|
||||
|
||||
return self.getHydratedTodoCommits(hashPool, commits, true)
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getHydratedTodoCommits(hashPool *utils.StringPool, todoCommits []*models.Commit, todoFileHasShortHashes bool) ([]*models.Commit, error) {
|
||||
if len(todoCommits) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commitHashes := lo.FilterMap(commits, func(commit *models.Commit, _ int) (string, bool) {
|
||||
return commit.Hash, commit.Hash != ""
|
||||
commitHashes := lo.FilterMap(todoCommits, func(commit *models.Commit, _ int) (string, bool) {
|
||||
return commit.Hash(), commit.Hash() != ""
|
||||
})
|
||||
|
||||
// note that we're not filtering these as we do non-rebasing commits just because
|
||||
@@ -271,15 +293,15 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
|
||||
|
||||
fullCommits := map[string]*models.Commit{}
|
||||
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
commit := self.extractCommitFromLine(line, false)
|
||||
fullCommits[commit.Hash] = commit
|
||||
commit := self.extractCommitFromLine(hashPool, line, false)
|
||||
fullCommits[commit.Hash()] = commit
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
findFullCommit := lo.Ternary(self.version.IsOlderThan(2, 25, 2),
|
||||
findFullCommit := lo.Ternary(todoFileHasShortHashes,
|
||||
func(hash string) *models.Commit {
|
||||
for s, c := range fullCommits {
|
||||
if strings.HasPrefix(s, hash) {
|
||||
@@ -292,11 +314,11 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
|
||||
return fullCommits[hash]
|
||||
})
|
||||
|
||||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
for _, rebasingCommit := range commits {
|
||||
if rebasingCommit.Hash == "" {
|
||||
hydratedCommits := make([]*models.Commit, 0, len(todoCommits))
|
||||
for _, rebasingCommit := range todoCommits {
|
||||
if rebasingCommit.Hash() == "" {
|
||||
hydratedCommits = append(hydratedCommits, rebasingCommit)
|
||||
} else if commit := findFullCommit(rebasingCommit.Hash); commit != nil {
|
||||
} else if commit := findFullCommit(rebasingCommit.Hash()); commit != nil {
|
||||
commit.Action = rebasingCommit.Action
|
||||
commit.Status = rebasingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
@@ -310,11 +332,7 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
|
||||
// git-rebase-todo example:
|
||||
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
|
||||
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
|
||||
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) []*models.Commit {
|
||||
if rebaseMode != enums.REBASE_MODE_INTERACTIVE {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getRebasingCommits(hashPool *utils.StringPool, addConflictingCommit bool) []*models.Commit {
|
||||
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()))
|
||||
@@ -332,13 +350,10 @@ func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) []*mod
|
||||
|
||||
// See if the current commit couldn't be applied because it conflicted; if
|
||||
// so, add a fake entry for it
|
||||
if conflictedCommitHash := self.getConflictedCommit(todos); conflictedCommitHash != "" {
|
||||
commits = append(commits, &models.Commit{
|
||||
Hash: conflictedCommitHash,
|
||||
Name: "",
|
||||
Status: models.StatusRebasing,
|
||||
Action: models.ActionConflict,
|
||||
})
|
||||
if addConflictingCommit {
|
||||
if conflictedCommit := self.getConflictedCommit(hashPool, todos); conflictedCommit != nil {
|
||||
commits = append(commits, conflictedCommit)
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range todos {
|
||||
@@ -350,47 +365,45 @@ func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) []*mod
|
||||
// Command does not have a commit associated, skip
|
||||
continue
|
||||
}
|
||||
commits = utils.Prepend(commits, &models.Commit{
|
||||
commits = utils.Prepend(commits, models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: t.Commit,
|
||||
Name: t.Msg,
|
||||
Status: models.StatusRebasing,
|
||||
Action: t.Command,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
return commits
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string {
|
||||
func (self *CommitLoader) getConflictedCommit(hashPool *utils.StringPool, todos []todo.Todo) *models.Commit {
|
||||
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 ""
|
||||
return nil
|
||||
}
|
||||
|
||||
doneTodos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
|
||||
if err != nil {
|
||||
self.Log.Error(fmt.Sprintf("error occurred while parsing rebase-merge/done file: %s", err.Error()))
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
amendFileExists := false
|
||||
if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend")); err == nil {
|
||||
amendFileExists = true
|
||||
}
|
||||
amendFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend"))
|
||||
messageFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/message"))
|
||||
|
||||
return self.getConflictedCommitImpl(todos, doneTodos, amendFileExists)
|
||||
return self.getConflictedCommitImpl(hashPool, todos, doneTodos, amendFileExists, messageFileExists)
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos []todo.Todo, amendFileExists bool) string {
|
||||
func (self *CommitLoader) getConflictedCommitImpl(hashPool *utils.StringPool, todos []todo.Todo, doneTodos []todo.Todo, amendFileExists bool, messageFileExists bool) *models.Commit {
|
||||
// Should never be possible, but just to be safe:
|
||||
if len(doneTodos) == 0 {
|
||||
self.Log.Error("no done entries in rebase-merge/done file")
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
lastTodo := doneTodos[len(doneTodos)-1]
|
||||
if lastTodo.Command == todo.Break || lastTodo.Command == todo.Exec || lastTodo.Command == todo.Reword {
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// In certain cases, git reschedules commands that failed. One example is if
|
||||
@@ -401,7 +414,7 @@ func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos [
|
||||
// same, the command was rescheduled.
|
||||
if len(doneTodos) > 0 && len(todos) > 0 && doneTodos[len(doneTodos)-1] == todos[0] {
|
||||
// Command was rescheduled, no need to display it
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// Older versions of git have a bug whereby, if a command is rescheduled,
|
||||
@@ -426,26 +439,99 @@ func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos [
|
||||
if len(doneTodos) >= 3 && len(todos) > 0 && doneTodos[len(doneTodos)-2] == todos[0] &&
|
||||
doneTodos[len(doneTodos)-1] == doneTodos[len(doneTodos)-3] {
|
||||
// Command was rescheduled, no need to display it
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
if lastTodo.Command == todo.Edit {
|
||||
if amendFileExists {
|
||||
// Special case for "edit": if the "amend" file exists, the "edit"
|
||||
// command was successful, otherwise it wasn't
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
if !messageFileExists {
|
||||
// As an additional check, see if the "message" file exists; if it
|
||||
// doesn't, it must be because a multi-commit cherry-pick or revert
|
||||
// was performed in the meantime, which deleted both the amend file
|
||||
// and the message file.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// I don't think this is ever possible, but again, just to be safe:
|
||||
if lastTodo.Commit == "" {
|
||||
self.Log.Error("last command in rebase-merge/done file doesn't have a commit")
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// Any other todo that has a commit associated with it must have failed with
|
||||
// a conflict, otherwise we wouldn't have stopped the rebase:
|
||||
return lastTodo.Commit
|
||||
return models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: lastTodo.Commit,
|
||||
Action: lastTodo.Command,
|
||||
Status: models.StatusConflicted,
|
||||
})
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getSequencerCommits(hashPool *utils.StringPool) []*models.Commit {
|
||||
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "sequencer/todo"))
|
||||
if err != nil {
|
||||
self.Log.Error(fmt.Sprintf("error occurred reading sequencer/todo: %s", err.Error()))
|
||||
// we assume an error means the file doesn't exist so we just return
|
||||
return nil
|
||||
}
|
||||
|
||||
commits := []*models.Commit{}
|
||||
|
||||
todos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
|
||||
if err != nil {
|
||||
self.Log.Error(fmt.Sprintf("error occurred while parsing sequencer/todo file: %s", err.Error()))
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, t := range todos {
|
||||
if t.Commit == "" {
|
||||
// Command does not have a commit associated, skip
|
||||
continue
|
||||
}
|
||||
commits = utils.Prepend(commits, models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: t.Commit,
|
||||
Name: t.Msg,
|
||||
Status: models.StatusCherryPickingOrReverting,
|
||||
Action: t.Command,
|
||||
}))
|
||||
}
|
||||
|
||||
return commits
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getConflictedSequencerCommit(hashPool *utils.StringPool, workingTreeState models.WorkingTreeState) *models.Commit {
|
||||
var shaFile string
|
||||
var action todo.TodoCommand
|
||||
if workingTreeState.CherryPicking {
|
||||
shaFile = "CHERRY_PICK_HEAD"
|
||||
action = todo.Pick
|
||||
} else if workingTreeState.Reverting {
|
||||
shaFile = "REVERT_HEAD"
|
||||
action = todo.Revert
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), shaFile))
|
||||
if err != nil {
|
||||
self.Log.Error(fmt.Sprintf("error occurred reading %s: %s", shaFile, err.Error()))
|
||||
// we assume an error means the file doesn't exist so we just return
|
||||
return nil
|
||||
}
|
||||
lines := strings.Split(string(bytesContent), "\n")
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
return models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: lines[0],
|
||||
Status: models.StatusConflicted,
|
||||
Action: action,
|
||||
})
|
||||
}
|
||||
|
||||
func setCommitMergedStatuses(ancestor string, commits []*models.Commit) {
|
||||
@@ -456,7 +542,7 @@ func setCommitMergedStatuses(ancestor string, commits []*models.Commit) {
|
||||
passedAncestor := false
|
||||
for i, commit := range commits {
|
||||
// some commits aren't really commits and don't have hashes, such as the update-ref todo
|
||||
if commit.Hash != "" && strings.HasPrefix(ancestor, commit.Hash) {
|
||||
if commit.Hash() != "" && strings.HasPrefix(ancestor, commit.Hash()) {
|
||||
passedAncestor = true
|
||||
}
|
||||
if commit.Status != models.StatusPushed && commit.Status != models.StatusUnpushed {
|
||||
@@ -497,7 +583,7 @@ func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
|
||||
}
|
||||
|
||||
// getLog gets the git log.
|
||||
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
|
||||
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj {
|
||||
gitLogOrder := self.AppState.GitLogOrder
|
||||
|
||||
refSpec := opts.RefName
|
||||
@@ -524,4 +610,4 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
|
||||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s`
|
||||
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s`
|
||||
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
"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"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stefanhaller/git-todo-parser/todo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -28,52 +29,48 @@ var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18d
|
||||
|
||||
func TestGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
expectedCommits []*models.Commit
|
||||
expectedError error
|
||||
logOrder string
|
||||
rebaseMode enums.RebaseMode
|
||||
opts GetCommitsOptions
|
||||
mainBranches []string
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
expectedCommitOpts []models.NewCommitOpts
|
||||
expectedError error
|
||||
logOrder string
|
||||
opts GetCommitsOptions
|
||||
mainBranches []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "should return no commits if there are none",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
|
||||
testName: "should return no commits if there are none",
|
||||
logOrder: "topo-order",
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
expectedCommitOpts: []models.NewCommitOpts{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should use proper upstream name for branch",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", RefForPushedStatus: "refs/heads/mybranch", IncludeRebaseCommits: false},
|
||||
testName: "should use proper upstream name for branch",
|
||||
logOrder: "topo-order",
|
||||
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", RefForPushedStatus: "refs/heads/mybranch", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "refs/heads/mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
expectedCommitOpts: []models.NewCommitOpts{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return commits if they are present",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
|
||||
mainBranches: []string{"master", "main", "develop"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
|
||||
// here it's testing which of the configured main branches have an upstream
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). // this one does
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead
|
||||
@@ -84,7 +81,7 @@ func TestGetCommits(t *testing.T) {
|
||||
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "refs/remotes/origin/master", "refs/remotes/origin/main"}, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
expectedCommitOpts: []models.NewCommitOpts{
|
||||
{
|
||||
Hash: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
@@ -203,14 +200,13 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
testName: "should not call merge-base for mainBranches if none exist",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
|
||||
mainBranches: []string{"master", "main"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
// here it's testing which of the configured main branches exist; neither does
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/master"}, "", errors.New("error")).
|
||||
@@ -219,7 +215,7 @@ func TestGetCommits(t *testing.T) {
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/main"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/main"}, "", errors.New("error")),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
expectedCommitOpts: []models.NewCommitOpts{
|
||||
{
|
||||
Hash: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
@@ -240,14 +236,13 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
testName: "should call merge-base for all main branches that exist",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
|
||||
mainBranches: []string{"master", "main", "develop", "1.0-hotfixes"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
// here it's testing which of the configured main branches exist
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")).
|
||||
@@ -258,7 +253,7 @@ func TestGetCommits(t *testing.T) {
|
||||
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "refs/remotes/origin/master", "refs/remotes/origin/develop", "refs/remotes/origin/1.0-hotfixes"}, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
expectedCommitOpts: []models.NewCommitOpts{
|
||||
{
|
||||
Hash: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
@@ -277,43 +272,41 @@ func TestGetCommits(t *testing.T) {
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should not specify order if `log.order` is `default`",
|
||||
logOrder: "default",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
|
||||
testName: "should not specify order if `log.order` is `default`",
|
||||
logOrder: "default",
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
expectedCommitOpts: []models.NewCommitOpts{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should set filter path",
|
||||
logOrder: "default",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", FilterPath: "src"},
|
||||
testName: "should set filter path",
|
||||
logOrder: "default",
|
||||
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", FilterPath: "src"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
expectedCommitOpts: []models.NewCommitOpts{},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
common := utils.NewDummyCommon()
|
||||
common := common.NewDummyCommon()
|
||||
common.AppState = &config.AppState{}
|
||||
common.AppState.GitLogOrder = scenario.logOrder
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(scenario.runner)
|
||||
|
||||
builder := &CommitLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
|
||||
dotGitDir: ".git",
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
getWorkingTreeState: func() models.WorkingTreeState { return models.WorkingTreeState{} },
|
||||
dotGitDir: ".git",
|
||||
readFile: func(filename string) ([]byte, error) {
|
||||
return []byte(""), nil
|
||||
},
|
||||
@@ -322,12 +315,17 @@ func TestGetCommits(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
hashPool := &utils.StringPool{}
|
||||
|
||||
common.UserConfig().Git.MainBranches = scenario.mainBranches
|
||||
opts := scenario.opts
|
||||
opts.MainBranches = NewMainBranches(common, cmd)
|
||||
opts.HashPool = hashPool
|
||||
commits, err := builder.GetCommits(opts)
|
||||
|
||||
assert.Equal(t, scenario.expectedCommits, commits)
|
||||
expectedCommits := lo.Map(scenario.expectedCommitOpts,
|
||||
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
|
||||
assert.Equal(t, expectedCommits, commits)
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
|
||||
scenario.runner.CheckForMissingCalls()
|
||||
@@ -336,19 +334,22 @@ func TestGetCommits(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
todos []todo.Todo
|
||||
doneTodos []todo.Todo
|
||||
amendFileExists bool
|
||||
expectedHash string
|
||||
testName string
|
||||
todos []todo.Todo
|
||||
doneTodos []todo.Todo
|
||||
amendFileExists bool
|
||||
messageFileExists bool
|
||||
expectedResult *models.Commit
|
||||
}{
|
||||
{
|
||||
testName: "no done todos",
|
||||
todos: []todo.Todo{},
|
||||
doneTodos: []todo.Todo{},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "common case (conflict)",
|
||||
@@ -364,7 +365,11 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "fa1afe1",
|
||||
expectedResult: models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: "fa1afe1",
|
||||
Action: todo.Pick,
|
||||
Status: models.StatusConflicted,
|
||||
}),
|
||||
},
|
||||
{
|
||||
testName: "last command was 'break'",
|
||||
@@ -373,7 +378,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
{Command: todo.Break},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "last command was 'exec'",
|
||||
@@ -385,7 +390,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "last command was 'reword'",
|
||||
@@ -394,7 +399,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
{Command: todo.Reword},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "'pick' was rescheduled",
|
||||
@@ -411,7 +416,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "'pick' was rescheduled, buggy git version",
|
||||
@@ -436,7 +441,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "conflicting 'pick' after 'exec'",
|
||||
@@ -461,7 +466,11 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "fa1afe1",
|
||||
expectedResult: models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: "fa1afe1",
|
||||
Action: todo.Pick,
|
||||
Status: models.StatusConflicted,
|
||||
}),
|
||||
},
|
||||
{
|
||||
testName: "'edit' with amend file",
|
||||
@@ -473,10 +482,10 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
},
|
||||
},
|
||||
amendFileExists: true,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "'edit' without amend file",
|
||||
testName: "'edit' without amend file but message file",
|
||||
todos: []todo.Todo{},
|
||||
doneTodos: []todo.Todo{
|
||||
{
|
||||
@@ -484,19 +493,37 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
Commit: "fa1afe1",
|
||||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "fa1afe1",
|
||||
amendFileExists: false,
|
||||
messageFileExists: true,
|
||||
expectedResult: models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: "fa1afe1",
|
||||
Action: todo.Edit,
|
||||
Status: models.StatusConflicted,
|
||||
}),
|
||||
},
|
||||
{
|
||||
testName: "'edit' without amend and without message file",
|
||||
todos: []todo.Todo{},
|
||||
doneTodos: []todo.Todo{
|
||||
{
|
||||
Command: todo.Edit,
|
||||
Commit: "fa1afe1",
|
||||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
messageFileExists: false,
|
||||
expectedResult: nil,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
common := utils.NewDummyCommon()
|
||||
common := common.NewDummyCommon()
|
||||
|
||||
builder := &CommitLoader{
|
||||
Common: common,
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(oscommands.NewFakeRunner(t)),
|
||||
getRebaseMode: func() (enums.RebaseMode, error) { return enums.REBASE_MODE_INTERACTIVE, nil },
|
||||
dotGitDir: ".git",
|
||||
Common: common,
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(oscommands.NewFakeRunner(t)),
|
||||
getWorkingTreeState: func() models.WorkingTreeState { return models.WorkingTreeState{Rebasing: true} },
|
||||
dotGitDir: ".git",
|
||||
readFile: func(filename string) ([]byte, error) {
|
||||
return []byte(""), nil
|
||||
},
|
||||
@@ -505,30 +532,30 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
hash := builder.getConflictedCommitImpl(scenario.todos, scenario.doneTodos, scenario.amendFileExists)
|
||||
assert.Equal(t, scenario.expectedHash, hash)
|
||||
hash := builder.getConflictedCommitImpl(hashPool, scenario.todos, scenario.doneTodos, scenario.amendFileExists, scenario.messageFileExists)
|
||||
assert.Equal(t, scenario.expectedResult, hash)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitLoader_setCommitMergedStatuses(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
commits []*models.Commit
|
||||
ancestor string
|
||||
expectedCommits []*models.Commit
|
||||
testName string
|
||||
commitOpts []models.NewCommitOpts
|
||||
ancestor string
|
||||
expectedCommitOpts []models.NewCommitOpts
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "basic",
|
||||
commits: []*models.Commit{
|
||||
commitOpts: []models.NewCommitOpts{
|
||||
{Hash: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
|
||||
{Hash: "67890", Name: "2", Action: models.ActionNone, Status: models.StatusPushed},
|
||||
{Hash: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusPushed},
|
||||
},
|
||||
ancestor: "67890",
|
||||
expectedCommits: []*models.Commit{
|
||||
expectedCommitOpts: []models.NewCommitOpts{
|
||||
{Hash: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
|
||||
{Hash: "67890", Name: "2", Action: models.ActionNone, Status: models.StatusMerged},
|
||||
{Hash: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusMerged},
|
||||
@@ -536,13 +563,13 @@ func TestCommitLoader_setCommitMergedStatuses(t *testing.T) {
|
||||
},
|
||||
{
|
||||
testName: "with update-ref",
|
||||
commits: []*models.Commit{
|
||||
commitOpts: []models.NewCommitOpts{
|
||||
{Hash: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
|
||||
{Hash: "", Name: "", Action: todo.UpdateRef, Status: models.StatusNone},
|
||||
{Hash: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusPushed},
|
||||
},
|
||||
ancestor: "deadbeef",
|
||||
expectedCommits: []*models.Commit{
|
||||
expectedCommitOpts: []models.NewCommitOpts{
|
||||
{Hash: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
|
||||
{Hash: "", Name: "", Action: todo.UpdateRef, Status: models.StatusNone},
|
||||
{Hash: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusPushed},
|
||||
@@ -552,9 +579,14 @@ func TestCommitLoader_setCommitMergedStatuses(t *testing.T) {
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
expectedCommits := scenario.commits
|
||||
setCommitMergedStatuses(scenario.ancestor, expectedCommits)
|
||||
assert.Equal(t, scenario.expectedCommits, expectedCommits)
|
||||
hashPool := &utils.StringPool{}
|
||||
|
||||
commits := lo.Map(scenario.commitOpts,
|
||||
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
|
||||
setCommitMergedStatuses(scenario.ancestor, commits)
|
||||
expectedCommits := lo.Map(scenario.expectedCommitOpts,
|
||||
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
|
||||
assert.Equal(t, expectedCommits, commits)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
@@ -32,7 +31,7 @@ func buildGitCommon(deps commonDeps) *GitCommon {
|
||||
|
||||
gitCommon.Common = deps.common
|
||||
if gitCommon.Common == nil {
|
||||
gitCommon.Common = utils.NewDummyCommonWithUserConfigAndAppState(deps.userConfig, deps.appState)
|
||||
gitCommon.Common = common.NewDummyCommonWithUserConfigAndAppState(deps.userConfig, deps.appState)
|
||||
}
|
||||
|
||||
if deps.fs != nil {
|
||||
|
||||
@@ -18,7 +18,7 @@ func NewDiffCommands(gitCommon *GitCommon) *DiffCommands {
|
||||
|
||||
// This is for generating diffs to be shown in the UI (e.g. rendering a range
|
||||
// diff to the main view). It uses a custom pager if one is configured.
|
||||
func (self *DiffCommands) DiffCmdObj(diffArgs []string) oscommands.ICmdObj {
|
||||
func (self *DiffCommands) DiffCmdObj(diffArgs []string) *oscommands.CmdObj {
|
||||
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
|
||||
useExtDiff := extDiffCmd != ""
|
||||
ignoreWhitespace := self.AppState.IgnoreWhitespaceInDiffView
|
||||
@@ -83,7 +83,7 @@ type DiffToolCmdOptions struct {
|
||||
Staged bool
|
||||
}
|
||||
|
||||
func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) oscommands.ICmdObj {
|
||||
func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) *oscommands.CmdObj {
|
||||
return self.cmd.New(NewGitCmd("difftool").
|
||||
Arg("--no-prompt").
|
||||
ArgIf(opts.IsDirectory, "--dir-diff").
|
||||
@@ -95,7 +95,7 @@ func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) oscommands
|
||||
ToArgv())
|
||||
}
|
||||
|
||||
func (self *DiffCommands) DiffIndexCmdObj(diffArgs ...string) oscommands.ICmdObj {
|
||||
func (self *DiffCommands) DiffIndexCmdObj(diffArgs ...string) *oscommands.CmdObj {
|
||||
return self.cmd.New(
|
||||
NewGitCmd("diff-index").
|
||||
Config("diff.noprefix=false").
|
||||
|
||||
@@ -86,7 +86,7 @@ func (self *FileCommands) GetEditCmdStr(filenames []string) (string, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
template, suspend := config.GetEditTemplate(&self.UserConfig().OS, self.guessDefaultEditor)
|
||||
template, suspend := config.GetEditTemplate(self.os.Platform.Shell, &self.UserConfig().OS, self.guessDefaultEditor)
|
||||
quotedFilenames := lo.Map(filenames, func(filename string, _ int) string { return self.cmd.Quote(filename) })
|
||||
|
||||
templateValues := map[string]string{
|
||||
@@ -105,7 +105,7 @@ func (self *FileCommands) GetEditAtLineCmdStr(filename string, lineNumber int) (
|
||||
}
|
||||
}
|
||||
|
||||
template, suspend := config.GetEditAtLineTemplate(&self.UserConfig().OS, self.guessDefaultEditor)
|
||||
template, suspend := config.GetEditAtLineTemplate(self.os.Platform.Shell, &self.UserConfig().OS, self.guessDefaultEditor)
|
||||
|
||||
templateValues := map[string]string{
|
||||
"filename": self.cmd.Quote(filename),
|
||||
@@ -124,7 +124,7 @@ func (self *FileCommands) GetEditAtLineAndWaitCmdStr(filename string, lineNumber
|
||||
}
|
||||
}
|
||||
|
||||
template := config.GetEditAtLineAndWaitTemplate(&self.UserConfig().OS, self.guessDefaultEditor)
|
||||
template := config.GetEditAtLineAndWaitTemplate(self.os.Platform.Shell, &self.UserConfig().OS, self.guessDefaultEditor)
|
||||
|
||||
templateValues := map[string]string{
|
||||
"filename": self.cmd.Quote(filename),
|
||||
@@ -136,7 +136,7 @@ func (self *FileCommands) GetEditAtLineAndWaitCmdStr(filename string, lineNumber
|
||||
}
|
||||
|
||||
func (self *FileCommands) GetOpenDirInEditorCmdStr(path string) (string, bool) {
|
||||
template, suspend := config.GetOpenDirInEditorTemplate(&self.UserConfig().OS, self.guessDefaultEditor)
|
||||
template, suspend := config.GetOpenDirInEditorTemplate(self.os.Platform.Shell, &self.UserConfig().OS, self.guessDefaultEditor)
|
||||
|
||||
templateValues := map[string]string{
|
||||
"dir": self.cmd.Quote(path),
|
||||
|
||||
@@ -24,7 +24,7 @@ func (self *FlowCommands) GitFlowEnabled() bool {
|
||||
return self.config.GetGitFlowPrefixes() != ""
|
||||
}
|
||||
|
||||
func (self *FlowCommands) FinishCmdObj(branchName string) (oscommands.ICmdObj, error) {
|
||||
func (self *FlowCommands) FinishCmdObj(branchName string) (*oscommands.CmdObj, error) {
|
||||
prefixes := self.config.GetGitFlowPrefixes()
|
||||
|
||||
// need to find out what kind of branch this is
|
||||
@@ -54,7 +54,7 @@ func (self *FlowCommands) FinishCmdObj(branchName string) (oscommands.ICmdObj, e
|
||||
return self.cmd.New(cmdArgs), nil
|
||||
}
|
||||
|
||||
func (self *FlowCommands) StartCmdObj(branchType string, name string) oscommands.ICmdObj {
|
||||
func (self *FlowCommands) StartCmdObj(branchType string, name string) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("flow").Arg(branchType, "start", name).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/stefanhaller/git-todo-parser/todo"
|
||||
)
|
||||
|
||||
@@ -157,13 +156,13 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
baseIndex := sourceCommitIdx + 1
|
||||
|
||||
changes := []daemon.ChangeTodoAction{
|
||||
{Hash: commits[sourceCommitIdx].Hash, NewAction: todo.Edit},
|
||||
{Hash: commits[destinationCommitIdx].Hash, NewAction: todo.Edit},
|
||||
{Hash: commits[sourceCommitIdx].Hash(), NewAction: todo.Edit},
|
||||
{Hash: commits[destinationCommitIdx].Hash(), NewAction: todo.Edit},
|
||||
}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
|
||||
err := self.rebase.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseHashOrRoot: commits[baseIndex].Hash,
|
||||
baseHashOrRoot: commits[baseIndex].Hash(),
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}).Run()
|
||||
@@ -219,7 +218,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
|
||||
func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, stash bool) error {
|
||||
if stash {
|
||||
if err := self.stash.Push(self.Tr.StashPrefix + commits[commitIdx].Hash); err != nil {
|
||||
if err := self.stash.Push(self.Tr.StashPrefix + commits[commitIdx].Hash()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -229,7 +228,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
|
||||
}
|
||||
|
||||
if err := self.ApplyCustomPatch(true, true); err != nil {
|
||||
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
||||
if self.status.WorkingTreeState().Rebasing {
|
||||
_ = self.rebase.AbortRebase()
|
||||
}
|
||||
return err
|
||||
@@ -253,7 +252,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
|
||||
self.rebase.onSuccessfulContinue = func() error {
|
||||
// add patches to index
|
||||
if err := self.ApplyPatch(patch, ApplyPatchOpts{Index: true, ThreeWay: true}); err != nil {
|
||||
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
||||
if self.status.WorkingTreeState().Rebasing {
|
||||
_ = self.rebase.AbortRebase()
|
||||
}
|
||||
return err
|
||||
@@ -315,6 +314,29 @@ func (self *PatchCommands) PullPatchIntoNewCommit(
|
||||
return self.rebase.ContinueRebase()
|
||||
}
|
||||
|
||||
func (self *PatchCommands) PullPatchIntoNewCommitBefore(
|
||||
commits []*models.Commit,
|
||||
commitIdx int,
|
||||
commitSummary string,
|
||||
commitDescription string,
|
||||
) error {
|
||||
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx+1, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.ApplyCustomPatch(false, false); err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.commit.CommitCmdObj(commitSummary, commitDescription, false).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.PatchBuilder.Reset()
|
||||
return self.rebase.ContinueRebase()
|
||||
}
|
||||
|
||||
// We have just applied a patch in reverse to discard it from a commit; if we
|
||||
// now try to apply the patch again to move it to a later commit, or to the
|
||||
// index, then this would conflict "with itself" in case the patch contained
|
||||
@@ -324,7 +346,7 @@ func (self *PatchCommands) diffHeadAgainstCommit(commit *models.Commit) (string,
|
||||
cmdArgs := NewGitCmd("diff").
|
||||
Config("diff.noprefix=false").
|
||||
Arg("--no-ext-diff").
|
||||
Arg("HEAD.." + commit.Hash).
|
||||
Arg("HEAD.." + commit.Hash()).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).RunWithOutput()
|
||||
|
||||
@@ -53,9 +53,9 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, su
|
||||
return self.ContinueRebase()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
|
||||
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (*oscommands.CmdObj, error) {
|
||||
changes := []daemon.ChangeTodoAction{{
|
||||
Hash: commits[index].Hash,
|
||||
Hash: commits[index].Hash(),
|
||||
NewAction: todo.Reword,
|
||||
}}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
@@ -80,7 +80,7 @@ func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, start, end
|
||||
|
||||
func (self *RebaseCommands) AddCommitCoAuthor(commits []*models.Commit, start, end int, value string) error {
|
||||
return self.GenericAmend(commits, start, end, func(commit *models.Commit) error {
|
||||
return self.commit.AddCoAuthor(commit.Hash, value)
|
||||
return self.commit.AddCoAuthor(commit.Hash(), value)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func (self *RebaseCommands) MoveCommitsDown(commits []*models.Commit, startIdx i
|
||||
baseHashOrRoot := getBaseHashOrRoot(commits, endIdx+2)
|
||||
|
||||
hashes := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) string {
|
||||
return commit.Hash
|
||||
return commit.Hash()
|
||||
})
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
@@ -127,7 +127,7 @@ func (self *RebaseCommands) MoveCommitsUp(commits []*models.Commit, startIdx int
|
||||
baseHashOrRoot := getBaseHashOrRoot(commits, endIdx+1)
|
||||
|
||||
hashes := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) string {
|
||||
return commit.Hash
|
||||
return commit.Hash()
|
||||
})
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
@@ -147,7 +147,7 @@ func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, startIdx
|
||||
|
||||
changes := lo.FilterMap(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) (daemon.ChangeTodoAction, bool) {
|
||||
return daemon.ChangeTodoAction{
|
||||
Hash: commit.Hash,
|
||||
Hash: commit.Hash(),
|
||||
NewAction: action,
|
||||
}, !commit.IsMerge()
|
||||
})
|
||||
@@ -209,7 +209,7 @@ type PrepareInteractiveRebaseCommandOpts struct {
|
||||
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
|
||||
// we tell git to run lazygit to edit the todo list, and we pass the client
|
||||
// lazygit instructions what to do with the todo file
|
||||
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
|
||||
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) *oscommands.CmdObj {
|
||||
ex := oscommands.GetLazygitPath()
|
||||
|
||||
cmdArgs := NewGitCmd("rebase").
|
||||
@@ -293,7 +293,7 @@ func (self *RebaseCommands) getHashOfLastCommitMade() (string, error) {
|
||||
func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) error {
|
||||
commit := commits[commitIndex]
|
||||
|
||||
if err := self.commit.CreateFixupCommit(commit.Hash); err != nil {
|
||||
if err := self.commit.CreateFixupCommit(commit.Hash()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) e
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseHashOrRoot: getBaseHashOrRoot(commits, commitIndex+1),
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Hash, fixupHash, true),
|
||||
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Hash(), fixupHash, true),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ func (self *RebaseCommands) MoveFixupCommitDown(commits []*models.Commit, target
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseHashOrRoot: getBaseHashOrRoot(commits, targetCommitIndex+1),
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewMoveFixupCommitDownInstruction(commits[targetCommitIndex].Hash, fixupHash, false),
|
||||
instruction: daemon.NewMoveFixupCommitDownInstruction(commits[targetCommitIndex].Hash(), fixupHash, false),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ func todoFromCommit(commit *models.Commit) utils.Todo {
|
||||
if commit.Action == todo.UpdateRef {
|
||||
return utils.Todo{Ref: commit.Name}
|
||||
} else {
|
||||
return utils.Todo{Hash: commit.Hash}
|
||||
return utils.Todo{Hash: commit.Hash()}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ func todoFromCommit(commit *models.Commit) utils.Todo {
|
||||
func (self *RebaseCommands) EditRebaseTodo(commits []*models.Commit, action todo.TodoCommand) error {
|
||||
commitsWithAction := lo.Map(commits, func(commit *models.Commit, _ int) utils.TodoChange {
|
||||
return utils.TodoChange{
|
||||
Hash: commit.Hash,
|
||||
Hash: commit.Hash(),
|
||||
NewAction: action,
|
||||
}
|
||||
})
|
||||
@@ -383,7 +383,7 @@ func (self *RebaseCommands) MoveTodosUp(commits []*models.Commit) error {
|
||||
|
||||
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
|
||||
func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) error {
|
||||
hashOrRoot := commit.Hash + "^"
|
||||
hashOrRoot := commit.Hash() + "^"
|
||||
if commit.IsFirstCommit() {
|
||||
hashOrRoot = "--root"
|
||||
}
|
||||
@@ -400,7 +400,19 @@ func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) er
|
||||
func (self *RebaseCommands) BeginInteractiveRebaseForCommit(
|
||||
commits []*models.Commit, commitIndex int, keepCommitsThatBecomeEmpty bool,
|
||||
) error {
|
||||
return self.BeginInteractiveRebaseForCommitRange(commits, commitIndex, commitIndex, keepCommitsThatBecomeEmpty)
|
||||
if commitIndex < len(commits) && commits[commitIndex].IsMerge() {
|
||||
if self.config.NeedsGpgSubprocessForCommit() {
|
||||
return errors.New(self.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseHashOrRoot: getBaseHashOrRoot(commits, commitIndex),
|
||||
instruction: daemon.NewInsertBreakInstruction(),
|
||||
keepCommitsThatBecomeEmpty: keepCommitsThatBecomeEmpty,
|
||||
}).Run()
|
||||
} else {
|
||||
return self.BeginInteractiveRebaseForCommitRange(commits, commitIndex, commitIndex, keepCommitsThatBecomeEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) BeginInteractiveRebaseForCommitRange(
|
||||
@@ -420,7 +432,7 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommitRange(
|
||||
changes := make([]daemon.ChangeTodoAction, 0, end-start)
|
||||
for commitIndex := end; commitIndex >= start; commitIndex-- {
|
||||
changes = append(changes, daemon.ChangeTodoAction{
|
||||
Hash: commits[commitIndex].Hash,
|
||||
Hash: commits[commitIndex].Hash(),
|
||||
NewAction: todo.Edit,
|
||||
})
|
||||
}
|
||||
@@ -446,7 +458,7 @@ func (self *RebaseCommands) RebaseBranchFromBaseCommit(targetBranchName string,
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
|
||||
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd(commandType).Arg("--" + command).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
@@ -485,7 +497,7 @@ func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, comma
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
|
||||
func (self *RebaseCommands) runSkipEditorCommand(cmdObj *oscommands.CmdObj) error {
|
||||
instruction := daemon.NewExitImmediatelyInstruction()
|
||||
lazyGitPath := oscommands.GetLazygitPath()
|
||||
return cmdObj.
|
||||
@@ -533,41 +545,21 @@ func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, comm
|
||||
|
||||
// CherryPickCommits begins an interactive rebase with the given hashes being cherry picked onto HEAD
|
||||
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
|
||||
commitLines := lo.Map(commits, func(commit *models.Commit, _ int) string {
|
||||
return fmt.Sprintf("%s %s", utils.ShortHash(commit.Hash), commit.Name)
|
||||
})
|
||||
msg := utils.ResolvePlaceholderString(
|
||||
self.Tr.Log.CherryPickCommits,
|
||||
map[string]string{
|
||||
"commitLines": strings.Join(commitLines, "\n"),
|
||||
},
|
||||
)
|
||||
self.os.LogCommand(msg, false)
|
||||
hasMergeCommit := lo.SomeBy(commits, func(c *models.Commit) bool { return c.IsMerge() })
|
||||
cmdArgs := NewGitCmd("cherry-pick").
|
||||
Arg("--allow-empty").
|
||||
ArgIf(self.version.IsAtLeast(2, 45, 0), "--empty=keep", "--keep-redundant-commits").
|
||||
ArgIf(hasMergeCommit, "-m1").
|
||||
Arg(lo.Reverse(lo.Map(commits, func(c *models.Commit, _ int) string { return c.Hash() }))...).
|
||||
ToArgv()
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseHashOrRoot: "HEAD",
|
||||
instruction: daemon.NewCherryPickCommitsInstruction(commits),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
// CherryPickCommitsDuringRebase simply prepends the given commits to the existing git-rebase-todo file
|
||||
func (self *RebaseCommands) CherryPickCommitsDuringRebase(commits []*models.Commit) error {
|
||||
todoLines := lo.Map(commits, func(commit *models.Commit, _ int) daemon.TodoLine {
|
||||
return daemon.TodoLine{
|
||||
Action: "pick",
|
||||
Commit: commit,
|
||||
}
|
||||
})
|
||||
|
||||
todo := daemon.TodoLinesToString(todoLines)
|
||||
filePath := filepath.Join(self.repoPaths.worktreeGitDirPath, "rebase-merge/git-rebase-todo")
|
||||
return utils.PrependStrToTodoFile(filePath, []byte(todo))
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) DropMergeCommit(commits []*models.Commit, commitIndex int) error {
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseHashOrRoot: getBaseHashOrRoot(commits, commitIndex+1),
|
||||
instruction: daemon.NewDropMergeCommitInstruction(commits[commitIndex].Hash),
|
||||
instruction: daemon.NewDropMergeCommitInstruction(commits[commitIndex].Hash()),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
@@ -579,7 +571,7 @@ func getBaseHashOrRoot(commits []*models.Commit, index int) string {
|
||||
// be starting a rebase from 300 commits ago (which is the original commit limit
|
||||
// at time of writing)
|
||||
if index < len(commits) {
|
||||
return commits[index].Hash
|
||||
return commits[index].Hash()
|
||||
} else {
|
||||
return "--root"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -68,7 +69,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("matches editor env var", func(cmdObj oscommands.ICmdObj) bool {
|
||||
runner := oscommands.NewFakeRunner(t).ExpectFunc("matches editor env var", func(cmdObj *oscommands.CmdObj) bool {
|
||||
assert.EqualValues(t, cmdArgs, cmdObj.Args())
|
||||
envVars := cmdObj.GetEnvVars()
|
||||
for _, regexStr := range []string{
|
||||
@@ -97,7 +98,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
gitConfigMockResponses map[string]string
|
||||
commits []*models.Commit
|
||||
commitOpts []models.NewCommitOpts
|
||||
commitIndex int
|
||||
fileName []string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
@@ -108,7 +109,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
|
||||
{
|
||||
testName: "returns error when index outside of range of commits",
|
||||
gitConfigMockResponses: nil,
|
||||
commits: []*models.Commit{},
|
||||
commitOpts: []models.NewCommitOpts{},
|
||||
commitIndex: 0,
|
||||
fileName: []string{"test999.txt"},
|
||||
runner: oscommands.NewFakeRunner(t),
|
||||
@@ -119,7 +120,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
|
||||
{
|
||||
testName: "returns error when using gpg",
|
||||
gitConfigMockResponses: map[string]string{"commit.gpgSign": "true"},
|
||||
commits: []*models.Commit{{Name: "commit", Hash: "123456"}},
|
||||
commitOpts: []models.NewCommitOpts{{Name: "commit", Hash: "123456"}},
|
||||
commitIndex: 0,
|
||||
fileName: []string{"test999.txt"},
|
||||
runner: oscommands.NewFakeRunner(t),
|
||||
@@ -130,7 +131,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
|
||||
{
|
||||
testName: "checks out file if it already existed",
|
||||
gitConfigMockResponses: nil,
|
||||
commits: []*models.Commit{
|
||||
commitOpts: []models.NewCommitOpts{
|
||||
{Name: "commit", Hash: "123456"},
|
||||
{Name: "commit2", Hash: "abcdef"},
|
||||
},
|
||||
@@ -158,7 +159,11 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
|
||||
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
|
||||
})
|
||||
|
||||
s.test(instance.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
|
||||
hashPool := &utils.StringPool{}
|
||||
commits := lo.Map(s.commitOpts,
|
||||
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
|
||||
|
||||
s.test(instance.DiscardOldFileChanges(commits, s.commitIndex, s.fileName))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type ReflogCommitLoader struct {
|
||||
@@ -23,14 +24,14 @@ func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder)
|
||||
|
||||
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
|
||||
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
|
||||
func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string, filterAuthor string) ([]*models.Commit, bool, error) {
|
||||
func (self *ReflogCommitLoader) GetReflogCommits(hashPool *utils.StringPool, lastReflogCommit *models.Commit, filterPath string, filterAuthor string) ([]*models.Commit, bool, error) {
|
||||
commits := make([]*models.Commit, 0)
|
||||
|
||||
cmdArgs := NewGitCmd("log").
|
||||
Config("log.showSignature=false").
|
||||
Arg("-g").
|
||||
Arg("--abbrev=40").
|
||||
Arg("--format=%h%x00%ct%x00%gs%x00%p").
|
||||
Arg("--format=%h%x00%ct%x00%gs%x00%P").
|
||||
ArgIf(filterAuthor != "", "--author="+filterAuthor).
|
||||
ArgIf(filterPath != "", "--follow", "--", filterPath).
|
||||
ToArgv()
|
||||
@@ -39,7 +40,7 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
|
||||
|
||||
onlyObtainedNewReflogCommits := false
|
||||
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
commit, ok := self.parseLine(line)
|
||||
commit, ok := self.parseLine(hashPool, line)
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
@@ -65,10 +66,10 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
|
||||
}
|
||||
|
||||
func (self *ReflogCommitLoader) sameReflogCommit(a *models.Commit, b *models.Commit) bool {
|
||||
return a.Hash == b.Hash && a.UnixTimestamp == b.UnixTimestamp && a.Name == b.Name
|
||||
return a.Hash() == b.Hash() && a.UnixTimestamp == b.UnixTimestamp && a.Name == b.Name
|
||||
}
|
||||
|
||||
func (self *ReflogCommitLoader) parseLine(line string) (*models.Commit, bool) {
|
||||
func (self *ReflogCommitLoader) parseLine(hashPool *utils.StringPool, line string) (*models.Commit, bool) {
|
||||
fields := strings.SplitN(line, "\x00", 4)
|
||||
if len(fields) <= 3 {
|
||||
return nil, false
|
||||
@@ -82,11 +83,11 @@ func (self *ReflogCommitLoader) parseLine(line string) (*models.Commit, bool) {
|
||||
parents = strings.Split(parentHashes, " ")
|
||||
}
|
||||
|
||||
return &models.Commit{
|
||||
return models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: fields[0],
|
||||
Name: fields[2],
|
||||
UnixTimestamp: int64(unixTimestamp),
|
||||
Status: models.StatusReflog,
|
||||
Parents: parents,
|
||||
}, true
|
||||
}), true
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
|
||||
"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/sanity-io/litter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -26,29 +28,31 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
lastReflogCommit *models.Commit
|
||||
filterPath string
|
||||
filterAuthor string
|
||||
expectedCommits []*models.Commit
|
||||
expectedCommitOpts []models.NewCommitOpts
|
||||
expectedOnlyObtainedNew bool
|
||||
expectedError error
|
||||
}
|
||||
|
||||
hashPool := &utils.StringPool{}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "no reflog entries",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, "", nil),
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P"}, "", nil),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedCommitOpts: []models.NewCommitOpts{},
|
||||
expectedOnlyObtainedNew: false,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "some reflog entries",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, reflogOutput, nil),
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P"}, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
expectedCommits: []*models.Commit{
|
||||
expectedCommitOpts: []models.NewCommitOpts{
|
||||
{
|
||||
Hash: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
@@ -91,16 +95,16 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
testName: "some reflog entries where last commit is given",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, reflogOutput, nil),
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P"}, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: &models.Commit{
|
||||
lastReflogCommit: models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
expectedCommits: []*models.Commit{
|
||||
}),
|
||||
expectedCommitOpts: []models.NewCommitOpts{
|
||||
{
|
||||
Hash: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
@@ -115,17 +119,17 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
testName: "when passing filterPath",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p", "--follow", "--", "path"}, reflogOutput, nil),
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P", "--follow", "--", "path"}, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: &models.Commit{
|
||||
lastReflogCommit: models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
}),
|
||||
filterPath: "path",
|
||||
expectedCommits: []*models.Commit{
|
||||
expectedCommitOpts: []models.NewCommitOpts{
|
||||
{
|
||||
Hash: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
@@ -140,17 +144,17 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
testName: "when passing filterAuthor",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p", "--author=John Doe <john@doe.com>"}, reflogOutput, nil),
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P", "--author=John Doe <john@doe.com>"}, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: &models.Commit{
|
||||
lastReflogCommit: models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
}),
|
||||
filterAuthor: "John Doe <john@doe.com>",
|
||||
expectedCommits: []*models.Commit{
|
||||
expectedCommitOpts: []models.NewCommitOpts{
|
||||
{
|
||||
Hash: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
@@ -165,11 +169,11 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
testName: "when command returns error",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, "", errors.New("haha")),
|
||||
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P"}, "", errors.New("haha")),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
filterPath: "",
|
||||
expectedCommits: nil,
|
||||
expectedCommitOpts: nil,
|
||||
expectedOnlyObtainedNew: false,
|
||||
expectedError: errors.New("haha"),
|
||||
},
|
||||
@@ -178,15 +182,20 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
builder := &ReflogCommitLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
Common: common.NewDummyCommon(),
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
}
|
||||
|
||||
commits, onlyObtainednew, err := builder.GetReflogCommits(scenario.lastReflogCommit, scenario.filterPath, scenario.filterAuthor)
|
||||
commits, onlyObtainednew, err := builder.GetReflogCommits(hashPool, scenario.lastReflogCommit, scenario.filterPath, scenario.filterAuthor)
|
||||
assert.Equal(t, scenario.expectedOnlyObtainedNew, onlyObtainednew)
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
t.Logf("actual commits: \n%s", litter.Sdump(commits))
|
||||
assert.Equal(t, scenario.expectedCommits, commits)
|
||||
var expectedCommits []*models.Commit
|
||||
if scenario.expectedCommitOpts != nil {
|
||||
expectedCommits = lo.Map(scenario.expectedCommitOpts,
|
||||
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
|
||||
}
|
||||
assert.Equal(t, expectedCommits, commits)
|
||||
|
||||
scenario.runner.CheckForMissingCalls()
|
||||
})
|
||||
|
||||
@@ -80,7 +80,7 @@ func (self *StashCommands) Hash(index int) (string, error) {
|
||||
return strings.Trim(hash, "\r\n"), err
|
||||
}
|
||||
|
||||
func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
|
||||
func (self *StashCommands) ShowStashEntryCmdObj(index int) *oscommands.CmdObj {
|
||||
// "-u" is the same as "--include-untracked", but the latter fails in older git versions for some reason
|
||||
cmdArgs := NewGitCmd("stash").Arg("show").
|
||||
Arg("-p").
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestGetStashEntries(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
|
||||
|
||||
loader := NewStashLoader(utils.NewDummyCommon(), cmd)
|
||||
loader := NewStashLoader(common.NewDummyCommon(), cmd)
|
||||
|
||||
assert.EqualValues(t, s.expectedStashEntries, loader.GetStashEntries(s.filterPath))
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
type StatusCommands struct {
|
||||
@@ -20,50 +20,68 @@ func NewStatusCommands(
|
||||
}
|
||||
}
|
||||
|
||||
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
|
||||
// and "interactive" for interactive rebase
|
||||
func (self *StatusCommands) RebaseMode() (enums.RebaseMode, error) {
|
||||
ok, err := self.IsInNormalRebase()
|
||||
if err == nil && ok {
|
||||
return enums.REBASE_MODE_NORMAL, nil
|
||||
}
|
||||
ok, err = self.IsInInteractiveRebase()
|
||||
if err == nil && ok {
|
||||
return enums.REBASE_MODE_INTERACTIVE, err
|
||||
}
|
||||
|
||||
return enums.REBASE_MODE_NONE, err
|
||||
}
|
||||
|
||||
func (self *StatusCommands) WorkingTreeState() enums.RebaseMode {
|
||||
rebaseMode, _ := self.RebaseMode()
|
||||
if rebaseMode != enums.REBASE_MODE_NONE {
|
||||
return enums.REBASE_MODE_REBASING
|
||||
}
|
||||
merging, _ := self.IsInMergeState()
|
||||
if merging {
|
||||
return enums.REBASE_MODE_MERGING
|
||||
}
|
||||
return enums.REBASE_MODE_NONE
|
||||
func (self *StatusCommands) WorkingTreeState() models.WorkingTreeState {
|
||||
result := models.WorkingTreeState{}
|
||||
result.Rebasing, _ = self.IsInRebase()
|
||||
result.Merging, _ = self.IsInMergeState()
|
||||
result.CherryPicking, _ = self.IsInCherryPick()
|
||||
result.Reverting, _ = self.IsInRevert()
|
||||
return result
|
||||
}
|
||||
|
||||
func (self *StatusCommands) IsBareRepo() bool {
|
||||
return self.repoPaths.isBareRepo
|
||||
}
|
||||
|
||||
func (self *StatusCommands) IsInNormalRebase() (bool, error) {
|
||||
func (self *StatusCommands) IsInRebase() (bool, error) {
|
||||
exists, err := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge"))
|
||||
if err == nil && exists {
|
||||
return true, nil
|
||||
}
|
||||
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply"))
|
||||
}
|
||||
|
||||
func (self *StatusCommands) IsInInteractiveRebase() (bool, error) {
|
||||
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge"))
|
||||
}
|
||||
|
||||
// IsInMergeState states whether we are still mid-merge
|
||||
func (self *StatusCommands) IsInMergeState() (bool, error) {
|
||||
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "MERGE_HEAD"))
|
||||
}
|
||||
|
||||
func (self *StatusCommands) IsInCherryPick() (bool, error) {
|
||||
exists, err := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "CHERRY_PICK_HEAD"))
|
||||
if err != nil || !exists {
|
||||
return exists, err
|
||||
}
|
||||
// Sometimes, CHERRY_PICK_HEAD is present during rebases even if no
|
||||
// cherry-pick is in progress. I suppose this is because rebase used to be
|
||||
// implemented as a series of cherry-picks, so this could be remnants of
|
||||
// code that is shared between cherry-pick and rebase, or something. The way
|
||||
// to tell if this is the case is to check for the presence of the
|
||||
// stopped-sha file, which records the sha of the last pick that was
|
||||
// executed before the rebase stopped, and seeing if the sha in that file is
|
||||
// the same as the one in CHERRY_PICK_HEAD.
|
||||
cherryPickHead, err := os.ReadFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "CHERRY_PICK_HEAD"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
stoppedSha, err := os.ReadFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge", "stopped-sha"))
|
||||
if err != nil {
|
||||
// If we get an error we assume the file doesn't exist
|
||||
return true, nil
|
||||
}
|
||||
cherryPickHeadStr := strings.TrimSpace(string(cherryPickHead))
|
||||
stoppedShaStr := strings.TrimSpace(string(stoppedSha))
|
||||
// Need to use HasPrefix here because the cherry-pick HEAD is a full sha1,
|
||||
// but stopped-sha is an abbreviated sha1
|
||||
if strings.HasPrefix(cherryPickHeadStr, stoppedShaStr) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (self *StatusCommands) IsInRevert() (bool, error) {
|
||||
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "REVERT_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 {
|
||||
|
||||
@@ -237,35 +237,35 @@ func (self *SubmoduleCommands) Update(path string) error {
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj {
|
||||
func (self *SubmoduleCommands) BulkInitCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("submodule").Arg("init").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj {
|
||||
func (self *SubmoduleCommands) BulkUpdateCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("submodule").Arg("update").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj {
|
||||
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("submodule").Arg("update", "--force").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) BulkUpdateRecursivelyCmdObj() oscommands.ICmdObj {
|
||||
func (self *SubmoduleCommands) BulkUpdateRecursivelyCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("submodule").Arg("update", "--init", "--recursive").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj {
|
||||
func (self *SubmoduleCommands) BulkDeinitCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("submodule").Arg("deinit", "--all", "--force").
|
||||
ToArgv()
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ type PushOpts struct {
|
||||
SetUpstream bool
|
||||
}
|
||||
|
||||
func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (oscommands.ICmdObj, error) {
|
||||
func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (*oscommands.CmdObj, error) {
|
||||
if opts.UpstreamBranch != "" && opts.UpstreamRemote == "" {
|
||||
return nil, errors.New(self.Tr.MustSpecifyOriginError)
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func (self *SyncCommands) fetchCommandBuilder(fetchAll bool) *GitCommandBuilder
|
||||
ArgIf(self.version.IsAtLeast(2, 29, 0), "--no-write-fetch-head")
|
||||
}
|
||||
|
||||
func (self *SyncCommands) FetchCmdObj(task gocui.Task) oscommands.ICmdObj {
|
||||
func (self *SyncCommands) FetchCmdObj(task gocui.Task) *oscommands.CmdObj {
|
||||
cmdArgs := self.fetchCommandBuilder(self.UserConfig().Git.FetchAll).ToArgv()
|
||||
|
||||
cmdObj := self.cmd.New(cmdArgs)
|
||||
@@ -74,7 +74,7 @@ func (self *SyncCommands) Fetch(task gocui.Task) error {
|
||||
return self.FetchCmdObj(task).Run()
|
||||
}
|
||||
|
||||
func (self *SyncCommands) FetchBackgroundCmdObj() oscommands.ICmdObj {
|
||||
func (self *SyncCommands) FetchBackgroundCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := self.fetchCommandBuilder(self.UserConfig().Git.FetchAll).ToArgv()
|
||||
|
||||
cmdObj := self.cmd.New(cmdArgs)
|
||||
|
||||
@@ -12,14 +12,14 @@ func TestSyncPush(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
opts PushOpts
|
||||
test func(oscommands.ICmdObj, error)
|
||||
test func(*oscommands.CmdObj, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Push with force disabled",
|
||||
opts: PushOpts{ForceWithLease: false},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -27,7 +27,7 @@ func TestSyncPush(t *testing.T) {
|
||||
{
|
||||
testName: "Push with force-with-lease enabled",
|
||||
opts: PushOpts{ForceWithLease: true},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -35,7 +35,7 @@ func TestSyncPush(t *testing.T) {
|
||||
{
|
||||
testName: "Push with force enabled",
|
||||
opts: PushOpts{Force: true},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -48,7 +48,7 @@ func TestSyncPush(t *testing.T) {
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "origin", "refs/heads/master:master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -62,7 +62,7 @@ func TestSyncPush(t *testing.T) {
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--set-upstream", "origin", "refs/heads/master-local:master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -76,7 +76,7 @@ func TestSyncPush(t *testing.T) {
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease", "--set-upstream", "origin", "refs/heads/master:master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -89,7 +89,7 @@ func TestSyncPush(t *testing.T) {
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
|
||||
},
|
||||
@@ -109,14 +109,14 @@ func TestSyncFetch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
fetchAllConfig bool
|
||||
test func(oscommands.ICmdObj)
|
||||
test func(*oscommands.CmdObj)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Fetch in foreground (all=false)",
|
||||
fetchAllConfig: false,
|
||||
test: func(cmdObj oscommands.ICmdObj) {
|
||||
test: func(cmdObj *oscommands.CmdObj) {
|
||||
assert.True(t, cmdObj.ShouldLog())
|
||||
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.PROMPT)
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch"})
|
||||
@@ -125,7 +125,7 @@ func TestSyncFetch(t *testing.T) {
|
||||
{
|
||||
testName: "Fetch in foreground (all=true)",
|
||||
fetchAllConfig: true,
|
||||
test: func(cmdObj oscommands.ICmdObj) {
|
||||
test: func(cmdObj *oscommands.CmdObj) {
|
||||
assert.True(t, cmdObj.ShouldLog())
|
||||
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.PROMPT)
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch", "--all"})
|
||||
@@ -147,14 +147,14 @@ func TestSyncFetchBackground(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
fetchAllConfig bool
|
||||
test func(oscommands.ICmdObj)
|
||||
test func(*oscommands.CmdObj)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Fetch in background (all=false)",
|
||||
fetchAllConfig: false,
|
||||
test: func(cmdObj oscommands.ICmdObj) {
|
||||
test: func(cmdObj *oscommands.CmdObj) {
|
||||
assert.False(t, cmdObj.ShouldLog())
|
||||
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.FAIL)
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch"})
|
||||
@@ -163,7 +163,7 @@ func TestSyncFetchBackground(t *testing.T) {
|
||||
{
|
||||
testName: "Fetch in background (all=true)",
|
||||
fetchAllConfig: true,
|
||||
test: func(cmdObj oscommands.ICmdObj) {
|
||||
test: func(cmdObj *oscommands.CmdObj) {
|
||||
assert.False(t, cmdObj.ShouldLog())
|
||||
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.FAIL)
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch", "--all"})
|
||||
|
||||
@@ -15,7 +15,7 @@ func NewTagCommands(gitCommon *GitCommon) *TagCommands {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagCommands) CreateLightweightObj(tagName string, ref string, force bool) oscommands.ICmdObj {
|
||||
func (self *TagCommands) CreateLightweightObj(tagName string, ref string, force bool) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("tag").
|
||||
ArgIf(force, "--force").
|
||||
Arg("--", tagName).
|
||||
@@ -25,7 +25,7 @@ func (self *TagCommands) CreateLightweightObj(tagName string, ref string, force
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *TagCommands) CreateAnnotatedObj(tagName, ref, msg string, force bool) oscommands.ICmdObj {
|
||||
func (self *TagCommands) CreateAnnotatedObj(tagName, ref, msg string, force bool) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("tag").Arg(tagName).
|
||||
ArgIf(force, "--force").
|
||||
ArgIf(len(ref) > 0, ref).
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestGetTags(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
loader := &TagLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
Common: common.NewDummyCommon(),
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func NewWorkingTreeCommands(
|
||||
}
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj {
|
||||
func (self *WorkingTreeCommands) OpenMergeToolCmdObj() *oscommands.CmdObj {
|
||||
return self.cmd.New(NewGitCmd("mergetool").ToArgv())
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool,
|
||||
return s
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool) oscommands.ICmdObj {
|
||||
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool) *oscommands.CmdObj {
|
||||
colorArg := self.UserConfig().Git.Paging.ColorArg
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
@@ -293,7 +293,7 @@ func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bo
|
||||
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) *oscommands.CmdObj {
|
||||
contextSize := self.AppState.DiffContextSize
|
||||
|
||||
colorArg := self.UserConfig().Git.Paging.ColorArg
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stefanhaller/git-todo-parser/todo"
|
||||
)
|
||||
|
||||
// Special commit hash for empty tree object
|
||||
const EmptyTreeCommitHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
||||
|
||||
type CommitStatus int
|
||||
type CommitStatus uint8
|
||||
|
||||
const (
|
||||
StatusNone CommitStatus = iota
|
||||
@@ -18,7 +19,8 @@ const (
|
||||
StatusPushed
|
||||
StatusMerged
|
||||
StatusRebasing
|
||||
StatusSelected
|
||||
StatusCherryPickingOrReverting
|
||||
StatusConflicted
|
||||
StatusReflog
|
||||
)
|
||||
|
||||
@@ -26,11 +28,9 @@ const (
|
||||
// Conveniently for us, the todo package starts the enum at 1, and given
|
||||
// that it doesn't have a "none" value, we're setting ours to 0
|
||||
ActionNone todo.TodoCommand = 0
|
||||
// "Comment" is the last one of the todo package's enum entries
|
||||
ActionConflict = todo.Comment + 1
|
||||
)
|
||||
|
||||
type Divergence int
|
||||
type Divergence uint8
|
||||
|
||||
// For a divergence log (left/right comparison of two refs) this is set to
|
||||
// either DivergenceLeft or DivergenceRight for each commit; for normal
|
||||
@@ -43,35 +43,74 @@ const (
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Hash string
|
||||
hash *string
|
||||
Name string
|
||||
Status CommitStatus
|
||||
Action todo.TodoCommand
|
||||
Tags []string
|
||||
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
|
||||
AuthorName string // something like 'Jesse Duffield'
|
||||
AuthorEmail string // something like 'jessedduffield@gmail.com'
|
||||
UnixTimestamp int64
|
||||
Divergence Divergence // set to DivergenceNone unless we are showing the divergence view
|
||||
|
||||
// Hashes of parent commits (will be multiple if it's a merge commit)
|
||||
Parents []string
|
||||
parents []*string
|
||||
|
||||
Status CommitStatus
|
||||
Action todo.TodoCommand
|
||||
Divergence Divergence // set to DivergenceNone unless we are showing the divergence view
|
||||
}
|
||||
|
||||
type NewCommitOpts struct {
|
||||
Hash string
|
||||
Name string
|
||||
Status CommitStatus
|
||||
Action todo.TodoCommand
|
||||
Tags []string
|
||||
ExtraInfo string
|
||||
AuthorName string
|
||||
AuthorEmail string
|
||||
UnixTimestamp int64
|
||||
Divergence Divergence
|
||||
Parents []string
|
||||
}
|
||||
|
||||
func NewCommit(hashPool *utils.StringPool, opts NewCommitOpts) *Commit {
|
||||
return &Commit{
|
||||
hash: hashPool.Add(opts.Hash),
|
||||
Name: opts.Name,
|
||||
Status: opts.Status,
|
||||
Action: opts.Action,
|
||||
Tags: opts.Tags,
|
||||
ExtraInfo: opts.ExtraInfo,
|
||||
AuthorName: opts.AuthorName,
|
||||
AuthorEmail: opts.AuthorEmail,
|
||||
UnixTimestamp: opts.UnixTimestamp,
|
||||
Divergence: opts.Divergence,
|
||||
parents: lo.Map(opts.Parents, func(s string, _ int) *string { return hashPool.Add(s) }),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commit) Hash() string {
|
||||
return *c.hash
|
||||
}
|
||||
|
||||
func (c *Commit) HashPtr() *string {
|
||||
return c.hash
|
||||
}
|
||||
|
||||
func (c *Commit) ShortHash() string {
|
||||
return utils.ShortHash(c.Hash)
|
||||
return utils.ShortHash(c.Hash())
|
||||
}
|
||||
|
||||
func (c *Commit) FullRefName() string {
|
||||
return c.Hash
|
||||
return c.Hash()
|
||||
}
|
||||
|
||||
func (c *Commit) RefName() string {
|
||||
return c.Hash
|
||||
return c.Hash()
|
||||
}
|
||||
|
||||
func (c *Commit) ShortRefName() string {
|
||||
return c.Hash[:7]
|
||||
return c.Hash()[:7]
|
||||
}
|
||||
|
||||
func (c *Commit) ParentRefName() string {
|
||||
@@ -81,8 +120,16 @@ func (c *Commit) ParentRefName() string {
|
||||
return c.RefName() + "^"
|
||||
}
|
||||
|
||||
func (c *Commit) Parents() []string {
|
||||
return lo.Map(c.parents, func(s *string, _ int) string { return *s })
|
||||
}
|
||||
|
||||
func (c *Commit) ParentPtrs() []*string {
|
||||
return c.parents
|
||||
}
|
||||
|
||||
func (c *Commit) IsFirstCommit() bool {
|
||||
return len(c.Parents) == 0
|
||||
return len(c.parents) == 0
|
||||
}
|
||||
|
||||
func (c *Commit) ID() string {
|
||||
@@ -90,11 +137,11 @@ func (c *Commit) ID() string {
|
||||
}
|
||||
|
||||
func (c *Commit) Description() string {
|
||||
return fmt.Sprintf("%s %s", c.Hash[:7], c.Name)
|
||||
return fmt.Sprintf("%s %s", c.Hash()[:7], c.Name)
|
||||
}
|
||||
|
||||
func (c *Commit) IsMerge() bool {
|
||||
return len(c.Parents) > 1
|
||||
return len(c.parents) > 1
|
||||
}
|
||||
|
||||
// returns true if this commit is not actually in the git log but instead
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package models
|
||||
|
||||
import "github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
|
||||
// The state of the working tree. Several of these can be true at once.
|
||||
// In particular, the concrete multi-state combinations that can occur in
|
||||
// practice are Rebasing+CherryPicking, and Rebasing+Reverting. Theoretically, I
|
||||
// guess Rebasing+Merging could also happen, but it probably won't in practice.
|
||||
type WorkingTreeState struct {
|
||||
Rebasing bool
|
||||
Merging bool
|
||||
CherryPicking bool
|
||||
Reverting bool
|
||||
}
|
||||
|
||||
func (self WorkingTreeState) Any() bool {
|
||||
return self.Rebasing || self.Merging || self.CherryPicking || self.Reverting
|
||||
}
|
||||
|
||||
func (self WorkingTreeState) None() bool {
|
||||
return !self.Any()
|
||||
}
|
||||
|
||||
type EffectiveWorkingTreeState int
|
||||
|
||||
const (
|
||||
// this means we're neither rebasing nor merging, cherry-picking, or reverting
|
||||
WORKING_TREE_STATE_NONE EffectiveWorkingTreeState = iota
|
||||
WORKING_TREE_STATE_REBASING
|
||||
WORKING_TREE_STATE_MERGING
|
||||
WORKING_TREE_STATE_CHERRY_PICKING
|
||||
WORKING_TREE_STATE_REVERTING
|
||||
)
|
||||
|
||||
// Effective returns the "current" state; if several states are true at once,
|
||||
// this is the one that should be displayed in status views, and it's the one
|
||||
// that the user can continue or abort.
|
||||
//
|
||||
// As an example, if you are stopped in an interactive rebase, and then you
|
||||
// perform a cherry-pick, and the cherry-pick conflicts, then both
|
||||
// WorkingTreeState.Rebasing and WorkingTreeState.CherryPicking are true.
|
||||
// The effective state is cherry-picking, because that's the one you can
|
||||
// continue or abort. It is not possible to continue the rebase without first
|
||||
// aborting the cherry-pick.
|
||||
func (self WorkingTreeState) Effective() EffectiveWorkingTreeState {
|
||||
if self.Reverting {
|
||||
return WORKING_TREE_STATE_REVERTING
|
||||
}
|
||||
if self.CherryPicking {
|
||||
return WORKING_TREE_STATE_CHERRY_PICKING
|
||||
}
|
||||
if self.Merging {
|
||||
return WORKING_TREE_STATE_MERGING
|
||||
}
|
||||
if self.Rebasing {
|
||||
return WORKING_TREE_STATE_REBASING
|
||||
}
|
||||
return WORKING_TREE_STATE_NONE
|
||||
}
|
||||
|
||||
func (self WorkingTreeState) Title(tr *i18n.TranslationSet) string {
|
||||
return map[EffectiveWorkingTreeState]string{
|
||||
WORKING_TREE_STATE_REBASING: tr.RebasingStatus,
|
||||
WORKING_TREE_STATE_MERGING: tr.MergingStatus,
|
||||
WORKING_TREE_STATE_CHERRY_PICKING: tr.CherryPickingStatus,
|
||||
WORKING_TREE_STATE_REVERTING: tr.RevertingStatus,
|
||||
}[self.Effective()]
|
||||
}
|
||||
|
||||
func (self WorkingTreeState) LowerCaseTitle(tr *i18n.TranslationSet) string {
|
||||
return map[EffectiveWorkingTreeState]string{
|
||||
WORKING_TREE_STATE_REBASING: tr.LowercaseRebasingStatus,
|
||||
WORKING_TREE_STATE_MERGING: tr.LowercaseMergingStatus,
|
||||
WORKING_TREE_STATE_CHERRY_PICKING: tr.LowercaseCherryPickingStatus,
|
||||
WORKING_TREE_STATE_REVERTING: tr.LowercaseRevertingStatus,
|
||||
}[self.Effective()]
|
||||
}
|
||||
|
||||
func (self WorkingTreeState) OptionsMenuTitle(tr *i18n.TranslationSet) string {
|
||||
return map[EffectiveWorkingTreeState]string{
|
||||
WORKING_TREE_STATE_REBASING: tr.RebaseOptionsTitle,
|
||||
WORKING_TREE_STATE_MERGING: tr.MergeOptionsTitle,
|
||||
WORKING_TREE_STATE_CHERRY_PICKING: tr.CherryPickOptionsTitle,
|
||||
WORKING_TREE_STATE_REVERTING: tr.RevertOptionsTitle,
|
||||
}[self.Effective()]
|
||||
}
|
||||
|
||||
func (self WorkingTreeState) OptionsMapTitle(tr *i18n.TranslationSet) string {
|
||||
return map[EffectiveWorkingTreeState]string{
|
||||
WORKING_TREE_STATE_REBASING: tr.ViewRebaseOptions,
|
||||
WORKING_TREE_STATE_MERGING: tr.ViewMergeOptions,
|
||||
WORKING_TREE_STATE_CHERRY_PICKING: tr.ViewCherryPickOptions,
|
||||
WORKING_TREE_STATE_REVERTING: tr.ViewRevertOptions,
|
||||
}[self.Effective()]
|
||||
}
|
||||
|
||||
func (self WorkingTreeState) CommandName() string {
|
||||
return map[EffectiveWorkingTreeState]string{
|
||||
WORKING_TREE_STATE_REBASING: "rebase",
|
||||
WORKING_TREE_STATE_MERGING: "merge",
|
||||
WORKING_TREE_STATE_CHERRY_PICKING: "cherry-pick",
|
||||
WORKING_TREE_STATE_REVERTING: "revert",
|
||||
}[self.Effective()]
|
||||
}
|
||||
|
||||
func (self WorkingTreeState) CanShowTodos() bool {
|
||||
return self.Rebasing || self.CherryPicking || self.Reverting
|
||||
}
|
||||
|
||||
func (self WorkingTreeState) CanSkip() bool {
|
||||
return self.Rebasing || self.CherryPicking || self.Reverting
|
||||
}
|
||||
@@ -11,67 +11,6 @@ import (
|
||||
|
||||
// A command object is a general way to represent a command to be run on the
|
||||
// command line.
|
||||
type ICmdObj interface {
|
||||
GetCmd() *exec.Cmd
|
||||
// outputs string representation of command. Note that if the command was built
|
||||
// using NewFromArgs, the output won't be quite the same as what you would type
|
||||
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
|
||||
ToString() string
|
||||
|
||||
// outputs args vector e.g. ["git", "commit", "-m", "my message"]
|
||||
Args() []string
|
||||
|
||||
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
|
||||
RunWithOutput() (string, error)
|
||||
// runs the command and returns stdout and stderr as a string, and an error if any
|
||||
RunWithOutputs() (string, string, error)
|
||||
// runs the command and runs a callback function on each line of the output. If the callback returns true for the boolean value, we kill the process and return.
|
||||
RunAndProcessLines(onLine func(line string) (bool, error)) error
|
||||
|
||||
// Be calling DontLog(), we're saying that once we call Run(), we don't want to
|
||||
// log the command in the UI (it'll still be logged in the log file). The general rule
|
||||
// is that if a command doesn't change the git state (e.g. read commands like `git diff`)
|
||||
// then we don't want to log it. If we are changing something (e.g. `git add .`) then
|
||||
// we do. The only exception is if we're running a command in the background periodically
|
||||
// like `git fetch`, which technically does mutate stuff but isn't something we need
|
||||
// to notify the user about.
|
||||
DontLog() ICmdObj
|
||||
|
||||
// This returns false if DontLog() was called
|
||||
ShouldLog() bool
|
||||
|
||||
// when you call this, then call Run(), we'll stream the output to the cmdWriter (i.e. the command log panel)
|
||||
StreamOutput() ICmdObj
|
||||
// returns true if StreamOutput() was called
|
||||
ShouldStreamOutput() bool
|
||||
|
||||
// if you call this before ShouldStreamOutput we'll consider an error with no
|
||||
// stderr content as a non-error. Not yet supported for Run or RunWithOutput (
|
||||
// but adding support is trivial)
|
||||
IgnoreEmptyError() ICmdObj
|
||||
// returns true if IgnoreEmptyError() was called
|
||||
ShouldIgnoreEmptyError() bool
|
||||
|
||||
PromptOnCredentialRequest(task gocui.Task) ICmdObj
|
||||
FailOnCredentialRequest() ICmdObj
|
||||
|
||||
WithMutex(mutex *deadlock.Mutex) ICmdObj
|
||||
Mutex() *deadlock.Mutex
|
||||
|
||||
GetCredentialStrategy() CredentialStrategy
|
||||
GetTask() gocui.Task
|
||||
|
||||
Clone() ICmdObj
|
||||
}
|
||||
|
||||
type CmdObj struct {
|
||||
cmd *exec.Cmd
|
||||
|
||||
@@ -83,6 +22,9 @@ type CmdObj struct {
|
||||
// see StreamOutput()
|
||||
streamOutput bool
|
||||
|
||||
// see UsePty()
|
||||
usePty bool
|
||||
|
||||
// see IgnoreEmptyError()
|
||||
ignoreEmptyError bool
|
||||
|
||||
@@ -109,12 +51,13 @@ const (
|
||||
FAIL
|
||||
)
|
||||
|
||||
var _ ICmdObj = &CmdObj{}
|
||||
|
||||
func (self *CmdObj) GetCmd() *exec.Cmd {
|
||||
return self.cmd
|
||||
}
|
||||
|
||||
// outputs string representation of command. Note that if the command was built
|
||||
// using NewFromArgs, the output won't be quite the same as what you would type
|
||||
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
|
||||
func (self *CmdObj) ToString() string {
|
||||
// if a given arg contains a space, we need to wrap it in quotes
|
||||
quotedArgs := lo.Map(self.cmd.Args, func(arg string, _ int) string {
|
||||
@@ -127,11 +70,19 @@ func (self *CmdObj) ToString() string {
|
||||
return strings.Join(quotedArgs, " ")
|
||||
}
|
||||
|
||||
// outputs args vector e.g. ["git", "commit", "-m", "my message"]
|
||||
func (self *CmdObj) Args() []string {
|
||||
return self.cmd.Args
|
||||
}
|
||||
|
||||
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
|
||||
// Set a string to be used as stdin for the command.
|
||||
func (self *CmdObj) SetStdin(input string) *CmdObj {
|
||||
self.cmd.Stdin = strings.NewReader(input)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) AddEnvVars(vars ...string) *CmdObj {
|
||||
self.cmd.Env = append(self.cmd.Env, vars...)
|
||||
|
||||
return self
|
||||
@@ -141,75 +92,109 @@ func (self *CmdObj) GetEnvVars() []string {
|
||||
return self.cmd.Env
|
||||
}
|
||||
|
||||
func (self *CmdObj) SetWd(wd string) ICmdObj {
|
||||
// sets the working directory
|
||||
func (self *CmdObj) SetWd(wd string) *CmdObj {
|
||||
self.cmd.Dir = wd
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) DontLog() ICmdObj {
|
||||
// By calling DontLog(), we're saying that once we call Run(), we don't want to
|
||||
// log the command in the UI (it'll still be logged in the log file). The general rule
|
||||
// is that if a command doesn't change the git state (e.g. read commands like `git diff`)
|
||||
// then we don't want to log it. If we are changing something (e.g. `git add .`) then
|
||||
// we do. The only exception is if we're running a command in the background periodically
|
||||
// like `git fetch`, which technically does mutate stuff but isn't something we need
|
||||
// to notify the user about.
|
||||
func (self *CmdObj) DontLog() *CmdObj {
|
||||
self.dontLog = true
|
||||
return self
|
||||
}
|
||||
|
||||
// This returns false if DontLog() was called
|
||||
func (self *CmdObj) ShouldLog() bool {
|
||||
return !self.dontLog
|
||||
}
|
||||
|
||||
func (self *CmdObj) StreamOutput() ICmdObj {
|
||||
// when you call this, then call Run(), we'll stream the output to the cmdWriter (i.e. the command log panel)
|
||||
func (self *CmdObj) StreamOutput() *CmdObj {
|
||||
self.streamOutput = true
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
// returns true if StreamOutput() was called
|
||||
func (self *CmdObj) ShouldStreamOutput() bool {
|
||||
return self.streamOutput
|
||||
}
|
||||
|
||||
func (self *CmdObj) IgnoreEmptyError() ICmdObj {
|
||||
// when you call this, then call Run(), we'll use a PTY to run the command. Only
|
||||
// has an effect if StreamOutput() was also called. Ignored on Windows.
|
||||
func (self *CmdObj) UsePty() *CmdObj {
|
||||
self.usePty = true
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
// returns true if UsePty() was called
|
||||
func (self *CmdObj) ShouldUsePty() bool {
|
||||
return self.usePty
|
||||
}
|
||||
|
||||
// if you call this before ShouldStreamOutput we'll consider an error with no
|
||||
// stderr content as a non-error. Not yet supported for Run or RunWithOutput (
|
||||
// but adding support is trivial)
|
||||
func (self *CmdObj) IgnoreEmptyError() *CmdObj {
|
||||
self.ignoreEmptyError = true
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
// returns true if IgnoreEmptyError() was called
|
||||
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
|
||||
return self.ignoreEmptyError
|
||||
}
|
||||
|
||||
func (self *CmdObj) Mutex() *deadlock.Mutex {
|
||||
return self.mutex
|
||||
}
|
||||
|
||||
func (self *CmdObj) WithMutex(mutex *deadlock.Mutex) ICmdObj {
|
||||
func (self *CmdObj) WithMutex(mutex *deadlock.Mutex) *CmdObj {
|
||||
self.mutex = mutex
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
|
||||
return self.ignoreEmptyError
|
||||
}
|
||||
|
||||
// runs the command and returns an error if any
|
||||
func (self *CmdObj) Run() error {
|
||||
return self.runner.Run(self)
|
||||
}
|
||||
|
||||
// runs the command and returns the output as a string, and an error if any
|
||||
func (self *CmdObj) RunWithOutput() (string, error) {
|
||||
return self.runner.RunWithOutput(self)
|
||||
}
|
||||
|
||||
// runs the command and returns stdout and stderr as a string, and an error if any
|
||||
func (self *CmdObj) RunWithOutputs() (string, string, error) {
|
||||
return self.runner.RunWithOutputs(self)
|
||||
}
|
||||
|
||||
// runs the command and runs a callback function on each line of the output. If the callback
|
||||
// returns true for the boolean value, we kill the process and return.
|
||||
func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
|
||||
return self.runner.RunAndProcessLines(self, onLine)
|
||||
}
|
||||
|
||||
func (self *CmdObj) PromptOnCredentialRequest(task gocui.Task) ICmdObj {
|
||||
func (self *CmdObj) PromptOnCredentialRequest(task gocui.Task) *CmdObj {
|
||||
self.credentialStrategy = PROMPT
|
||||
self.usePty = true
|
||||
self.task = task
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) FailOnCredentialRequest() ICmdObj {
|
||||
func (self *CmdObj) FailOnCredentialRequest() *CmdObj {
|
||||
self.credentialStrategy = FAIL
|
||||
|
||||
return self
|
||||
@@ -223,7 +208,7 @@ func (self *CmdObj) GetTask() gocui.Task {
|
||||
return self.task
|
||||
}
|
||||
|
||||
func (self *CmdObj) Clone() ICmdObj {
|
||||
func (self *CmdObj) Clone() *CmdObj {
|
||||
clone := &CmdObj{}
|
||||
*clone = *self
|
||||
clone.cmd = cloneCmd(self.cmd)
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
|
||||
type ICmdObjBuilder interface {
|
||||
// NewFromArgs takes a slice of strings like []string{"git", "commit"} and returns a new command object.
|
||||
New(args []string) ICmdObj
|
||||
New(args []string) *CmdObj
|
||||
// NewShell takes a string like `git commit` and returns an executable shell command for it e.g. `sh -c 'git commit'`
|
||||
// shellFunctionsFile is an optional file path that will be sourced before executing the command. Callers should pass
|
||||
// the value of UserConfig.OS.ShellFunctionsFile.
|
||||
NewShell(commandStr string, shellFunctionsFile string) ICmdObj
|
||||
NewShell(commandStr string, shellFunctionsFile string) *CmdObj
|
||||
// Quote wraps a string in quotes with any necessary escaping applied. The reason for bundling this up with the other methods in this interface is that we basically always need to make use of this when creating new command objects.
|
||||
Quote(str string) string
|
||||
}
|
||||
@@ -28,13 +28,13 @@ type CmdObjBuilder struct {
|
||||
// poor man's version of explicitly saying that struct X implements interface Y
|
||||
var _ ICmdObjBuilder = &CmdObjBuilder{}
|
||||
|
||||
func (self *CmdObjBuilder) New(args []string) ICmdObj {
|
||||
func (self *CmdObjBuilder) New(args []string) *CmdObj {
|
||||
cmdObj := self.NewWithEnviron(args, os.Environ())
|
||||
return cmdObj
|
||||
}
|
||||
|
||||
// A command with explicit environment from env
|
||||
func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) ICmdObj {
|
||||
func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) *CmdObj {
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Env = env
|
||||
|
||||
@@ -44,7 +44,7 @@ func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) ICmdObj {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) NewShell(commandStr string, shellFunctionsFile string) ICmdObj {
|
||||
func (self *CmdObjBuilder) NewShell(commandStr string, shellFunctionsFile string) *CmdObj {
|
||||
if len(shellFunctionsFile) > 0 {
|
||||
commandStr = fmt.Sprintf("%ssource %s\n%s", self.platform.PrefixForShellFunctionsFile, shellFunctionsFile, commandStr)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -11,14 +12,15 @@ import (
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ICmdObjRunner interface {
|
||||
Run(cmdObj ICmdObj) error
|
||||
RunWithOutput(cmdObj ICmdObj) (string, error)
|
||||
RunWithOutputs(cmdObj ICmdObj) (string, string, error)
|
||||
RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
|
||||
Run(cmdObj *CmdObj) error
|
||||
RunWithOutput(cmdObj *CmdObj) (string, error)
|
||||
RunWithOutputs(cmdObj *CmdObj) (string, string, error)
|
||||
RunAndProcessLines(cmdObj *CmdObj, onLine func(line string) (bool, error)) error
|
||||
}
|
||||
|
||||
type cmdObjRunner struct {
|
||||
@@ -28,7 +30,7 @@ type cmdObjRunner struct {
|
||||
|
||||
var _ ICmdObjRunner = &cmdObjRunner{}
|
||||
|
||||
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
|
||||
func (self *cmdObjRunner) Run(cmdObj *CmdObj) error {
|
||||
if cmdObj.Mutex() != nil {
|
||||
cmdObj.Mutex().Lock()
|
||||
defer cmdObj.Mutex().Unlock()
|
||||
@@ -46,7 +48,7 @@ func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
func (self *cmdObjRunner) RunWithOutput(cmdObj *CmdObj) (string, error) {
|
||||
if cmdObj.Mutex() != nil {
|
||||
cmdObj.Mutex().Lock()
|
||||
defer cmdObj.Mutex().Unlock()
|
||||
@@ -71,7 +73,7 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
return self.RunWithOutputAux(cmdObj)
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error) {
|
||||
func (self *cmdObjRunner) RunWithOutputs(cmdObj *CmdObj) (string, string, error) {
|
||||
if cmdObj.Mutex() != nil {
|
||||
cmdObj.Mutex().Lock()
|
||||
defer cmdObj.Mutex().Unlock()
|
||||
@@ -96,7 +98,7 @@ func (self *cmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error)
|
||||
return self.RunWithOutputsAux(cmdObj)
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
|
||||
func (self *cmdObjRunner) RunWithOutputAux(cmdObj *CmdObj) (string, error) {
|
||||
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
|
||||
|
||||
if cmdObj.ShouldLog() {
|
||||
@@ -114,7 +116,7 @@ func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutputsAux(cmdObj ICmdObj) (string, string, error) {
|
||||
func (self *cmdObjRunner) RunWithOutputsAux(cmdObj *CmdObj) (string, string, error) {
|
||||
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
|
||||
|
||||
if cmdObj.ShouldLog() {
|
||||
@@ -139,7 +141,7 @@ func (self *cmdObjRunner) RunWithOutputsAux(cmdObj ICmdObj) (string, string, err
|
||||
return stdout, stderr, err
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
func (self *cmdObjRunner) RunAndProcessLines(cmdObj *CmdObj, onLine func(line string) (bool, error)) error {
|
||||
if cmdObj.Mutex() != nil {
|
||||
cmdObj.Mutex().Lock()
|
||||
defer cmdObj.Mutex().Unlock()
|
||||
@@ -190,7 +192,7 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) {
|
||||
func (self *cmdObjRunner) logCmdObj(cmdObj *CmdObj) {
|
||||
self.guiIO.logCommandFn(cmdObj.ToString(), true)
|
||||
}
|
||||
|
||||
@@ -213,7 +215,7 @@ type cmdHandler struct {
|
||||
close func() error
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) error {
|
||||
func (self *cmdObjRunner) runAndStream(cmdObj *CmdObj) error {
|
||||
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
|
||||
go func() {
|
||||
_, _ = io.Copy(cmdWriter, handler.stdoutPipe)
|
||||
@@ -222,7 +224,7 @@ func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) error {
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) runAndStreamAux(
|
||||
cmdObj ICmdObj,
|
||||
cmdObj *CmdObj,
|
||||
onRun func(*cmdHandler, io.Writer),
|
||||
) error {
|
||||
cmdWriter := self.guiIO.newCmdWriterFn()
|
||||
@@ -236,7 +238,13 @@ func (self *cmdObjRunner) runAndStreamAux(
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
|
||||
|
||||
handler, err := self.getCmdHandler(cmd)
|
||||
var handler *cmdHandler
|
||||
var err error
|
||||
if cmdObj.ShouldUsePty() {
|
||||
handler, err = self.getCmdHandlerPty(cmd)
|
||||
} else {
|
||||
handler, err = self.getCmdHandlerNonPty(cmd)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -297,7 +305,7 @@ var failPromptFn = func(CredentialType) <-chan string {
|
||||
return ch
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
|
||||
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj *CmdObj) error {
|
||||
promptFn, err := self.getCredentialPromptFn(cmdObj)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -306,7 +314,7 @@ func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
|
||||
return self.runAndDetectCredentialRequest(cmdObj, promptFn)
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) getCredentialPromptFn(cmdObj ICmdObj) (func(CredentialType) <-chan string, error) {
|
||||
func (self *cmdObjRunner) getCredentialPromptFn(cmdObj *CmdObj) (func(CredentialType) <-chan string, error) {
|
||||
switch cmdObj.GetCredentialStrategy() {
|
||||
case PROMPT:
|
||||
return self.guiIO.promptForCredentialFn, nil
|
||||
@@ -322,7 +330,7 @@ func (self *cmdObjRunner) getCredentialPromptFn(cmdObj ICmdObj) (func(Credential
|
||||
// promptUserForCredential is a function that gets executed when this function detect you need to fill in a password or passphrase
|
||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||
func (self *cmdObjRunner) runAndDetectCredentialRequest(
|
||||
cmdObj ICmdObj,
|
||||
cmdObj *CmdObj,
|
||||
promptUserForCredential func(CredentialType) <-chan string,
|
||||
) error {
|
||||
// setting the output to english so we can parse it for a username/password request
|
||||
@@ -410,3 +418,38 @@ func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (Crede
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
type Buffer struct {
|
||||
b bytes.Buffer
|
||||
m deadlock.Mutex
|
||||
}
|
||||
|
||||
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Read(p)
|
||||
}
|
||||
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Write(p)
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) getCmdHandlerNonPty(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
cmd.Stdout = stdoutWriter
|
||||
|
||||
buf := &Buffer{}
|
||||
cmd.Stdin = buf
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cmdHandler{
|
||||
stdoutPipe: stdoutReader,
|
||||
stdinPipe: buf,
|
||||
close: func() error { return nil },
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
// we define this separately for windows and non-windows given that windows does
|
||||
// not have great PTY support and we need a PTY to handle a credential request
|
||||
func (self *cmdObjRunner) getCmdHandler(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
ptmx, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,48 +4,10 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
b bytes.Buffer
|
||||
m deadlock.Mutex
|
||||
}
|
||||
|
||||
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Read(p)
|
||||
}
|
||||
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Write(p)
|
||||
}
|
||||
|
||||
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
|
||||
func (self *cmdObjRunner) getCmdHandler(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
cmd.Stdout = stdoutWriter
|
||||
|
||||
buf := &Buffer{}
|
||||
cmd.Stdin = buf
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// because we don't yet have windows support for a pty, we instead just
|
||||
// pass our standard stream handlers and because there's no pty to close
|
||||
// we pass a no-op function for that.
|
||||
return &cmdHandler{
|
||||
stdoutPipe: stdoutReader,
|
||||
stdinPipe: buf,
|
||||
close: func() error { return nil },
|
||||
}, nil
|
||||
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
// We don't have PTY support on Windows yet, so we just return a non-PTY handler.
|
||||
return self.getCmdHandlerNonPty(cmd)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
// NewDummyOSCommand creates a new dummy OSCommand for testing
|
||||
func NewDummyOSCommand() *OSCommand {
|
||||
osCmd := NewOSCommand(utils.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
|
||||
osCmd := NewOSCommand(common.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
|
||||
|
||||
return osCmd
|
||||
}
|
||||
@@ -23,9 +23,9 @@ type OSCommandDeps struct {
|
||||
}
|
||||
|
||||
func NewDummyOSCommandWithDeps(deps OSCommandDeps) *OSCommand {
|
||||
common := deps.Common
|
||||
if common == nil {
|
||||
common = utils.NewDummyCommon()
|
||||
cmn := deps.Common
|
||||
if cmn == nil {
|
||||
cmn = common.NewDummyCommon()
|
||||
}
|
||||
|
||||
platform := deps.Platform
|
||||
@@ -34,7 +34,7 @@ func NewDummyOSCommandWithDeps(deps OSCommandDeps) *OSCommand {
|
||||
}
|
||||
|
||||
return &OSCommand{
|
||||
Common: common,
|
||||
Common: cmn,
|
||||
Platform: platform,
|
||||
getenvFn: deps.GetenvFn,
|
||||
removeFileFn: deps.RemoveFileFn,
|
||||
@@ -59,7 +59,7 @@ var dummyPlatform = &Platform{
|
||||
}
|
||||
|
||||
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
|
||||
osCommand := NewOSCommand(utils.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
|
||||
osCommand := NewOSCommand(common.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
|
||||
osCommand.Cmd = NewDummyCmdObjBuilder(runner)
|
||||
|
||||
return osCommand
|
||||
|
||||
@@ -28,7 +28,7 @@ type FakeCmdObjRunner struct {
|
||||
type CmdObjMatcher struct {
|
||||
description string
|
||||
// returns true if the matcher matches the command object
|
||||
test func(ICmdObj) bool
|
||||
test func(*CmdObj) bool
|
||||
|
||||
// output of the command
|
||||
output string
|
||||
@@ -48,12 +48,12 @@ func (self *FakeCmdObjRunner) remainingExpectedCmds() []CmdObjMatcher {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) Run(cmdObj ICmdObj) error {
|
||||
func (self *FakeCmdObjRunner) Run(cmdObj *CmdObj) error {
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
func (self *FakeCmdObjRunner) RunWithOutput(cmdObj *CmdObj) (string, error) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
@@ -78,12 +78,12 @@ func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error) {
|
||||
func (self *FakeCmdObjRunner) RunWithOutputs(cmdObj *CmdObj) (string, string, error) {
|
||||
output, err := self.RunWithOutput(cmdObj)
|
||||
return output, "", err
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj *CmdObj, onLine func(line string) (bool, error)) error {
|
||||
output, err := self.RunWithOutput(cmdObj)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -105,7 +105,7 @@ func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(lin
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) ExpectFunc(description string, fn func(cmdObj ICmdObj) bool, output string, err error) *FakeCmdObjRunner {
|
||||
func (self *FakeCmdObjRunner) ExpectFunc(description string, fn func(cmdObj *CmdObj) bool, output string, err error) *FakeCmdObjRunner {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
@@ -121,7 +121,7 @@ func (self *FakeCmdObjRunner) ExpectFunc(description string, fn func(cmdObj ICmd
|
||||
|
||||
func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
|
||||
description := fmt.Sprintf("matches args %s", strings.Join(expectedArgs, " "))
|
||||
self.ExpectFunc(description, func(cmdObj ICmdObj) bool {
|
||||
self.ExpectFunc(description, func(cmdObj *CmdObj) bool {
|
||||
return slices.Equal(expectedArgs, cmdObj.GetCmd().Args)
|
||||
}, output, err)
|
||||
|
||||
@@ -130,7 +130,7 @@ func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, e
|
||||
|
||||
func (self *FakeCmdObjRunner) ExpectGitArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
|
||||
description := fmt.Sprintf("matches git args %s", strings.Join(expectedArgs, " "))
|
||||
self.ExpectFunc(description, func(cmdObj ICmdObj) bool {
|
||||
self.ExpectFunc(description, func(cmdObj *CmdObj) bool {
|
||||
return slices.Equal(expectedArgs, cmdObj.GetCmd().Args[1:])
|
||||
}, output, err)
|
||||
|
||||
|
||||
@@ -208,13 +208,13 @@ 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 := lo.Map(cmdObjs, func(cmdObj ICmdObj, _ int) *exec.Cmd {
|
||||
func (c *OSCommand) PipeCommands(cmdObjs ...*CmdObj) error {
|
||||
cmds := lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) *exec.Cmd {
|
||||
return cmdObj.GetCmd()
|
||||
})
|
||||
|
||||
logCmdStr := strings.Join(
|
||||
lo.Map(cmdObjs, func(cmdObj ICmdObj, _ int) string {
|
||||
lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) string {
|
||||
return cmdObj.ToString()
|
||||
}),
|
||||
" | ",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@@ -54,7 +53,7 @@ func (self *Patch) Lines() []*PatchLine {
|
||||
|
||||
// Returns the patch line index of the first line in the given hunk
|
||||
func (self *Patch) HunkStartIdx(hunkIndex int) int {
|
||||
hunkIndex = utils.Clamp(hunkIndex, 0, len(self.hunks)-1)
|
||||
hunkIndex = lo.Clamp(hunkIndex, 0, len(self.hunks)-1)
|
||||
|
||||
result := len(self.header)
|
||||
for i := 0; i < hunkIndex; i++ {
|
||||
@@ -65,7 +64,7 @@ func (self *Patch) HunkStartIdx(hunkIndex int) int {
|
||||
|
||||
// Returns the patch line index of the last line in the given hunk
|
||||
func (self *Patch) HunkEndIdx(hunkIndex int) int {
|
||||
hunkIndex = utils.Clamp(hunkIndex, 0, len(self.hunks)-1)
|
||||
hunkIndex = lo.Clamp(hunkIndex, 0, len(self.hunks)-1)
|
||||
|
||||
return self.HunkStartIdx(hunkIndex) + self.hunks[hunkIndex].lineCount() - 1
|
||||
}
|
||||
@@ -118,7 +117,7 @@ func (self *Patch) HunkContainingLine(idx int) int {
|
||||
|
||||
// Returns the patch line index of the next change (i.e. addition or deletion).
|
||||
func (self *Patch) GetNextChangeIdx(idx int) int {
|
||||
idx = utils.Clamp(idx, 0, self.LineCount()-1)
|
||||
idx = lo.Clamp(idx, 0, self.LineCount()-1)
|
||||
|
||||
lines := self.Lines()
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package enums
|
||||
|
||||
type RebaseMode int
|
||||
|
||||
const (
|
||||
// this means we're neither rebasing nor merging
|
||||
REBASE_MODE_NONE RebaseMode = iota
|
||||
// this means normal rebase as opposed to interactive rebase
|
||||
REBASE_MODE_NORMAL
|
||||
REBASE_MODE_INTERACTIVE
|
||||
// REBASE_MODE_REBASING is a general state that captures both REBASE_MODE_NORMAL and REBASE_MODE_INTERACTIVE
|
||||
REBASE_MODE_REBASING
|
||||
REBASE_MODE_MERGING
|
||||
)
|
||||
|
||||
func (self RebaseMode) IsMerging() bool {
|
||||
return self == REBASE_MODE_MERGING
|
||||
}
|
||||
|
||||
func (self RebaseMode) IsRebasing() bool {
|
||||
return self == REBASE_MODE_INTERACTIVE || self == REBASE_MODE_NORMAL || self == REBASE_MODE_REBASING
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func NewDummyCommon() *Common {
|
||||
tr := i18n.EnglishTranslationSet()
|
||||
cmn := &Common{
|
||||
Log: utils.NewDummyLog(),
|
||||
Tr: tr,
|
||||
Fs: afero.NewOsFs(),
|
||||
}
|
||||
cmn.SetUserConfig(config.GetDefaultConfig())
|
||||
return cmn
|
||||
}
|
||||
|
||||
func NewDummyCommonWithUserConfigAndAppState(userConfig *config.UserConfig, appState *config.AppState) *Common {
|
||||
tr := i18n.EnglishTranslationSet()
|
||||
cmn := &Common{
|
||||
Log: utils.NewDummyLog(),
|
||||
Tr: tr,
|
||||
AppState: appState,
|
||||
// TODO: remove dependency on actual filesystem in tests and switch to using
|
||||
// in-memory for everything
|
||||
Fs: afero.NewOsFs(),
|
||||
}
|
||||
cmn.SetUserConfig(userConfig)
|
||||
return cmn
|
||||
}
|
||||
+178
-40
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/jesseduffield/generics/orderedset"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils/yaml_utils"
|
||||
"github.com/samber/lo"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -94,7 +97,7 @@ func NewAppConfig(
|
||||
configFiles = []*ConfigFile{configFile}
|
||||
}
|
||||
|
||||
userConfig, err := loadUserConfigWithDefaults(configFiles)
|
||||
userConfig, err := loadUserConfigWithDefaults(configFiles, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -143,11 +146,11 @@ func findOrCreateConfigDir() (string, error) {
|
||||
return folder, os.MkdirAll(folder, 0o755)
|
||||
}
|
||||
|
||||
func loadUserConfigWithDefaults(configFiles []*ConfigFile) (*UserConfig, error) {
|
||||
return loadUserConfig(configFiles, GetDefaultConfig())
|
||||
func loadUserConfigWithDefaults(configFiles []*ConfigFile, isGuiInitialized bool) (*UserConfig, error) {
|
||||
return loadUserConfig(configFiles, GetDefaultConfig(), isGuiInitialized)
|
||||
}
|
||||
|
||||
func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, error) {
|
||||
func loadUserConfig(configFiles []*ConfigFile, base *UserConfig, isGuiInitialized bool) (*UserConfig, error) {
|
||||
for _, configFile := range configFiles {
|
||||
path := configFile.Path
|
||||
statInfo, err := os.Stat(path)
|
||||
@@ -192,7 +195,7 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content, err = migrateUserConfig(path, content)
|
||||
content, err = migrateUserConfig(path, content, isGuiInitialized)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -213,41 +216,64 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e
|
||||
return base, nil
|
||||
}
|
||||
|
||||
type ChangesSet = orderedset.OrderedSet[string]
|
||||
|
||||
func NewChangesSet() *ChangesSet {
|
||||
return orderedset.New[string]()
|
||||
}
|
||||
|
||||
// Do any backward-compatibility migrations of things that have changed in the
|
||||
// config over time; examples are renaming a key to a better name, moving a key
|
||||
// from one container to another, or changing the type of a key (e.g. from bool
|
||||
// to an enum).
|
||||
func migrateUserConfig(path string, content []byte) ([]byte, error) {
|
||||
changedContent, err := computeMigratedConfig(path, content)
|
||||
func migrateUserConfig(path string, content []byte, isGuiInitialized bool) ([]byte, error) {
|
||||
changes := NewChangesSet()
|
||||
|
||||
changedContent, didChange, err := computeMigratedConfig(path, content, changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write config back if changed
|
||||
if string(changedContent) != string(content) {
|
||||
fmt.Println("Provided user config is deprecated but auto-fixable. Attempting to write fixed version back to file...")
|
||||
if err := os.WriteFile(path, changedContent, 0o644); err != nil {
|
||||
return nil, fmt.Errorf("While attempting to write back fixed user config to %s, an error occurred: %s", path, err)
|
||||
}
|
||||
fmt.Printf("Success. New config written to %s\n", path)
|
||||
return changedContent, nil
|
||||
// Nothing to do if config didn't change
|
||||
if !didChange {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
return content, nil
|
||||
changesText := "The following changes were made:\n\n"
|
||||
changesText += strings.Join(lo.Map(changes.ToSliceFromOldest(), func(change string, _ int) string {
|
||||
return fmt.Sprintf("- %s\n", change)
|
||||
}), "")
|
||||
|
||||
// Write config back
|
||||
if !isGuiInitialized {
|
||||
fmt.Printf("The user config file %s must be migrated. Attempting to do this automatically.\n", path)
|
||||
fmt.Println(changesText)
|
||||
}
|
||||
if err := os.WriteFile(path, changedContent, 0o644); err != nil {
|
||||
errorMsg := fmt.Sprintf("While attempting to write back migrated user config to %s, an error occurred: %s", path, err)
|
||||
if isGuiInitialized {
|
||||
errorMsg += "\n\n" + changesText
|
||||
}
|
||||
return nil, errors.New(errorMsg)
|
||||
}
|
||||
if !isGuiInitialized {
|
||||
fmt.Printf("Config file saved successfully to %s\n", path)
|
||||
}
|
||||
return changedContent, nil
|
||||
}
|
||||
|
||||
// A pure function helper for testing purposes
|
||||
func computeMigratedConfig(path string, content []byte) ([]byte, error) {
|
||||
func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([]byte, bool, error) {
|
||||
var err error
|
||||
var rootNode yaml.Node
|
||||
err = yaml.Unmarshal(content, &rootNode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse YAML: %w", err)
|
||||
return nil, false, fmt.Errorf("failed to parse YAML: %w", err)
|
||||
}
|
||||
var originalCopy yaml.Node
|
||||
err = yaml.Unmarshal(content, &originalCopy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse YAML, but only the second time!?!? How did that happen: %w", err)
|
||||
return nil, false, fmt.Errorf("failed to parse YAML, but only the second time!?!? How did that happen: %w", err)
|
||||
}
|
||||
|
||||
pathsToReplace := []struct {
|
||||
@@ -260,50 +286,64 @@ func computeMigratedConfig(path string, content []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
for _, pathToReplace := range pathsToReplace {
|
||||
err := yaml_utils.RenameYamlKey(&rootNode, pathToReplace.oldPath, pathToReplace.newName)
|
||||
err, didReplace := yaml_utils.RenameYamlKey(&rootNode, pathToReplace.oldPath, pathToReplace.newName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't migrate config file at `%s` for key %s: %s", path, strings.Join(pathToReplace.oldPath, "."), err)
|
||||
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s` for key %s: %s", path, strings.Join(pathToReplace.oldPath, "."), err)
|
||||
}
|
||||
if didReplace {
|
||||
changes.Add(fmt.Sprintf("Renamed '%s' to '%s'", strings.Join(pathToReplace.oldPath, "."), pathToReplace.newName))
|
||||
}
|
||||
}
|
||||
|
||||
err = changeNullKeybindingsToDisabled(&rootNode)
|
||||
err = changeNullKeybindingsToDisabled(&rootNode, changes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||
}
|
||||
|
||||
err = changeElementToSequence(&rootNode, []string{"git", "commitPrefix"})
|
||||
err = changeElementToSequence(&rootNode, []string{"git", "commitPrefix"}, changes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||
}
|
||||
|
||||
err = changeCommitPrefixesMap(&rootNode)
|
||||
err = changeCommitPrefixesMap(&rootNode, changes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||
}
|
||||
|
||||
err = changeCustomCommandStreamAndOutputToOutputEnum(&rootNode, changes)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||
}
|
||||
|
||||
err = migrateAllBranchesLogCmd(&rootNode, changes)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||
}
|
||||
|
||||
// Add more migrations here...
|
||||
|
||||
if !reflect.DeepEqual(rootNode, originalCopy) {
|
||||
newContent, err := yaml_utils.YamlMarshal(&rootNode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to remarsal!\n %w", err)
|
||||
}
|
||||
return newContent, nil
|
||||
} else {
|
||||
return content, nil
|
||||
if reflect.DeepEqual(rootNode, originalCopy) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
newContent, err := yaml_utils.YamlMarshal(&rootNode)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("Failed to remarsal!\n %w", err)
|
||||
}
|
||||
return newContent, true, nil
|
||||
}
|
||||
|
||||
func changeNullKeybindingsToDisabled(rootNode *yaml.Node) error {
|
||||
func changeNullKeybindingsToDisabled(rootNode *yaml.Node, changes *ChangesSet) error {
|
||||
return yaml_utils.Walk(rootNode, func(node *yaml.Node, path string) {
|
||||
if strings.HasPrefix(path, "keybinding.") && node.Kind == yaml.ScalarNode && node.Tag == "!!null" {
|
||||
node.Value = "<disabled>"
|
||||
node.Tag = "!!str"
|
||||
changes.Add(fmt.Sprintf("Changed 'null' to '<disabled>' for keybinding '%s'", path))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func changeElementToSequence(rootNode *yaml.Node, path []string) error {
|
||||
func changeElementToSequence(rootNode *yaml.Node, path []string, changes *ChangesSet) error {
|
||||
return yaml_utils.TransformNode(rootNode, path, func(node *yaml.Node) error {
|
||||
if node.Kind == yaml.MappingNode {
|
||||
nodeContentCopy := node.Content
|
||||
@@ -315,13 +355,15 @@ func changeElementToSequence(rootNode *yaml.Node, path []string) error {
|
||||
Content: nodeContentCopy,
|
||||
}}
|
||||
|
||||
changes.Add(fmt.Sprintf("Changed '%s' to an array of strings", strings.Join(path, ".")))
|
||||
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func changeCommitPrefixesMap(rootNode *yaml.Node) error {
|
||||
func changeCommitPrefixesMap(rootNode *yaml.Node, changes *ChangesSet) error {
|
||||
return yaml_utils.TransformNode(rootNode, []string{"git", "commitPrefixes"}, func(prefixesNode *yaml.Node) error {
|
||||
if prefixesNode.Kind == yaml.MappingNode {
|
||||
for _, contentNode := range prefixesNode.Content {
|
||||
@@ -334,6 +376,7 @@ func changeCommitPrefixesMap(rootNode *yaml.Node) error {
|
||||
Kind: yaml.MappingNode,
|
||||
Content: nodeContentCopy,
|
||||
}}
|
||||
changes.Add("Changed 'git.commitPrefixes' elements to arrays of strings")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,6 +384,101 @@ func changeCommitPrefixesMap(rootNode *yaml.Node) error {
|
||||
})
|
||||
}
|
||||
|
||||
func changeCustomCommandStreamAndOutputToOutputEnum(rootNode *yaml.Node, changes *ChangesSet) error {
|
||||
return yaml_utils.Walk(rootNode, func(node *yaml.Node, path string) {
|
||||
// We are being lazy here and rely on the fact that the only mapping
|
||||
// nodes in the tree under customCommands are actual custom commands. If
|
||||
// this ever changes (e.g. because we add a struct field to
|
||||
// customCommand), then we need to change this to iterate properly.
|
||||
if strings.HasPrefix(path, "customCommands[") && node.Kind == yaml.MappingNode {
|
||||
output := ""
|
||||
if streamKey, streamValue := yaml_utils.RemoveKey(node, "subprocess"); streamKey != nil {
|
||||
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" {
|
||||
output = "terminal"
|
||||
changes.Add("Changed 'subprocess: true' to 'output: terminal' in custom command")
|
||||
} else {
|
||||
changes.Add("Deleted redundant 'subprocess: false' in custom command")
|
||||
}
|
||||
}
|
||||
if streamKey, streamValue := yaml_utils.RemoveKey(node, "stream"); streamKey != nil {
|
||||
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
|
||||
output = "log"
|
||||
changes.Add("Changed 'stream: true' to 'output: log' in custom command")
|
||||
} else {
|
||||
changes.Add(fmt.Sprintf("Deleted redundant 'stream: %v' property in custom command", streamValue.Value))
|
||||
}
|
||||
}
|
||||
if streamKey, streamValue := yaml_utils.RemoveKey(node, "showOutput"); streamKey != nil {
|
||||
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
|
||||
changes.Add("Changed 'showOutput: true' to 'output: popup' in custom command")
|
||||
output = "popup"
|
||||
} else {
|
||||
changes.Add(fmt.Sprintf("Deleted redundant 'showOutput: %v' property in custom command", streamValue.Value))
|
||||
}
|
||||
}
|
||||
if output != "" {
|
||||
outputKeyNode := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: "output",
|
||||
Tag: "!!str",
|
||||
}
|
||||
outputValueNode := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: output,
|
||||
Tag: "!!str",
|
||||
}
|
||||
node.Content = append(node.Content, outputKeyNode, outputValueNode)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// This migration is special because users have already defined
|
||||
// a single element at `allBranchesLogCmd` and the sequence at `allBranchesLogCmds`.
|
||||
// Some users have explicitly set `allBranchesLogCmd` to be an empty string in order
|
||||
// to remove it, so in that case we just delete the element, and add nothing to the list
|
||||
func migrateAllBranchesLogCmd(rootNode *yaml.Node, changes *ChangesSet) error {
|
||||
return yaml_utils.TransformNode(rootNode, []string{"git"}, func(gitNode *yaml.Node) error {
|
||||
cmdKeyNode, cmdValueNode := yaml_utils.LookupKey(gitNode, "allBranchesLogCmd")
|
||||
// Nothing to do if they do not have the deprecated item
|
||||
if cmdKeyNode == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmdsKeyNode, cmdsValueNode := yaml_utils.LookupKey(gitNode, "allBranchesLogCmds")
|
||||
var change string
|
||||
if cmdsKeyNode == nil {
|
||||
// Create empty sequence node and attach it onto the root git node
|
||||
// We will later populate it with the individual allBranchesLogCmd record
|
||||
cmdsKeyNode = &yaml.Node{Kind: yaml.ScalarNode, Value: "allBranchesLogCmds"}
|
||||
cmdsValueNode = &yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{}}
|
||||
gitNode.Content = append(gitNode.Content,
|
||||
cmdsKeyNode,
|
||||
cmdsValueNode,
|
||||
)
|
||||
change = "Created git.allBranchesLogCmds array containing value of git.allBranchesLogCmd"
|
||||
} else {
|
||||
if cmdsValueNode.Kind != yaml.SequenceNode {
|
||||
return errors.New("You should have an allBranchesLogCmds defined as a sequence!")
|
||||
}
|
||||
|
||||
change = "Prepended git.allBranchesLogCmd value to git.allBranchesLogCmds array"
|
||||
}
|
||||
|
||||
if cmdValueNode.Value != "" {
|
||||
// Prepending the individual element to make it show up first in the list, which was prior behavior
|
||||
cmdsValueNode.Content = utils.Prepend(cmdsValueNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: cmdValueNode.Value})
|
||||
changes.Add(change)
|
||||
}
|
||||
|
||||
// Clear out the existing allBranchesLogCmd, now that we have migrated it into the list
|
||||
_, _ = yaml_utils.RemoveKey(gitNode, "allBranchesLogCmd")
|
||||
changes.Add("Removed obsolete git.allBranchesLogCmd")
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetDebug() bool {
|
||||
return c.debug
|
||||
}
|
||||
@@ -381,7 +519,7 @@ func (c *AppConfig) GetUserConfigDir() string {
|
||||
|
||||
func (c *AppConfig) ReloadUserConfigForRepo(repoConfigFiles []*ConfigFile) error {
|
||||
configFiles := append(c.globalUserConfigFiles, repoConfigFiles...)
|
||||
userConfig, err := loadUserConfigWithDefaults(configFiles)
|
||||
userConfig, err := loadUserConfigWithDefaults(configFiles, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -406,7 +544,7 @@ func (c *AppConfig) ReloadChangedUserConfigFiles() (error, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
userConfig, err := loadUserConfigWithDefaults(c.userConfigFiles)
|
||||
userConfig, err := loadUserConfigWithDefaults(c.userConfigFiles, true)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
|
||||
+422
-31
@@ -6,17 +6,165 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommitPrefixMigrations(t *testing.T) {
|
||||
func TestMigrationOfRenamedKeys(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectedDidChange bool
|
||||
expectedChanges []string
|
||||
}{
|
||||
{
|
||||
name: "Empty String",
|
||||
input: "",
|
||||
expected: "",
|
||||
}, {
|
||||
name: "Empty String",
|
||||
input: "",
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "No rename needed",
|
||||
input: `foo:
|
||||
bar: 5
|
||||
`,
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "Rename one",
|
||||
input: `gui:
|
||||
skipUnstageLineWarning: true
|
||||
`,
|
||||
expected: `gui:
|
||||
skipDiscardChangeWarning: true
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Renamed 'gui.skipUnstageLineWarning' to 'skipDiscardChangeWarning'"},
|
||||
},
|
||||
{
|
||||
name: "Rename several",
|
||||
input: `gui:
|
||||
windowSize: half
|
||||
skipUnstageLineWarning: true
|
||||
keybinding:
|
||||
universal:
|
||||
executeCustomCommand: a
|
||||
`,
|
||||
expected: `gui:
|
||||
screenMode: half
|
||||
skipDiscardChangeWarning: true
|
||||
keybinding:
|
||||
universal:
|
||||
executeShellCommand: a
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{
|
||||
"Renamed 'gui.skipUnstageLineWarning' to 'skipDiscardChangeWarning'",
|
||||
"Renamed 'keybinding.universal.executeCustomCommand' to 'executeShellCommand'",
|
||||
"Renamed 'gui.windowSize' to 'screenMode'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
changes := NewChangesSet()
|
||||
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expectedDidChange, didChange)
|
||||
if didChange {
|
||||
assert.Equal(t, s.expected, string(actual))
|
||||
}
|
||||
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateNullKeybindingsToDisabled(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectedDidChange bool
|
||||
expectedChanges []string
|
||||
}{
|
||||
{
|
||||
name: "Empty String",
|
||||
input: "",
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "No change needed",
|
||||
input: `keybinding:
|
||||
universal:
|
||||
quit: q
|
||||
`,
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "Change one",
|
||||
input: `keybinding:
|
||||
universal:
|
||||
quit: null
|
||||
`,
|
||||
expected: `keybinding:
|
||||
universal:
|
||||
quit: <disabled>
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Changed 'null' to '<disabled>' for keybinding 'keybinding.universal.quit'"},
|
||||
},
|
||||
{
|
||||
name: "Change several",
|
||||
input: `keybinding:
|
||||
universal:
|
||||
quit: null
|
||||
return: <esc>
|
||||
new: null
|
||||
`,
|
||||
expected: `keybinding:
|
||||
universal:
|
||||
quit: <disabled>
|
||||
return: <esc>
|
||||
new: <disabled>
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{
|
||||
"Changed 'null' to '<disabled>' for keybinding 'keybinding.universal.quit'",
|
||||
"Changed 'null' to '<disabled>' for keybinding 'keybinding.universal.new'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
changes := NewChangesSet()
|
||||
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expectedDidChange, didChange)
|
||||
if didChange {
|
||||
assert.Equal(t, s.expected, string(actual))
|
||||
}
|
||||
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitPrefixMigrations(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectedDidChange bool
|
||||
expectedChanges []string
|
||||
}{
|
||||
{
|
||||
name: "Empty String",
|
||||
input: "",
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "Single CommitPrefix Rename",
|
||||
input: `git:
|
||||
commitPrefix:
|
||||
@@ -28,7 +176,10 @@ func TestCommitPrefixMigrations(t *testing.T) {
|
||||
- pattern: "^\\w+-\\w+.*"
|
||||
replace: '[JIRA $0] '
|
||||
`,
|
||||
}, {
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Changed 'git.commitPrefix' to an array of strings"},
|
||||
},
|
||||
{
|
||||
name: "Complicated CommitPrefixes Rename",
|
||||
input: `git:
|
||||
commitPrefixes:
|
||||
@@ -48,13 +199,16 @@ func TestCommitPrefixMigrations(t *testing.T) {
|
||||
- pattern: "^foo.bar*"
|
||||
replace: '[FUN $0] '
|
||||
`,
|
||||
}, {
|
||||
name: "Incomplete Configuration",
|
||||
input: "git:",
|
||||
expected: "git:",
|
||||
}, {
|
||||
// This test intentionally uses non-standard indentation to test that the migration
|
||||
// does not change the input.
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Changed 'git.commitPrefixes' elements to arrays of strings"},
|
||||
},
|
||||
{
|
||||
name: "Incomplete Configuration",
|
||||
input: "git:",
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "No changes made when already migrated",
|
||||
input: `
|
||||
git:
|
||||
@@ -65,25 +219,144 @@ git:
|
||||
foo:
|
||||
- pattern: "^\\w+-\\w+.*"
|
||||
replace: '[JIRA $0] '`,
|
||||
expected: `
|
||||
git:
|
||||
commitPrefix:
|
||||
- pattern: "Hello World"
|
||||
replace: "Goodbye"
|
||||
commitPrefixes:
|
||||
foo:
|
||||
- pattern: "^\\w+-\\w+.*"
|
||||
replace: '[JIRA $0] '`,
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
actual, err := computeMigratedConfig("path doesn't matter", []byte(s.input))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
changes := NewChangesSet()
|
||||
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expectedDidChange, didChange)
|
||||
if didChange {
|
||||
assert.Equal(t, s.expected, string(actual))
|
||||
}
|
||||
assert.Equal(t, s.expected, string(actual))
|
||||
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomCommandsOutputMigration(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectedDidChange bool
|
||||
expectedChanges []string
|
||||
}{
|
||||
{
|
||||
name: "Empty String",
|
||||
input: "",
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "Convert subprocess to output=terminal",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
subprocess: true
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
output: terminal
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Changed 'subprocess: true' to 'output: terminal' in custom command"},
|
||||
},
|
||||
{
|
||||
name: "Convert stream to output=log",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
stream: true
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
output: log
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Changed 'stream: true' to 'output: log' in custom command"},
|
||||
},
|
||||
{
|
||||
name: "Convert showOutput to output=popup",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
showOutput: true
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
output: popup
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Changed 'showOutput: true' to 'output: popup' in custom command"},
|
||||
},
|
||||
{
|
||||
name: "Subprocess wins over the other two",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
subprocess: true
|
||||
stream: true
|
||||
showOutput: true
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
output: terminal
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{
|
||||
"Changed 'subprocess: true' to 'output: terminal' in custom command",
|
||||
"Deleted redundant 'stream: true' property in custom command",
|
||||
"Deleted redundant 'showOutput: true' property in custom command",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Stream wins over showOutput",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
stream: true
|
||||
showOutput: true
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
output: log
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{
|
||||
"Changed 'stream: true' to 'output: log' in custom command",
|
||||
"Deleted redundant 'showOutput: true' property in custom command",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Explicitly setting to false doesn't create an output=none key",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
subprocess: false
|
||||
stream: false
|
||||
showOutput: false
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{
|
||||
"Deleted redundant 'subprocess: false' in custom command",
|
||||
"Deleted redundant 'stream: false' property in custom command",
|
||||
"Deleted redundant 'showOutput: false' property in custom command",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
changes := NewChangesSet()
|
||||
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expectedDidChange, didChange)
|
||||
if didChange {
|
||||
assert.Equal(t, s.expected, string(actual))
|
||||
}
|
||||
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -691,7 +964,125 @@ keybinding:
|
||||
`)
|
||||
|
||||
func BenchmarkMigrationOnLargeConfiguration(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = computeMigratedConfig("path doesn't matter", largeConfiguration)
|
||||
for b.Loop() {
|
||||
changes := NewChangesSet()
|
||||
_, _, _ = computeMigratedConfig("path doesn't matter", largeConfiguration, changes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllBranchesLogCmdMigrations(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectedDidChange bool
|
||||
expectedChanges []string
|
||||
}{
|
||||
{
|
||||
name: "Incomplete Configuration Passes uneventfully",
|
||||
input: "git:",
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "Single Cmd with no Cmds",
|
||||
input: `git:
|
||||
allBranchesLogCmd: git log --graph --oneline
|
||||
`,
|
||||
expected: `git:
|
||||
allBranchesLogCmds:
|
||||
- git log --graph --oneline
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{
|
||||
"Created git.allBranchesLogCmds array containing value of git.allBranchesLogCmd",
|
||||
"Removed obsolete git.allBranchesLogCmd",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Cmd with one existing Cmds",
|
||||
input: `git:
|
||||
allBranchesLogCmd: git log --graph --oneline
|
||||
allBranchesLogCmds:
|
||||
- git log --graph --oneline --pretty
|
||||
`,
|
||||
expected: `git:
|
||||
allBranchesLogCmds:
|
||||
- git log --graph --oneline
|
||||
- git log --graph --oneline --pretty
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{
|
||||
"Prepended git.allBranchesLogCmd value to git.allBranchesLogCmds array",
|
||||
"Removed obsolete git.allBranchesLogCmd",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Only Cmds set have no changes",
|
||||
input: `git:
|
||||
allBranchesLogCmds:
|
||||
- git log
|
||||
`,
|
||||
expected: "",
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "Removes Empty Cmd When at end of yaml",
|
||||
input: `git:
|
||||
allBranchesLogCmds:
|
||||
- git log --graph --oneline
|
||||
allBranchesLogCmd:
|
||||
`,
|
||||
expected: `git:
|
||||
allBranchesLogCmds:
|
||||
- git log --graph --oneline
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Removed obsolete git.allBranchesLogCmd"},
|
||||
},
|
||||
{
|
||||
name: "Migrates when sequence defined inline",
|
||||
input: `git:
|
||||
allBranchesLogCmds: [foo, bar]
|
||||
allBranchesLogCmd: baz
|
||||
`,
|
||||
expected: `git:
|
||||
allBranchesLogCmds: [baz, foo, bar]
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{
|
||||
"Prepended git.allBranchesLogCmd value to git.allBranchesLogCmds array",
|
||||
"Removed obsolete git.allBranchesLogCmd",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Removes Empty Cmd With Keys Afterwards",
|
||||
input: `git:
|
||||
allBranchesLogCmds:
|
||||
- git log --graph --oneline
|
||||
allBranchesLogCmd:
|
||||
foo: bar
|
||||
`,
|
||||
expected: `git:
|
||||
allBranchesLogCmds:
|
||||
- git log --graph --oneline
|
||||
foo: bar
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Removed obsolete git.allBranchesLogCmd"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
changes := NewChangesSet()
|
||||
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expectedDidChange, didChange)
|
||||
if didChange {
|
||||
assert.Equal(t, s.expected, string(actual))
|
||||
}
|
||||
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetEditTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
|
||||
preset := getPreset(osConfig, guessDefaultEditor)
|
||||
func GetEditTemplate(shell string, osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
|
||||
preset := getPreset(shell, osConfig, guessDefaultEditor)
|
||||
template := osConfig.Edit
|
||||
if template == "" {
|
||||
template = preset.editTemplate
|
||||
@@ -12,8 +15,8 @@ func GetEditTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (stri
|
||||
return template, getEditInTerminal(osConfig, preset)
|
||||
}
|
||||
|
||||
func GetEditAtLineTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
|
||||
preset := getPreset(osConfig, guessDefaultEditor)
|
||||
func GetEditAtLineTemplate(shell string, osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
|
||||
preset := getPreset(shell, osConfig, guessDefaultEditor)
|
||||
template := osConfig.EditAtLine
|
||||
if template == "" {
|
||||
template = preset.editAtLineTemplate
|
||||
@@ -21,8 +24,8 @@ func GetEditAtLineTemplate(osConfig *OSConfig, guessDefaultEditor func() string)
|
||||
return template, getEditInTerminal(osConfig, preset)
|
||||
}
|
||||
|
||||
func GetEditAtLineAndWaitTemplate(osConfig *OSConfig, guessDefaultEditor func() string) string {
|
||||
preset := getPreset(osConfig, guessDefaultEditor)
|
||||
func GetEditAtLineAndWaitTemplate(shell string, osConfig *OSConfig, guessDefaultEditor func() string) string {
|
||||
preset := getPreset(shell, osConfig, guessDefaultEditor)
|
||||
template := osConfig.EditAtLineAndWait
|
||||
if template == "" {
|
||||
template = preset.editAtLineAndWaitTemplate
|
||||
@@ -30,8 +33,8 @@ func GetEditAtLineAndWaitTemplate(osConfig *OSConfig, guessDefaultEditor func()
|
||||
return template
|
||||
}
|
||||
|
||||
func GetOpenDirInEditorTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
|
||||
preset := getPreset(osConfig, guessDefaultEditor)
|
||||
func GetOpenDirInEditorTemplate(shell string, osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
|
||||
preset := getPreset(shell, osConfig, guessDefaultEditor)
|
||||
template := osConfig.OpenDirInEditor
|
||||
if template == "" {
|
||||
template = preset.openDirInEditorTemplate
|
||||
@@ -50,17 +53,28 @@ type editPreset struct {
|
||||
func returnBool(a bool) func() bool { return (func() bool { return a }) }
|
||||
|
||||
// IF YOU ADD A PRESET TO THIS FUNCTION YOU MUST UPDATE THE `Supported presets` SECTION OF docs/Config.md
|
||||
func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
|
||||
func getPreset(shell string, osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
|
||||
var nvimRemoteEditTemplate, nvimRemoteEditAtLineTemplate, nvimRemoteOpenDirInEditorTemplate string
|
||||
// By default fish doesn't have SHELL variable set, but it does have FISH_VERSION since Nov 2012.
|
||||
if (strings.HasSuffix(shell, "fish")) || (os.Getenv("FISH_VERSION") != "") {
|
||||
nvimRemoteEditTemplate = `begin; if test -z "$NVIM"; nvim -- {{filename}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{filename}}; end; end`
|
||||
nvimRemoteEditAtLineTemplate = `begin; if test -z "$NVIM"; nvim +{{line}} -- {{filename}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{filename}}; nvim --server "$NVIM" --remote-send ":{{line}}<CR>"; end; end`
|
||||
nvimRemoteOpenDirInEditorTemplate = `begin; if test -z "$NVIM"; nvim -- {{dir}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{dir}}; end; end`
|
||||
} else {
|
||||
nvimRemoteEditTemplate = `[ -z "$NVIM" ] && (nvim -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}})`
|
||||
nvimRemoteEditAtLineTemplate = `[ -z "$NVIM" ] && (nvim +{{line}} -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}} && nvim --server "$NVIM" --remote-send ":{{line}}<CR>")`
|
||||
nvimRemoteOpenDirInEditorTemplate = `[ -z "$NVIM" ] && (nvim -- {{dir}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{dir}})`
|
||||
}
|
||||
presets := map[string]*editPreset{
|
||||
"vi": standardTerminalEditorPreset("vi"),
|
||||
"vim": standardTerminalEditorPreset("vim"),
|
||||
"nvim": standardTerminalEditorPreset("nvim"),
|
||||
"nvim-remote": {
|
||||
editTemplate: `[ -z "$NVIM" ] && (nvim -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}})`,
|
||||
editAtLineTemplate: `[ -z "$NVIM" ] && (nvim +{{line}} -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}} && nvim --server "$NVIM" --remote-send ":{{line}}<CR>")`,
|
||||
editTemplate: nvimRemoteEditTemplate,
|
||||
editAtLineTemplate: nvimRemoteEditAtLineTemplate,
|
||||
// No remote-wait support yet. See https://github.com/neovim/neovim/pull/17856
|
||||
editAtLineAndWaitTemplate: `nvim +{{line}} {{filename}}`,
|
||||
openDirInEditorTemplate: `[ -z "$NVIM" ] && (nvim -- {{dir}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{dir}})`,
|
||||
openDirInEditorTemplate: nvimRemoteOpenDirInEditorTemplate,
|
||||
suspend: func() bool {
|
||||
_, ok := os.LookupEnv("NVIM")
|
||||
return !ok
|
||||
|
||||
@@ -111,15 +111,15 @@ func TestGetEditTemplate(t *testing.T) {
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
template, suspend := GetEditTemplate(s.osConfig, s.guessDefaultEditor)
|
||||
template, suspend := GetEditTemplate("bash", s.osConfig, s.guessDefaultEditor)
|
||||
assert.Equal(t, s.expectedEditTemplate, template)
|
||||
assert.Equal(t, s.expectedSuspend, suspend)
|
||||
|
||||
template, suspend = GetEditAtLineTemplate(s.osConfig, s.guessDefaultEditor)
|
||||
template, suspend = GetEditAtLineTemplate("bash", s.osConfig, s.guessDefaultEditor)
|
||||
assert.Equal(t, s.expectedEditAtLineTemplate, template)
|
||||
assert.Equal(t, s.expectedSuspend, suspend)
|
||||
|
||||
template = GetEditAtLineAndWaitTemplate(s.osConfig, s.guessDefaultEditor)
|
||||
template = GetEditAtLineAndWaitTemplate("bash", s.osConfig, s.guessDefaultEditor)
|
||||
assert.Equal(t, s.expectedEditAtLineAndWaitTemplate, template)
|
||||
})
|
||||
}
|
||||
|
||||
+14
-14
@@ -244,6 +244,9 @@ type GitConfig struct {
|
||||
AutoFetch bool `yaml:"autoFetch"`
|
||||
// If true, periodically refresh files and submodules
|
||||
AutoRefresh bool `yaml:"autoRefresh"`
|
||||
// If not "none", lazygit will automatically fast-forward local branches to match their upstream after fetching. Applies to branches that are not the currently checked out branch, and only to those that are strictly behind their upstream (as opposed to diverged).
|
||||
// Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
|
||||
AutoForwardBranches string `yaml:"autoForwardBranches" jsonschema:"enum=none,enum=onlyMainBranches,enum=allBranches"`
|
||||
// If true, pass the --all arg to git fetch
|
||||
FetchAll bool `yaml:"fetchAll"`
|
||||
// If true, lazygit will automatically stage files that used to have merge
|
||||
@@ -253,9 +256,6 @@ type GitConfig struct {
|
||||
AutoStageResolvedConflicts bool `yaml:"autoStageResolvedConflicts"`
|
||||
// Command used when displaying the current branch git log in the main window
|
||||
BranchLogCmd string `yaml:"branchLogCmd"`
|
||||
// Command used to display git log of all branches in the main window.
|
||||
// Deprecated: Use `allBranchesLogCmds` instead.
|
||||
AllBranchesLogCmd string `yaml:"allBranchesLogCmd" jsonschema:"deprecated"`
|
||||
// Commands used to display git log of all branches in the main window, they will be cycled in order of appearance (array of strings)
|
||||
AllBranchesLogCmds []string `yaml:"allBranchesLogCmds"`
|
||||
// If true, do not spawn a separate process when using GPG
|
||||
@@ -395,6 +395,7 @@ type KeybindingUniversalConfig struct {
|
||||
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
|
||||
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
|
||||
JumpToBlock []string `yaml:"jumpToBlock"`
|
||||
FocusMainView string `yaml:"focusMainView"`
|
||||
NextMatch string `yaml:"nextMatch"`
|
||||
PrevMatch string `yaml:"prevMatch"`
|
||||
StartSearch string `yaml:"startSearch"`
|
||||
@@ -404,6 +405,7 @@ type KeybindingUniversalConfig struct {
|
||||
GoInto string `yaml:"goInto"`
|
||||
Confirm string `yaml:"confirm"`
|
||||
ConfirmInEditor string `yaml:"confirmInEditor"`
|
||||
ConfirmInEditorAlt string `yaml:"confirmInEditor-alt"`
|
||||
Remove string `yaml:"remove"`
|
||||
New string `yaml:"new"`
|
||||
Edit string `yaml:"edit"`
|
||||
@@ -478,6 +480,7 @@ type KeybindingBranchesConfig struct {
|
||||
RebaseBranch string `yaml:"rebaseBranch"`
|
||||
RenameBranch string `yaml:"renameBranch"`
|
||||
MergeIntoCurrentBranch string `yaml:"mergeIntoCurrentBranch"`
|
||||
MoveCommitsToNewBranch string `yaml:"moveCommitsToNewBranch"`
|
||||
ViewGitFlowOptions string `yaml:"viewGitFlowOptions"`
|
||||
FastForward string `yaml:"fastForward"`
|
||||
CreateTag string `yaml:"createTag"`
|
||||
@@ -632,22 +635,15 @@ type CustomCommand struct {
|
||||
Context string `yaml:"context" jsonschema:"example=status,example=files,example=worktrees,example=localBranches,example=remotes,example=remoteBranches,example=tags,example=commits,example=reflogCommits,example=subCommits,example=commitFiles,example=stash,example=global"`
|
||||
// The command to run (using Go template syntax for placeholder values)
|
||||
Command string `yaml:"command" jsonschema:"example=git fetch {{.Form.Remote}} {{.Form.Branch}} && git checkout FETCH_HEAD"`
|
||||
// If true, run the command in a subprocess (e.g. if the command requires user input)
|
||||
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
|
||||
Subprocess *bool `yaml:"subprocess"`
|
||||
// A list of prompts that will request user input before running the final command
|
||||
Prompts []CustomCommandPrompt `yaml:"prompts"`
|
||||
// Text to display while waiting for command to finish
|
||||
LoadingText string `yaml:"loadingText" jsonschema:"example=Loading..."`
|
||||
// Label for the custom command when displayed in the keybindings menu
|
||||
Description string `yaml:"description"`
|
||||
// If true, stream the command's output to the Command Log panel
|
||||
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
|
||||
Stream *bool `yaml:"stream"`
|
||||
// If true, show the command's output in a popup within Lazygit
|
||||
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
|
||||
ShowOutput *bool `yaml:"showOutput"`
|
||||
// The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title.
|
||||
// Where the output of the command should go. 'none' discards it, 'terminal' suspends lazygit and runs the command in the terminal (useful for commands that require user input), 'log' streams it to the command log, 'logWithPty' is like 'log' but runs the command in a pseudo terminal (can be useful for commands that produce colored output when the output is a terminal), and 'popup' shows it in a popup.
|
||||
Output string `yaml:"output" jsonschema:"enum=none,enum=terminal,enum=log,enum=logWithPty,enum=popup"`
|
||||
// The title to display in the popup panel if output is set to 'popup'. If left unset, the command will be used as the title.
|
||||
OutputTitle string `yaml:"outputTitle"`
|
||||
// Actions to take after the command has completed
|
||||
// [dev] Pointer so that we can tell whether it appears in the config file
|
||||
@@ -821,10 +817,11 @@ func GetDefaultConfig() *UserConfig {
|
||||
MainBranches: []string{"master", "main"},
|
||||
AutoFetch: true,
|
||||
AutoRefresh: true,
|
||||
AutoForwardBranches: "onlyMainBranches",
|
||||
FetchAll: true,
|
||||
AutoStageResolvedConflicts: true,
|
||||
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",
|
||||
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
|
||||
AllBranchesLogCmds: []string{"git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium"},
|
||||
DisableForcePushing: false,
|
||||
CommitPrefixes: map[string][]CommitPrefixConfig(nil),
|
||||
BranchPrefix: "",
|
||||
@@ -876,6 +873,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
PrevBlockAlt2: "<backtab>",
|
||||
NextBlockAlt2: "<tab>",
|
||||
JumpToBlock: []string{"1", "2", "3", "4", "5"},
|
||||
FocusMainView: "0",
|
||||
NextMatch: "n",
|
||||
PrevMatch: "N",
|
||||
StartSearch: "/",
|
||||
@@ -885,6 +883,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
GoInto: "<enter>",
|
||||
Confirm: "<enter>",
|
||||
ConfirmInEditor: "<a-enter>",
|
||||
ConfirmInEditorAlt: "<c-s>",
|
||||
Remove: "d",
|
||||
New: "n",
|
||||
Edit: "e",
|
||||
@@ -956,6 +955,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
RebaseBranch: "r",
|
||||
RenameBranch: "R",
|
||||
MergeIntoCurrentBranch: "M",
|
||||
MoveCommitsToNewBranch: "N",
|
||||
ViewGitFlowOptions: "i",
|
||||
FastForward: "f",
|
||||
CreateTag: "T",
|
||||
|
||||
@@ -19,6 +19,10 @@ func (config *UserConfig) Validate() error {
|
||||
[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateEnum("git.autoForwardBranches", config.Git.AutoForwardBranches,
|
||||
[]string{"none", "onlyMainBranches", "allBranches"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateKeybindings(config.Keybinding); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -97,21 +101,29 @@ func validateCustomCommands(customCommands []CustomCommand) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(customCommand.CommandMenu) > 0 &&
|
||||
(len(customCommand.Context) > 0 ||
|
||||
if len(customCommand.CommandMenu) > 0 {
|
||||
if len(customCommand.Context) > 0 ||
|
||||
len(customCommand.Command) > 0 ||
|
||||
customCommand.Subprocess != nil ||
|
||||
len(customCommand.Prompts) > 0 ||
|
||||
len(customCommand.LoadingText) > 0 ||
|
||||
customCommand.Stream != nil ||
|
||||
customCommand.ShowOutput != nil ||
|
||||
len(customCommand.Output) > 0 ||
|
||||
len(customCommand.OutputTitle) > 0 ||
|
||||
customCommand.After != nil) {
|
||||
commandRef := ""
|
||||
if len(customCommand.Key) > 0 {
|
||||
commandRef = fmt.Sprintf(" with key '%s'", customCommand.Key)
|
||||
customCommand.After != nil {
|
||||
commandRef := ""
|
||||
if len(customCommand.Key) > 0 {
|
||||
commandRef = fmt.Sprintf(" with key '%s'", customCommand.Key)
|
||||
}
|
||||
return fmt.Errorf("Error with custom command%s: it is not allowed to use both commandMenu and any of the other fields except key and description.", commandRef)
|
||||
}
|
||||
|
||||
if err := validateCustomCommands(customCommand.CommandMenu); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := validateEnum("customCommand.output", customCommand.Output,
|
||||
[]string{"", "none", "terminal", "log", "logWithPty", "popup"}); err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("Error with custom command%s: it is not allowed to use both commandMenu and any of the other fields except key and description.", commandRef)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -74,6 +74,46 @@ func TestUserConfigValidate_enums(t *testing.T) {
|
||||
{value: "invalid_value", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom command keybinding in sub menu",
|
||||
setup: func(config *UserConfig, value string) {
|
||||
config.CustomCommands = []CustomCommand{
|
||||
{
|
||||
Key: "X",
|
||||
Description: "My Custom Commands",
|
||||
CommandMenu: []CustomCommand{
|
||||
{Key: value, Command: "echo 'hello'", Context: "global"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
testCases: []testCase{
|
||||
{value: "", valid: true},
|
||||
{value: "<disabled>", valid: true},
|
||||
{value: "q", valid: true},
|
||||
{value: "<c-c>", valid: true},
|
||||
{value: "invalid_value", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom command output",
|
||||
setup: func(config *UserConfig, value string) {
|
||||
config.CustomCommands = []CustomCommand{
|
||||
{
|
||||
Output: value,
|
||||
},
|
||||
}
|
||||
},
|
||||
testCases: []testCase{
|
||||
{value: "", valid: true},
|
||||
{value: "none", valid: true},
|
||||
{value: "terminal", valid: true},
|
||||
{value: "log", valid: true},
|
||||
{value: "logWithPty", valid: true},
|
||||
{value: "popup", valid: true},
|
||||
{value: "invalid_value", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom command sub menu",
|
||||
setup: func(config *UserConfig, _ string) {
|
||||
@@ -97,7 +137,7 @@ func TestUserConfigValidate_enums(t *testing.T) {
|
||||
config.CustomCommands = []CustomCommand{
|
||||
{
|
||||
Key: "X",
|
||||
Context: "global",
|
||||
Context: "global", // context is not allowed for submenus
|
||||
CommandMenu: []CustomCommand{
|
||||
{Key: "1", Command: "echo 'hello'", Context: "global"},
|
||||
},
|
||||
@@ -111,11 +151,10 @@ func TestUserConfigValidate_enums(t *testing.T) {
|
||||
{
|
||||
name: "Custom command sub menu",
|
||||
setup: func(config *UserConfig, _ string) {
|
||||
falseVal := false
|
||||
config.CustomCommands = []CustomCommand{
|
||||
{
|
||||
Key: "X",
|
||||
Subprocess: &falseVal,
|
||||
Key: "X",
|
||||
LoadingText: "loading", // other properties are not allowed for submenus (using loadingText as an example)
|
||||
CommandMenu: []CustomCommand{
|
||||
{Key: "1", Command: "echo 'hello'", Context: "global"},
|
||||
},
|
||||
|
||||
@@ -125,7 +125,11 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
|
||||
func (self *BackgroundRoutineMgr) backgroundFetch() (err error) {
|
||||
err = self.gui.git.Sync.FetchBackground()
|
||||
|
||||
_ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
|
||||
_ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.SYNC})
|
||||
|
||||
if err == nil {
|
||||
err = self.gui.helpers.BranchesHelper.AutoForwardBranches()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (gui *Gui) getRandomTip() string {
|
||||
|
||||
// general advice
|
||||
"`git commit` is really just the programmer equivalent of saving your game. Always do it before embarking on an ambitious change!",
|
||||
"Try to separate commits that refactor code from commits that add new functionality: if they're squashed into the one commit, it can be hard to spot what's new.",
|
||||
"Try to separate commits that refactor code from commits that add new functionality: if they're squashed into one commit, it can be hard to spot what's new.",
|
||||
"If you ever want to experiment, it's easy to create a new branch off your current one and go nuts, then delete it afterwards",
|
||||
"Always read through the diff of your changes before assigning somebody to review your code. Better for you to catch any silly mistakes than your colleagues!",
|
||||
"If something goes wrong, you can always checkout a commit from your reflog to return to an earlier state",
|
||||
|
||||
@@ -357,3 +357,19 @@ func (self *ContextMgr) CurrentPopup() []types.Context {
|
||||
return context.GetKind() == types.TEMPORARY_POPUP || context.GetKind() == types.PERSISTENT_POPUP
|
||||
})
|
||||
}
|
||||
|
||||
func (self *ContextMgr) NextInStack(c types.Context) types.Context {
|
||||
self.RLock()
|
||||
defer self.RUnlock()
|
||||
|
||||
for i := range self.ContextStack {
|
||||
if self.ContextStack[i].GetKey() == c.GetKey() {
|
||||
if i == 0 {
|
||||
return nil
|
||||
}
|
||||
return self.ContextStack[i-1]
|
||||
}
|
||||
}
|
||||
|
||||
panic("context not in stack")
|
||||
}
|
||||
|
||||
@@ -13,12 +13,13 @@ type BaseContext struct {
|
||||
windowName string
|
||||
onGetOptionsMap func() map[string]string
|
||||
|
||||
keybindingsFns []types.KeybindingsFn
|
||||
mouseKeybindingsFns []types.MouseKeybindingsFn
|
||||
onClickFn func() error
|
||||
onRenderToMainFn func()
|
||||
onFocusFn onFocusFn
|
||||
onFocusLostFn onFocusLostFn
|
||||
keybindingsFns []types.KeybindingsFn
|
||||
mouseKeybindingsFns []types.MouseKeybindingsFn
|
||||
onClickFn func() error
|
||||
onClickFocusedMainViewFn onClickFocusedMainViewFn
|
||||
onRenderToMainFn func()
|
||||
onFocusFn onFocusFn
|
||||
onFocusLostFn onFocusLostFn
|
||||
|
||||
focusable bool
|
||||
transient bool
|
||||
@@ -31,8 +32,9 @@ type BaseContext struct {
|
||||
}
|
||||
|
||||
type (
|
||||
onFocusFn = func(types.OnFocusOpts)
|
||||
onFocusLostFn = func(types.OnFocusLostOpts)
|
||||
onFocusFn = func(types.OnFocusOpts)
|
||||
onFocusLostFn = func(types.OnFocusLostOpts)
|
||||
onClickFocusedMainViewFn = func(mainViewName string, clickedLineIdx int) error
|
||||
)
|
||||
|
||||
var _ types.IBaseContext = &BaseContext{}
|
||||
@@ -144,10 +146,20 @@ func (self *BaseContext) AddOnClickFn(fn func() error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BaseContext) AddOnClickFocusedMainViewFn(fn onClickFocusedMainViewFn) {
|
||||
if fn != nil {
|
||||
self.onClickFocusedMainViewFn = fn
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BaseContext) GetOnClick() func() error {
|
||||
return self.onClickFn
|
||||
}
|
||||
|
||||
func (self *BaseContext) GetOnClickFocusedMainView() onClickFocusedMainViewFn {
|
||||
return self.onClickFocusedMainViewFn
|
||||
}
|
||||
|
||||
func (self *BaseContext) AddOnRenderToMainFn(fn func()) {
|
||||
if fn != nil {
|
||||
self.onRenderToMainFn = fn
|
||||
|
||||
@@ -30,7 +30,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
||||
c.State().GetItemOperation,
|
||||
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
||||
c.Modes().Diffing.Ref,
|
||||
c.Views().Branches.InnerWidth(),
|
||||
c.Views().Branches.InnerWidth()+c.Views().Branches.OriginX(),
|
||||
c.Tr,
|
||||
c.UserConfig(),
|
||||
c.Model().Worktrees,
|
||||
|
||||
@@ -98,8 +98,8 @@ type ContextTree struct {
|
||||
SubCommits *SubCommitsContext
|
||||
Stash *StashContext
|
||||
Suggestions *SuggestionsContext
|
||||
Normal types.Context
|
||||
NormalSecondary types.Context
|
||||
Normal *MainContext
|
||||
NormalSecondary *MainContext
|
||||
Staging *PatchExplorerContext
|
||||
StagingSecondary *PatchExplorerContext
|
||||
CustomPatchBuilder *PatchExplorerContext
|
||||
|
||||
@@ -26,6 +26,8 @@ type ListContextTrait struct {
|
||||
func (self *ListContextTrait) IsListContext() {}
|
||||
|
||||
func (self *ListContextTrait) FocusLine() {
|
||||
self.Context.FocusLine()
|
||||
|
||||
// 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.
|
||||
@@ -147,3 +149,7 @@ func (self *ListContextTrait) TotalContentHeight() int {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) IndexForGotoBottom() int {
|
||||
return self.list.Len() - 1
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (self *ListRenderer) ModelIndexToViewIndex(modelIndex int) int {
|
||||
}
|
||||
|
||||
func (self *ListRenderer) ViewIndexToModelIndex(viewIndex int) int {
|
||||
viewIndex = utils.Clamp(viewIndex, 0, self.list.Len()+self.numNonModelItems)
|
||||
viewIndex = lo.Clamp(viewIndex, 0, self.list.Len()+self.numNonModelItems)
|
||||
if self.modelIndicesByViewIndex != nil {
|
||||
return self.modelIndicesByViewIndex[viewIndex]
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
@@ -32,16 +32,15 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
||||
)
|
||||
|
||||
getDisplayStrings := func(startIdx int, endIdx int) [][]string {
|
||||
selectedCommitHash := ""
|
||||
var selectedCommitHashPtr *string
|
||||
|
||||
if c.Context().Current().GetKey() == LOCAL_COMMITS_CONTEXT_KEY {
|
||||
selectedCommit := viewModel.GetSelected()
|
||||
if selectedCommit != nil {
|
||||
selectedCommitHash = selectedCommit.Hash
|
||||
selectedCommitHashPtr = selectedCommit.HashPtr()
|
||||
}
|
||||
}
|
||||
|
||||
showYouAreHereLabel := c.Model().WorkingTreeStateAtLastCommitRefresh == enums.REBASE_MODE_REBASING
|
||||
hasRebaseUpdateRefsConfig := c.Git().Config.GetRebaseUpdateRefs()
|
||||
|
||||
return presentation.GetCommitListDisplayStrings(
|
||||
@@ -58,15 +57,59 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
||||
c.UserConfig().Gui.ShortTimeFormat,
|
||||
time.Now(),
|
||||
c.UserConfig().Git.ParseEmoji,
|
||||
selectedCommitHash,
|
||||
selectedCommitHashPtr,
|
||||
startIdx,
|
||||
endIdx,
|
||||
shouldShowGraph(c),
|
||||
c.Model().BisectInfo,
|
||||
showYouAreHereLabel,
|
||||
)
|
||||
}
|
||||
|
||||
getNonModelItems := func() []*NonModelItem {
|
||||
result := []*NonModelItem{}
|
||||
if c.Model().WorkingTreeStateAtLastCommitRefresh.CanShowTodos() {
|
||||
if c.Model().WorkingTreeStateAtLastCommitRefresh.Rebasing {
|
||||
result = append(result, &NonModelItem{
|
||||
Index: 0,
|
||||
Content: fmt.Sprintf("--- %s ---", c.Tr.PendingRebaseTodosSectionHeader),
|
||||
})
|
||||
}
|
||||
|
||||
if c.Model().WorkingTreeStateAtLastCommitRefresh.CherryPicking ||
|
||||
c.Model().WorkingTreeStateAtLastCommitRefresh.Reverting {
|
||||
_, firstCherryPickOrRevertTodo, found := lo.FindIndexOf(
|
||||
c.Model().Commits, func(c *models.Commit) bool {
|
||||
return c.Status == models.StatusCherryPickingOrReverting ||
|
||||
c.Status == models.StatusConflicted
|
||||
})
|
||||
if !found {
|
||||
firstCherryPickOrRevertTodo = 0
|
||||
}
|
||||
label := lo.Ternary(c.Model().WorkingTreeStateAtLastCommitRefresh.CherryPicking,
|
||||
c.Tr.PendingCherryPicksSectionHeader,
|
||||
c.Tr.PendingRevertsSectionHeader)
|
||||
result = append(result, &NonModelItem{
|
||||
Index: firstCherryPickOrRevertTodo,
|
||||
Content: fmt.Sprintf("--- %s ---", label),
|
||||
})
|
||||
}
|
||||
|
||||
_, firstRealCommit, found := lo.FindIndexOf(
|
||||
c.Model().Commits, func(c *models.Commit) bool {
|
||||
return !c.IsTODO()
|
||||
})
|
||||
if !found {
|
||||
firstRealCommit = 0
|
||||
}
|
||||
result = append(result, &NonModelItem{
|
||||
Index: firstRealCommit,
|
||||
Content: fmt.Sprintf("--- %s ---", c.Tr.CommitsSectionHeader),
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
ctx := &LocalCommitsContext{
|
||||
LocalCommitsViewModel: viewModel,
|
||||
SearchTrait: NewSearchTrait(c),
|
||||
@@ -83,6 +126,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
||||
ListRenderer: ListRenderer{
|
||||
list: viewModel,
|
||||
getDisplayStrings: getDisplayStrings,
|
||||
getNonModelItems: getNonModelItems,
|
||||
},
|
||||
c: c,
|
||||
refreshViewportOnChange: true,
|
||||
@@ -148,7 +192,7 @@ func (self *LocalCommitsContext) GetSelectedCommitHash() string {
|
||||
if commit == nil {
|
||||
return ""
|
||||
}
|
||||
return commit.Hash
|
||||
return commit.Hash()
|
||||
}
|
||||
|
||||
func (self *LocalCommitsContext) SelectCommitByHash(hash string) bool {
|
||||
@@ -156,7 +200,7 @@ func (self *LocalCommitsContext) SelectCommitByHash(hash string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, idx, found := lo.FindIndexOf(self.GetItems(), func(c *models.Commit) bool { return c.Hash == hash }); found {
|
||||
if _, idx, found := lo.FindIndexOf(self.GetItems(), func(c *models.Commit) bool { return c.Hash() == hash }); found {
|
||||
self.SetSelection(idx)
|
||||
return true
|
||||
}
|
||||
@@ -175,7 +219,7 @@ func (self *LocalCommitsContext) RefForAdjustingLineNumberInDiff() string {
|
||||
if commits == nil {
|
||||
return ""
|
||||
}
|
||||
return commits[0].Hash
|
||||
return commits[0].Hash()
|
||||
}
|
||||
|
||||
func (self *LocalCommitsContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
|
||||
@@ -240,8 +284,25 @@ func searchModelCommits(caseSensitive bool, commits []*models.Commit, columnPosi
|
||||
// that we render. So we just set the XStart and XEnd values to the
|
||||
// start and end of the commit hash column, which is the second one.
|
||||
result := gocui.SearchPosition{XStart: columnPositions[1], XEnd: columnPositions[2] - 1, Y: idx}
|
||||
return result, strings.Contains(normalize(commit.Hash), searchStr) ||
|
||||
return result, strings.Contains(normalize(commit.Hash()), searchStr) ||
|
||||
strings.Contains(normalize(commit.Name), searchStr) ||
|
||||
strings.Contains(normalize(commit.ExtraInfo), searchStr) // allow searching for tags
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsContext) IndexForGotoBottom() int {
|
||||
commits := self.GetCommits()
|
||||
selectedIdx := self.GetSelectedLineIdx()
|
||||
if selectedIdx >= 0 && selectedIdx < len(commits)-1 {
|
||||
if commits[selectedIdx+1].Status != models.StatusMerged {
|
||||
_, idx, found := lo.FindIndexOf(commits, func(c *models.Commit) bool {
|
||||
return c.Status == models.StatusMerged
|
||||
})
|
||||
if found {
|
||||
return idx - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self.list.Len() - 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type MainContext struct {
|
||||
*SimpleContext
|
||||
*SearchTrait
|
||||
}
|
||||
|
||||
var _ types.ISearchableContext = (*MainContext)(nil)
|
||||
|
||||
func NewMainContext(
|
||||
view *gocui.View,
|
||||
windowName string,
|
||||
key types.ContextKey,
|
||||
c *ContextCommon,
|
||||
) *MainContext {
|
||||
ctx := &MainContext{
|
||||
SimpleContext: NewSimpleContext(
|
||||
NewBaseContext(NewBaseContextOpts{
|
||||
Kind: types.MAIN_CONTEXT,
|
||||
View: view,
|
||||
WindowName: windowName,
|
||||
Key: key,
|
||||
Focusable: true,
|
||||
HighlightOnFocus: false,
|
||||
})),
|
||||
SearchTrait: NewSearchTrait(c),
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(int) error { return nil }))
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (self *MainContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
|
||||
return nil
|
||||
}
|
||||
+16
-32
@@ -25,38 +25,22 @@ func NewContextTree(c *ContextCommon) *ContextTree {
|
||||
Focusable: true,
|
||||
}),
|
||||
),
|
||||
Files: NewWorkingTreeContext(c),
|
||||
Submodules: NewSubmodulesContext(c),
|
||||
Menu: NewMenuContext(c),
|
||||
Remotes: NewRemotesContext(c),
|
||||
Worktrees: NewWorktreesContext(c),
|
||||
RemoteBranches: NewRemoteBranchesContext(c),
|
||||
LocalCommits: NewLocalCommitsContext(c),
|
||||
CommitFiles: commitFilesContext,
|
||||
ReflogCommits: NewReflogCommitsContext(c),
|
||||
SubCommits: NewSubCommitsContext(c),
|
||||
Branches: NewBranchesContext(c),
|
||||
Tags: NewTagsContext(c),
|
||||
Stash: NewStashContext(c),
|
||||
Suggestions: NewSuggestionsContext(c),
|
||||
Normal: NewSimpleContext(
|
||||
NewBaseContext(NewBaseContextOpts{
|
||||
Kind: types.MAIN_CONTEXT,
|
||||
View: c.Views().Main,
|
||||
WindowName: "main",
|
||||
Key: NORMAL_MAIN_CONTEXT_KEY,
|
||||
Focusable: false,
|
||||
}),
|
||||
),
|
||||
NormalSecondary: NewSimpleContext(
|
||||
NewBaseContext(NewBaseContextOpts{
|
||||
Kind: types.MAIN_CONTEXT,
|
||||
View: c.Views().Secondary,
|
||||
WindowName: "secondary",
|
||||
Key: NORMAL_SECONDARY_CONTEXT_KEY,
|
||||
Focusable: false,
|
||||
}),
|
||||
),
|
||||
Files: NewWorkingTreeContext(c),
|
||||
Submodules: NewSubmodulesContext(c),
|
||||
Menu: NewMenuContext(c),
|
||||
Remotes: NewRemotesContext(c),
|
||||
Worktrees: NewWorktreesContext(c),
|
||||
RemoteBranches: NewRemoteBranchesContext(c),
|
||||
LocalCommits: NewLocalCommitsContext(c),
|
||||
CommitFiles: commitFilesContext,
|
||||
ReflogCommits: NewReflogCommitsContext(c),
|
||||
SubCommits: NewSubCommitsContext(c),
|
||||
Branches: NewBranchesContext(c),
|
||||
Tags: NewTagsContext(c),
|
||||
Stash: NewStashContext(c),
|
||||
Suggestions: NewSuggestionsContext(c),
|
||||
Normal: NewMainContext(c.Views().Main, "main", NORMAL_MAIN_CONTEXT_KEY, c),
|
||||
NormalSecondary: NewMainContext(c.Views().Secondary, "secondary", NORMAL_SECONDARY_CONTEXT_KEY, c),
|
||||
Staging: NewPatchExplorerContext(
|
||||
c.Views().Staging,
|
||||
"main",
|
||||
|
||||
@@ -54,6 +54,9 @@ func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SimpleContext) FocusLine() {
|
||||
}
|
||||
|
||||
func (self *SimpleContext) HandleRender() {
|
||||
if self.handleRenderFunc != nil {
|
||||
self.handleRenderFunc()
|
||||
|
||||
@@ -46,11 +46,11 @@ func NewSubCommitsContext(
|
||||
return [][]string{}
|
||||
}
|
||||
|
||||
selectedCommitHash := ""
|
||||
var selectedCommitHashPtr *string
|
||||
if c.Context().Current().GetKey() == SUB_COMMITS_CONTEXT_KEY {
|
||||
selectedCommit := viewModel.GetSelected()
|
||||
if selectedCommit != nil {
|
||||
selectedCommitHash = selectedCommit.Hash
|
||||
selectedCommitHashPtr = selectedCommit.HashPtr()
|
||||
}
|
||||
}
|
||||
branches := []*models.Branch{}
|
||||
@@ -72,12 +72,11 @@ func NewSubCommitsContext(
|
||||
c.UserConfig().Gui.ShortTimeFormat,
|
||||
time.Now(),
|
||||
c.UserConfig().Git.ParseEmoji,
|
||||
selectedCommitHash,
|
||||
selectedCommitHashPtr,
|
||||
startIdx,
|
||||
endIdx,
|
||||
shouldShowGraph(c),
|
||||
git_commands.NewNullBisectInfo(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -222,9 +221,26 @@ func (self *SubCommitsContext) RefForAdjustingLineNumberInDiff() string {
|
||||
if commits == nil {
|
||||
return ""
|
||||
}
|
||||
return commits[0].Hash
|
||||
return commits[0].Hash()
|
||||
}
|
||||
|
||||
func (self *SubCommitsContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
|
||||
return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), searchStr)
|
||||
}
|
||||
|
||||
func (self *SubCommitsContext) IndexForGotoBottom() int {
|
||||
commits := self.GetCommits()
|
||||
selectedIdx := self.GetSelectedLineIdx()
|
||||
if selectedIdx >= 0 && selectedIdx < len(commits)-1 {
|
||||
if commits[selectedIdx+1].Status != models.StatusMerged {
|
||||
_, idx, found := lo.FindIndexOf(commits, func(c *models.Commit) bool {
|
||||
return c.Status == models.StatusMerged
|
||||
})
|
||||
if found {
|
||||
return idx - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self.list.Len() - 1
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package traits
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type RangeSelectMode int
|
||||
@@ -85,7 +86,7 @@ func (self *ListCursor) clampValue(value int) int {
|
||||
clampedValue := -1
|
||||
length := self.getLength()
|
||||
if length > 0 {
|
||||
clampedValue = utils.Clamp(value, 0, length-1)
|
||||
clampedValue = lo.Clamp(value, 0, length-1)
|
||||
}
|
||||
|
||||
return clampedValue
|
||||
|
||||
+34
-4
@@ -27,12 +27,11 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
helperCommon := gui.c
|
||||
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
|
||||
reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo)
|
||||
refsHelper := helpers.NewRefsHelper(helperCommon)
|
||||
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon)
|
||||
refsHelper := helpers.NewRefsHelper(helperCommon, rebaseHelper)
|
||||
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon)
|
||||
worktreeHelper := helpers.NewWorktreeHelper(helperCommon, reposHelper, refsHelper, suggestionsHelper)
|
||||
|
||||
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper)
|
||||
|
||||
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
|
||||
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
|
||||
getCommitSummary := func() string {
|
||||
@@ -180,7 +179,8 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
globalController := controllers.NewGlobalController(common)
|
||||
contextLinesController := controllers.NewContextLinesController(common)
|
||||
renameSimilarityThresholdController := controllers.NewRenameSimilarityThresholdController(common)
|
||||
verticalScrollControllerFactory := controllers.NewVerticalScrollControllerFactory(common, &gui.viewBufferManagerMap)
|
||||
verticalScrollControllerFactory := controllers.NewVerticalScrollControllerFactory(common)
|
||||
viewSelectionControllerFactory := controllers.NewViewSelectionControllerFactory(common)
|
||||
|
||||
branchesController := controllers.NewBranchesController(common)
|
||||
gitFlowController := controllers.NewGitFlowController(common)
|
||||
@@ -189,6 +189,8 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
patchExplorerControllerFactory := controllers.NewPatchExplorerControllerFactory(common)
|
||||
stagingController := controllers.NewStagingController(common, gui.State.Contexts.Staging, gui.State.Contexts.StagingSecondary, false)
|
||||
stagingSecondaryController := controllers.NewStagingController(common, gui.State.Contexts.StagingSecondary, gui.State.Contexts.Staging, true)
|
||||
mainViewController := controllers.NewMainViewController(common, gui.State.Contexts.Normal, gui.State.Contexts.NormalSecondary)
|
||||
secondaryViewController := controllers.NewMainViewController(common, gui.State.Contexts.NormalSecondary, gui.State.Contexts.Normal)
|
||||
patchBuildingController := controllers.NewPatchBuildingController(common)
|
||||
snakeController := controllers.NewSnakeController(common)
|
||||
reflogCommitsController := controllers.NewReflogCommitsController(common)
|
||||
@@ -263,6 +265,22 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
))
|
||||
}
|
||||
|
||||
for _, context := range []types.Context{
|
||||
gui.State.Contexts.Files,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
gui.State.Contexts.Tags,
|
||||
gui.State.Contexts.LocalCommits,
|
||||
gui.State.Contexts.ReflogCommits,
|
||||
gui.State.Contexts.SubCommits,
|
||||
gui.State.Contexts.CommitFiles,
|
||||
gui.State.Contexts.Stash,
|
||||
} {
|
||||
controllers.AttachControllers(context, controllers.NewSwitchToFocusedMainViewController(
|
||||
common, context,
|
||||
))
|
||||
}
|
||||
|
||||
for _, context := range []controllers.ContainsCommits{
|
||||
gui.State.Contexts.LocalCommits,
|
||||
gui.State.Contexts.ReflogCommits,
|
||||
@@ -306,6 +324,18 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
mergeConflictsController,
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.Normal,
|
||||
mainViewController,
|
||||
verticalScrollControllerFactory.Create(gui.State.Contexts.Normal),
|
||||
viewSelectionControllerFactory.Create(gui.State.Contexts.Normal),
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.NormalSecondary,
|
||||
secondaryViewController,
|
||||
verticalScrollControllerFactory.Create(gui.State.Contexts.NormalSecondary),
|
||||
viewSelectionControllerFactory.Create(gui.State.Contexts.NormalSecondary),
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.Files,
|
||||
filesController,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ func AttachControllers(context types.Context, controllers ...types.IController)
|
||||
context.AddKeybindingsFn(controller.GetKeybindings)
|
||||
context.AddMouseKeybindingsFn(controller.GetMouseKeybindings)
|
||||
context.AddOnClickFn(controller.GetOnClick())
|
||||
context.AddOnClickFocusedMainViewFn(controller.GetOnClickFocusedMainView())
|
||||
context.AddOnRenderToMainFn(controller.GetOnRenderToMain())
|
||||
context.AddOnFocusFn(controller.GetOnFocus())
|
||||
context.AddOnFocusLostFn(controller.GetOnFocusLost())
|
||||
|
||||
@@ -19,6 +19,10 @@ func (self *baseController) GetOnClick() func() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *baseController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *baseController) GetOnRenderToMain() func() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -80,6 +80,17 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
|
||||
GetDisabledReason: self.require(self.singleItemSelected()),
|
||||
Description: self.c.Tr.CreateNewBranchFromCommit,
|
||||
},
|
||||
{
|
||||
// Putting this in BasicCommitsController even though we really only want it in the commits
|
||||
// panel. But I find it important that this ends up next to "New Branch", and I couldn't
|
||||
// find another way to achieve this. It's not such a big deal to have it in subcommits and
|
||||
// reflog too, I'd say.
|
||||
Key: opts.GetKey(opts.Config.Branches.MoveCommitsToNewBranch),
|
||||
Handler: self.c.Helpers().Refs.MoveCommitsToNewBranch,
|
||||
GetDisabledReason: self.c.Helpers().Refs.CanMoveCommitsToNewBranch,
|
||||
Description: self.c.Tr.MoveCommitsToNewBranch,
|
||||
Tooltip: self.c.Tr.MoveCommitsToNewBranchTooltip,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
|
||||
Handler: self.withItem(self.createResetMenu),
|
||||
@@ -142,7 +153,7 @@ func (self *BasicCommitsController) getCommitMessageBody(hash string) string {
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) error {
|
||||
commitMessageBody := self.getCommitMessageBody(commit.Hash)
|
||||
commitMessageBody := self.getCommitMessageBody(commit.Hash())
|
||||
var commitMessageBodyDisabled *types.DisabledReason
|
||||
if commitMessageBody == "" {
|
||||
commitMessageBodyDisabled = &types.DisabledReason{
|
||||
@@ -224,16 +235,16 @@ func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) e
|
||||
|
||||
func (self *BasicCommitsController) copyCommitHashToClipboard(commit *models.Commit) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CopyCommitHashToClipboard)
|
||||
if err := self.c.OS().CopyToClipboard(commit.Hash); err != nil {
|
||||
if err := self.c.OS().CopyToClipboard(commit.Hash()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.c.Toast(fmt.Sprintf("'%s' %s", commit.Hash, self.c.Tr.CopiedToClipboard))
|
||||
self.c.Toast(fmt.Sprintf("'%s' %s", commit.Hash(), self.c.Tr.CopiedToClipboard))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) copyCommitURLToClipboard(commit *models.Commit) error {
|
||||
url, err := self.c.Helpers().Host.GetCommitURL(commit.Hash)
|
||||
url, err := self.c.Helpers().Host.GetCommitURL(commit.Hash())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -248,7 +259,7 @@ func (self *BasicCommitsController) copyCommitURLToClipboard(commit *models.Comm
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) copyCommitDiffToClipboard(commit *models.Commit) error {
|
||||
diff, err := self.c.Git().Commit.GetCommitDiff(commit.Hash)
|
||||
diff, err := self.c.Git().Commit.GetCommitDiff(commit.Hash())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -263,7 +274,7 @@ func (self *BasicCommitsController) copyCommitDiffToClipboard(commit *models.Com
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) copyAuthorToClipboard(commit *models.Commit) error {
|
||||
author, err := self.c.Git().Commit.GetCommitAuthor(commit.Hash)
|
||||
author, err := self.c.Git().Commit.GetCommitAuthor(commit.Hash())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -280,7 +291,7 @@ func (self *BasicCommitsController) copyAuthorToClipboard(commit *models.Commit)
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) copyCommitMessageToClipboard(commit *models.Commit) error {
|
||||
message, err := self.c.Git().Commit.GetCommitMessage(commit.Hash)
|
||||
message, err := self.c.Git().Commit.GetCommitMessage(commit.Hash())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -305,7 +316,7 @@ func (self *BasicCommitsController) copyCommitMessageBodyToClipboard(commitMessa
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) copyCommitSubjectToClipboard(commit *models.Commit) error {
|
||||
message, err := self.c.Git().Commit.GetCommitSubject(commit.Hash)
|
||||
message, err := self.c.Git().Commit.GetCommitSubject(commit.Hash())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -332,7 +343,7 @@ func (self *BasicCommitsController) copyCommitTagsToClipboard(commit *models.Com
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) openInBrowser(commit *models.Commit) error {
|
||||
url, err := self.c.Helpers().Host.GetCommitURL(commit.Hash)
|
||||
url, err := self.c.Helpers().Host.GetCommitURL(commit.Hash())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -350,7 +361,7 @@ func (self *BasicCommitsController) newBranch(commit *models.Commit) error {
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) createResetMenu(commit *models.Commit) error {
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu(commit.Hash)
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu(commit.Hash())
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) checkout(commit *models.Commit) error {
|
||||
@@ -363,13 +374,9 @@ func (self *BasicCommitsController) copyRange(*models.Commit) error {
|
||||
|
||||
func (self *BasicCommitsController) canCopyCommits(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
|
||||
for _, commit := range selectedCommits {
|
||||
if commit.Hash == "" {
|
||||
if commit.Hash() == "" {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CannotCherryPickNonCommit, ShowErrorInPanel: true}
|
||||
}
|
||||
|
||||
if commit.IsMerge() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CannotCherryPickMergeCommit, ShowErrorInPanel: true}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -69,7 +69,7 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c
|
||||
// Originally we were allowing the user to, from the bisect menu, select whether
|
||||
// they were talking about the selected commit or the current bisect commit,
|
||||
// and that was a bit confusing (and required extra keypresses).
|
||||
selectCurrentAfter := info.GetCurrentHash() == "" || info.GetCurrentHash() == commit.Hash
|
||||
selectCurrentAfter := info.GetCurrentHash() == "" || info.GetCurrentHash() == commit.Hash()
|
||||
// we need to wait to reselect if our bisect commits aren't ancestors of our 'start'
|
||||
// ref, because we'll be reloading our commits in that case.
|
||||
waitToReselect := selectCurrentAfter && !self.c.Git().Bisect.ReachableFromStart(info)
|
||||
@@ -79,7 +79,7 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c
|
||||
// use the selected commit in that case.
|
||||
|
||||
bisecting := info.GetCurrentHash() != ""
|
||||
hashToMark := lo.Ternary(bisecting, info.GetCurrentHash(), commit.Hash)
|
||||
hashToMark := lo.Ternary(bisecting, info.GetCurrentHash(), commit.Hash())
|
||||
shortHashToMark := utils.ShortHash(hashToMark)
|
||||
|
||||
// For marking a commit as bad, when we're not already bisecting, we require
|
||||
@@ -131,12 +131,12 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c
|
||||
Key: 's',
|
||||
},
|
||||
}
|
||||
if info.GetCurrentHash() != "" && info.GetCurrentHash() != commit.Hash {
|
||||
if info.GetCurrentHash() != "" && info.GetCurrentHash() != commit.Hash() {
|
||||
menuItems = append(menuItems, lo.ToPtr(types.MenuItem{
|
||||
Label: fmt.Sprintf(self.c.Tr.Bisect.SkipSelected, commit.ShortHash()),
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.BisectSkip)
|
||||
if err := self.c.Git().Bisect.Skip(commit.Hash); err != nil {
|
||||
if err := self.c.Git().Bisect.Skip(commit.Hash()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo,
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.c.Git().Bisect.Mark(commit.Hash, info.NewTerm()); err != nil {
|
||||
if err := self.c.Git().Bisect.Mark(commit.Hash(), info.NewTerm()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo,
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.c.Git().Bisect.Mark(commit.Hash, info.OldTerm()); err != nil {
|
||||
if err := self.c.Git().Bisect.Mark(commit.Hash(), info.OldTerm()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ func (self *BisectController) selectCurrentBisectCommit() {
|
||||
if info.GetCurrentHash() != "" {
|
||||
// find index of commit with that hash, move cursor to that.
|
||||
for i, commit := range self.c.Model().Commits {
|
||||
if commit.Hash == info.GetCurrentHash() {
|
||||
if commit.Hash() == info.GetCurrentHash() {
|
||||
self.context().SetSelection(i)
|
||||
self.context().HandleFocus(types.OnFocusOpts{})
|
||||
break
|
||||
|
||||
@@ -57,6 +57,13 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
||||
Description: self.c.Tr.NewBranch,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Branches.MoveCommitsToNewBranch),
|
||||
Handler: self.c.Helpers().Refs.MoveCommitsToNewBranch,
|
||||
GetDisabledReason: self.c.Helpers().Refs.CanMoveCommitsToNewBranch,
|
||||
Description: self.c.Tr.MoveCommitsToNewBranch,
|
||||
Tooltip: self.c.Tr.MoveCommitsToNewBranchTooltip,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Branches.CreatePullRequest),
|
||||
Handler: self.withItem(self.handleCreatePullRequest),
|
||||
|
||||
@@ -38,6 +38,10 @@ func (self *CommitDescriptionController) GetKeybindings(opts types.KeybindingsOp
|
||||
Key: opts.GetKey(opts.Config.Universal.ConfirmInEditor),
|
||||
Handler: self.confirm,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.ConfirmInEditorAlt),
|
||||
Handler: self.confirm,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.CommitMessage.CommitMenu),
|
||||
Handler: self.openCommitMenu,
|
||||
@@ -63,10 +67,27 @@ func (self *CommitDescriptionController) GetMouseKeybindings(opts types.Keybindi
|
||||
|
||||
func (self *CommitDescriptionController) GetOnFocus() func(types.OnFocusOpts) {
|
||||
return func(types.OnFocusOpts) {
|
||||
self.c.Views().CommitDescription.Footer = utils.ResolvePlaceholderString(self.c.Tr.CommitDescriptionFooter,
|
||||
map[string]string{
|
||||
"confirmInEditorKeybinding": keybindings.Label(self.c.UserConfig().Keybinding.Universal.ConfirmInEditor),
|
||||
})
|
||||
footer := ""
|
||||
if self.c.UserConfig().Keybinding.Universal.ConfirmInEditor != "<disabled>" || self.c.UserConfig().Keybinding.Universal.ConfirmInEditorAlt != "<disabled>" {
|
||||
if self.c.UserConfig().Keybinding.Universal.ConfirmInEditor == "<disabled>" {
|
||||
footer = utils.ResolvePlaceholderString(self.c.Tr.CommitDescriptionFooter,
|
||||
map[string]string{
|
||||
"confirmInEditorKeybinding": keybindings.Label(self.c.UserConfig().Keybinding.Universal.ConfirmInEditorAlt),
|
||||
})
|
||||
} else if self.c.UserConfig().Keybinding.Universal.ConfirmInEditorAlt == "<disabled>" {
|
||||
footer = utils.ResolvePlaceholderString(self.c.Tr.CommitDescriptionFooter,
|
||||
map[string]string{
|
||||
"confirmInEditorKeybinding": keybindings.Label(self.c.UserConfig().Keybinding.Universal.ConfirmInEditor),
|
||||
})
|
||||
} else {
|
||||
footer = utils.ResolvePlaceholderString(self.c.Tr.CommitDescriptionFooterTwoBindings,
|
||||
map[string]string{
|
||||
"confirmInEditorKeybinding1": keybindings.Label(self.c.UserConfig().Keybinding.Universal.ConfirmInEditor),
|
||||
"confirmInEditorKeybinding2": keybindings.Label(self.c.UserConfig().Keybinding.Universal.ConfirmInEditorAlt),
|
||||
})
|
||||
}
|
||||
}
|
||||
self.c.Views().CommitDescription.Footer = footer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
@@ -135,17 +137,6 @@ func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []
|
||||
return bindings
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
|
||||
return []*gocui.ViewMouseBinding{
|
||||
{
|
||||
ViewName: "patchBuilding",
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: self.onClickMain,
|
||||
FocusedView: self.context().GetViewName(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) context() *context.CommitFilesContext {
|
||||
return self.c.Contexts().CommitFiles
|
||||
}
|
||||
@@ -163,13 +154,8 @@ func (self *CommitFilesController) GetOnRenderToMain() func() {
|
||||
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
|
||||
task := types.NewRunPtyTask(cmdObj.GetCmd())
|
||||
|
||||
pair := self.c.MainViewPairs().Normal
|
||||
if node.File != nil {
|
||||
pair = self.c.MainViewPairs().PatchBuilding
|
||||
}
|
||||
|
||||
self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||
Pair: pair,
|
||||
Pair: self.c.MainViewPairs().Normal,
|
||||
Main: &types.ViewUpdateOpts{
|
||||
Title: self.c.Tr.Patch,
|
||||
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
|
||||
@@ -180,14 +166,6 @@ func (self *CommitFilesController) GetOnRenderToMain() func() {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error {
|
||||
node := self.context().GetSelected()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: opts.Y})
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) copyDiffToClipboard(path string, toastMessage string) error {
|
||||
from, to := self.context().GetFromAndToForDiff()
|
||||
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
||||
@@ -404,6 +382,11 @@ func (self *CommitFilesController) openDiffTool(node *filetree.CommitFileNode) e
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) toggleForPatch(selectedNodes []*filetree.CommitFileNode) error {
|
||||
if self.c.AppState.DiffContextSize == 0 {
|
||||
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage,
|
||||
keybindings.Label(self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView))
|
||||
}
|
||||
|
||||
toggle := func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.UpdatingPatch, func(gocui.Task) error {
|
||||
if !self.c.Git().Patch.PatchBuilder.Active() {
|
||||
@@ -495,6 +478,11 @@ func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode
|
||||
return self.handleToggleCommitFileDirCollapsed(node)
|
||||
}
|
||||
|
||||
if self.c.AppState.DiffContextSize == 0 {
|
||||
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage,
|
||||
keybindings.Label(self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView))
|
||||
}
|
||||
|
||||
enterTheFile := func() error {
|
||||
if !self.c.Git().Patch.PatchBuilder.Active() {
|
||||
if err := self.startPatchBuilder(); err != nil {
|
||||
@@ -555,6 +543,16 @@ func (self *CommitFilesController) expandAll() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error {
|
||||
return func(mainViewName string, clickedLineIdx int) error {
|
||||
node := self.getSelectedItem()
|
||||
if node != nil && node.File != nil {
|
||||
return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: mainViewName, ClickedViewLineIdx: clickedLineIdx})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: these functions are identical to those in files_controller.go (except for types) and
|
||||
// could also be cleaned up with some generics
|
||||
func normalisedSelectedCommitFileNodes(selectedNodes []*filetree.CommitFileNode) []*filetree.CommitFileNode {
|
||||
|
||||
@@ -22,6 +22,8 @@ var CONTEXT_KEYS_SHOWING_DIFFS = []types.ContextKey{
|
||||
context.STAGING_SECONDARY_CONTEXT_KEY,
|
||||
context.PATCH_BUILDING_MAIN_CONTEXT_KEY,
|
||||
context.PATCH_BUILDING_SECONDARY_CONTEXT_KEY,
|
||||
context.NORMAL_MAIN_CONTEXT_KEY,
|
||||
context.NORMAL_SECONDARY_CONTEXT_KEY,
|
||||
}
|
||||
|
||||
type ContextLinesController struct {
|
||||
@@ -97,7 +99,7 @@ func (self *ContextLinesController) applyChange() error {
|
||||
self.c.Toast(fmt.Sprintf(self.c.Tr.DiffContextSizeChanged, self.c.AppState.DiffContextSize))
|
||||
self.c.SaveAppStateAndLogError()
|
||||
|
||||
currentContext := self.c.Context().CurrentStatic()
|
||||
currentContext := self.currentSidePanel()
|
||||
switch currentContext.GetKey() {
|
||||
// we make an exception for our staging and patch building contexts because they actually need to refresh their state afterwards.
|
||||
case context.PATCH_BUILDING_MAIN_CONTEXT_KEY:
|
||||
@@ -121,6 +123,18 @@ func (self *ContextLinesController) checkCanChangeContext() error {
|
||||
func (self *ContextLinesController) isShowingDiff() bool {
|
||||
return lo.Contains(
|
||||
CONTEXT_KEYS_SHOWING_DIFFS,
|
||||
self.c.Context().CurrentStatic().GetKey(),
|
||||
self.currentSidePanel().GetKey(),
|
||||
)
|
||||
}
|
||||
|
||||
func (self *ContextLinesController) currentSidePanel() types.Context {
|
||||
currentContext := self.c.Context().CurrentStatic()
|
||||
if currentContext.GetKey() == context.NORMAL_MAIN_CONTEXT_KEY ||
|
||||
currentContext.GetKey() == context.NORMAL_SECONDARY_CONTEXT_KEY {
|
||||
if sidePanelContext := self.c.Context().NextInStack(currentContext); sidePanelContext != nil {
|
||||
return sidePanelContext
|
||||
}
|
||||
}
|
||||
|
||||
return currentContext
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type CustomPatchOptionsMenuAction struct {
|
||||
@@ -44,10 +44,10 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
|
||||
},
|
||||
}
|
||||
|
||||
if self.c.Git().Patch.PatchBuilder.CanRebase && self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_NONE {
|
||||
if self.c.Git().Patch.PatchBuilder.CanRebase && self.c.Git().Status.WorkingTreeState().None() {
|
||||
menuItems = append(menuItems, []*types.MenuItem{
|
||||
{
|
||||
Label: fmt.Sprintf(self.c.Tr.RemovePatchFromOriginalCommit, self.c.Git().Patch.PatchBuilder.To),
|
||||
Label: fmt.Sprintf(self.c.Tr.RemovePatchFromOriginalCommit, utils.ShortHash(self.c.Git().Patch.PatchBuilder.To)),
|
||||
Tooltip: self.c.Tr.RemovePatchFromOriginalCommitTooltip,
|
||||
OnPress: self.handleDeletePatchFromCommit,
|
||||
Key: 'd',
|
||||
@@ -64,11 +64,17 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
|
||||
OnPress: self.handlePullPatchIntoNewCommit,
|
||||
Key: 'n',
|
||||
},
|
||||
{
|
||||
Label: self.c.Tr.MovePatchIntoNewCommitBefore,
|
||||
Tooltip: self.c.Tr.MovePatchIntoNewCommitBeforeTooltip,
|
||||
OnPress: self.handlePullPatchIntoNewCommitBefore,
|
||||
Key: 'N',
|
||||
},
|
||||
}...)
|
||||
|
||||
if self.c.Context().Current().GetKey() == self.c.Contexts().LocalCommits.GetKey() {
|
||||
selectedCommit := self.c.Contexts().LocalCommits.GetSelected()
|
||||
if selectedCommit != nil && self.c.Git().Patch.PatchBuilder.To != selectedCommit.Hash {
|
||||
if selectedCommit != nil && self.c.Git().Patch.PatchBuilder.To != selectedCommit.Hash() {
|
||||
|
||||
var disabledReason *types.DisabledReason
|
||||
if self.c.Contexts().LocalCommits.AreMultipleItemsSelected() {
|
||||
@@ -81,7 +87,7 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
|
||||
append(
|
||||
[]*types.MenuItem{
|
||||
{
|
||||
Label: fmt.Sprintf(self.c.Tr.MovePatchToSelectedCommit, selectedCommit.Hash),
|
||||
Label: fmt.Sprintf(self.c.Tr.MovePatchToSelectedCommit, selectedCommit.Hash()),
|
||||
Tooltip: self.c.Tr.MovePatchToSelectedCommitTooltip,
|
||||
OnPress: self.handleMovePatchToSelectedCommit,
|
||||
Key: 'm',
|
||||
@@ -107,7 +113,7 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
|
||||
|
||||
func (self *CustomPatchOptionsMenuAction) getPatchCommitIndex() int {
|
||||
for index, commit := range self.c.Model().Commits {
|
||||
if commit.Hash == self.c.Git().Patch.PatchBuilder.To {
|
||||
if commit.Hash() == self.c.Git().Patch.PatchBuilder.To {
|
||||
return index
|
||||
}
|
||||
}
|
||||
@@ -115,7 +121,7 @@ func (self *CustomPatchOptionsMenuAction) getPatchCommitIndex() int {
|
||||
}
|
||||
|
||||
func (self *CustomPatchOptionsMenuAction) validateNormalWorkingTreeState() (bool, error) {
|
||||
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
|
||||
if self.c.Git().Status.WorkingTreeState().Any() {
|
||||
return false, errors.New(self.c.Tr.CantPatchWhileRebasingError)
|
||||
}
|
||||
return true, nil
|
||||
@@ -223,6 +229,41 @@ func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommitBefore() error {
|
||||
if ok, err := self.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
self.returnFocusFromPatchExplorerIfNecessary()
|
||||
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
self.c.Helpers().Commits.OpenCommitMessagePanel(
|
||||
&helpers.OpenCommitMessagePanelOpts{
|
||||
// Pass a commit index of one less than the moved-from commit, so that
|
||||
// you can press up arrow once to recall the original commit message:
|
||||
CommitIndex: commitIndex - 1,
|
||||
InitialMessage: "",
|
||||
SummaryTitle: self.c.Tr.CommitSummaryTitle,
|
||||
DescriptionTitle: self.c.Tr.CommitDescriptionTitle,
|
||||
PreserveMessage: false,
|
||||
OnConfirm: func(summary string, description string) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
|
||||
self.c.Helpers().Commits.CloseCommitMessagePanel()
|
||||
self.c.LogAction(self.c.Tr.Actions.MovePatchIntoNewCommit)
|
||||
err := self.c.Git().Patch.PullPatchIntoNewCommitBefore(self.c.Model().Commits, commitIndex, summary, description)
|
||||
if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err); err != nil {
|
||||
return err
|
||||
}
|
||||
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
|
||||
return nil
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CustomPatchOptionsMenuAction) handleApplyPatch(reverse bool) error {
|
||||
self.returnFocusFromPatchExplorerIfNecessary()
|
||||
|
||||
|
||||
@@ -208,36 +208,12 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
||||
|
||||
func (self *FilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
|
||||
return []*gocui.ViewMouseBinding{
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: self.onClickMain,
|
||||
FocusedView: self.context().GetViewName(),
|
||||
},
|
||||
{
|
||||
ViewName: "patchBuilding",
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: self.onClickMain,
|
||||
FocusedView: self.context().GetViewName(),
|
||||
},
|
||||
{
|
||||
ViewName: "mergeConflicts",
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: self.onClickMain,
|
||||
FocusedView: self.context().GetViewName(),
|
||||
},
|
||||
{
|
||||
ViewName: "secondary",
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: self.onClickSecondary,
|
||||
FocusedView: self.context().GetViewName(),
|
||||
},
|
||||
{
|
||||
ViewName: "patchBuildingSecondary",
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: self.onClickSecondary,
|
||||
FocusedView: self.context().GetViewName(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,11 +280,6 @@ func (self *FilesController) GetOnRenderToMain() func() {
|
||||
|
||||
self.c.Helpers().MergeConflicts.ResetMergeState()
|
||||
|
||||
pair := self.c.MainViewPairs().Normal
|
||||
if node.File != nil {
|
||||
pair = self.c.MainViewPairs().Staging
|
||||
}
|
||||
|
||||
split := self.c.UserConfig().Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
|
||||
mainShowsStaged := !split && node.GetHasStagedChanges()
|
||||
|
||||
@@ -318,7 +289,7 @@ func (self *FilesController) GetOnRenderToMain() func() {
|
||||
title = self.c.Tr.StagedChanges
|
||||
}
|
||||
refreshOpts := types.RefreshMainOpts{
|
||||
Pair: pair,
|
||||
Pair: self.c.MainViewPairs().Normal,
|
||||
Main: &types.ViewUpdateOpts{
|
||||
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
|
||||
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
|
||||
@@ -352,6 +323,16 @@ func (self *FilesController) GetOnClick() func() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error {
|
||||
return func(mainViewName string, clickedLineIdx int) error {
|
||||
node := self.getSelectedItem()
|
||||
if node != nil && node.File != nil {
|
||||
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: mainViewName, ClickedViewLineIdx: clickedLineIdx})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// if we are dealing with a status for which there is no key in this map,
|
||||
// then we won't optimistically render: we'll just let `git status` tell
|
||||
// us what the new status is.
|
||||
@@ -574,7 +555,8 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
|
||||
return self.handleNonInlineConflict(file)
|
||||
}
|
||||
|
||||
self.c.Context().Push(self.c.Contexts().Staging, opts)
|
||||
context := lo.Ternary(opts.ClickedWindowName == "secondary", self.c.Contexts().StagingSecondary, self.c.Contexts().Staging)
|
||||
self.c.Context().Push(context, opts)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -829,12 +811,12 @@ func (self *FilesController) handleAmendCommitPress() error {
|
||||
func (self *FilesController) isResolvingConflicts() bool {
|
||||
commits := self.c.Model().Commits
|
||||
for _, c := range commits {
|
||||
if c.Status != models.StatusRebasing {
|
||||
break
|
||||
}
|
||||
if c.Action == models.ActionConflict {
|
||||
if c.Status == models.StatusConflicted {
|
||||
return true
|
||||
}
|
||||
if !c.IsTODO() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1205,32 +1187,25 @@ func (self *FilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error
|
||||
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: opts.Y})
|
||||
}
|
||||
|
||||
func (self *FilesController) onClickSecondary(opts gocui.ViewMouseBindingOpts) error {
|
||||
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "secondary", ClickedViewLineIdx: opts.Y})
|
||||
}
|
||||
|
||||
func (self *FilesController) fetch() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.FetchingStatus, func(task gocui.Task) error {
|
||||
if err := self.fetchAux(task); err != nil {
|
||||
return err
|
||||
self.c.LogAction("Fetch")
|
||||
err := self.c.Git().Sync.Fetch(task)
|
||||
|
||||
if err != nil && strings.Contains(err.Error(), "exit status 128") {
|
||||
return errors.New(self.c.Tr.PassUnameWrong)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
|
||||
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.SYNC})
|
||||
|
||||
if err == nil {
|
||||
err = self.c.Helpers().BranchesHelper.AutoForwardBranches()
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilesController) fetchAux(task gocui.Task) (err error) {
|
||||
self.c.LogAction("Fetch")
|
||||
err = self.c.Git().Sync.Fetch(task)
|
||||
|
||||
if err != nil && strings.Contains(err.Error(), "exit status 128") {
|
||||
return errors.New(self.c.Tr.PassUnameWrong)
|
||||
}
|
||||
|
||||
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Couldn't think of a better term than 'normalised'. Alas.
|
||||
// The idea is that when you select a range of nodes, you will often have both
|
||||
// a node and its parent node selected. If we are trying to discard changes to the
|
||||
|
||||
@@ -35,11 +35,12 @@ func (self *GlobalController) GetKeybindings(opts types.KeybindingsOpts) []*type
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.CreateRebaseOptionsMenu),
|
||||
Handler: opts.Guards.NoPopupPanel(self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu),
|
||||
Description: self.c.Tr.ViewMergeRebaseOptions,
|
||||
Tooltip: self.c.Tr.ViewMergeRebaseOptionsTooltip,
|
||||
OpensMenu: true,
|
||||
Key: opts.GetKey(opts.Config.Universal.CreateRebaseOptionsMenu),
|
||||
Handler: opts.Guards.NoPopupPanel(self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu),
|
||||
Description: self.c.Tr.ViewMergeRebaseOptions,
|
||||
Tooltip: self.c.Tr.ViewMergeRebaseOptionsTooltip,
|
||||
OpensMenu: true,
|
||||
GetDisabledReason: self.canShowRebaseOptions,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.Refresh),
|
||||
@@ -191,3 +192,12 @@ func (self *GlobalController) escape() error {
|
||||
func (self *GlobalController) toggleWhitespace() error {
|
||||
return (&ToggleWhitespaceAction{c: self.c}).Call()
|
||||
}
|
||||
|
||||
func (self *GlobalController) canShowRebaseOptions() *types.DisabledReason {
|
||||
if self.c.Model().WorkingTreeStateAtLastCommitRefresh.None() {
|
||||
return &types.DisabledReason{
|
||||
Text: self.c.Tr.NotMergingOrRebasing,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
@@ -263,3 +264,38 @@ func (self *BranchesHelper) deleteRemoteBranches(remoteBranches []*models.Remote
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BranchesHelper) AutoForwardBranches() error {
|
||||
if self.c.UserConfig().Git.AutoForwardBranches == "none" {
|
||||
return nil
|
||||
}
|
||||
|
||||
branches := self.c.Model().Branches
|
||||
if len(branches) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
allBranches := self.c.UserConfig().Git.AutoForwardBranches == "allBranches"
|
||||
updateCommands := ""
|
||||
// The first branch is the currently checked out branch; skip it
|
||||
for _, branch := range branches[1:] {
|
||||
if branch.RemoteBranchStoredLocally() && (allBranches || lo.Contains(self.c.UserConfig().Git.MainBranches, branch.Name)) {
|
||||
isStrictlyBehind := branch.IsBehindForPull() && !branch.IsAheadForPull()
|
||||
if isStrictlyBehind {
|
||||
updateCommands += fmt.Sprintf("update %s %s %s\n", branch.FullRefName(), branch.FullUpstreamRefName(), branch.CommitHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updateCommands == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.AutoForwardBranches)
|
||||
self.c.LogCommand(strings.TrimRight(updateCommands, "\n"), false)
|
||||
err := self.c.Git().Branch.UpdateBranchRefs(updateCommands)
|
||||
|
||||
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES}, Mode: types.SYNC})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (self *CherryPickHelper) CopyRange(commitsList []*models.Commit, context ty
|
||||
commitSet := self.getData().SelectedHashSet()
|
||||
|
||||
allCommitsCopied := lo.EveryBy(commitsList[startIdx:endIdx+1], func(commit *models.Commit) bool {
|
||||
return commitSet.Includes(commit.Hash)
|
||||
return commitSet.Includes(commit.Hash())
|
||||
})
|
||||
|
||||
// if all selected commits are already copied, we'll uncopy them
|
||||
@@ -77,41 +77,23 @@ func (self *CherryPickHelper) Paste() error {
|
||||
"numCommits": strconv.Itoa(len(self.getData().CherryPickedCommits)),
|
||||
}),
|
||||
HandleConfirm: func() error {
|
||||
isInRebase, err := self.c.Git().Status.IsInInteractiveRebase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isInRebase {
|
||||
if err := self.c.Git().Rebase.CherryPickCommitsDuringRebase(self.getData().CherryPickedCommits); err != nil {
|
||||
return err
|
||||
}
|
||||
err = self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.Reset()
|
||||
}
|
||||
|
||||
return self.c.WithWaitingStatus(self.c.Tr.CherryPickingStatus, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CherryPick)
|
||||
err := self.c.Git().Rebase.CherryPickCommits(self.getData().CherryPickedCommits)
|
||||
err = self.rebaseHelper.CheckMergeOrRebase(err)
|
||||
result := self.c.Git().Rebase.CherryPickCommits(self.getData().CherryPickedCommits)
|
||||
err := self.rebaseHelper.CheckMergeOrRebase(result)
|
||||
if err != nil {
|
||||
return err
|
||||
return result
|
||||
}
|
||||
|
||||
// If we're in an interactive rebase at this point, it must
|
||||
// If we're in the cherry-picking state at this point, it must
|
||||
// be because there were conflicts. Don't clear the copied
|
||||
// commits in this case, since we might want to abort and
|
||||
// try pasting them again.
|
||||
isInRebase, err = self.c.Git().Status.IsInInteractiveRebase()
|
||||
if err != nil {
|
||||
return err
|
||||
// commits in this case, since we might want to abort and try
|
||||
// pasting them again.
|
||||
isInCherryPick, result := self.c.Git().Status.IsInCherryPick()
|
||||
if result != nil {
|
||||
return result
|
||||
}
|
||||
if !isInRebase {
|
||||
if !isInCherryPick {
|
||||
self.getData().DidPaste = true
|
||||
self.rerender()
|
||||
}
|
||||
|
||||
@@ -157,11 +157,8 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp
|
||||
self.c.Context().Push(self.c.Contexts().CommitMessage, types.OnFocusOpts{})
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) OnCommitSuccess() {
|
||||
// if we have a preserved message we want to clear it on success
|
||||
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
|
||||
self.c.Contexts().CommitMessage.SetPreservedMessageAndLogError("")
|
||||
}
|
||||
func (self *CommitsHelper) ClearPreservedCommitMessage() {
|
||||
self.c.Contexts().CommitMessage.SetPreservedMessageAndLogError("")
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) HandleCommitConfirm() error {
|
||||
|
||||
@@ -66,7 +66,7 @@ func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(commit *models.Comm
|
||||
return task
|
||||
}
|
||||
|
||||
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash, self.c.Modes().Filtering.GetPath())
|
||||
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash(), self.c.Modes().Filtering.GetPath())
|
||||
return types.NewRunPtyTask(cmdObj.GetCmd())
|
||||
}
|
||||
|
||||
|
||||
@@ -342,6 +342,6 @@ func (self *FixupHelper) blameAddedLines(commits []*models.Commit, addedLineHunk
|
||||
|
||||
func (self *FixupHelper) findCommit(commits []*models.Commit, hash string) (*models.Commit, int, bool) {
|
||||
return lo.FindIndexOf(commits, func(commit *models.Commit) bool {
|
||||
return commit.Hash == hash
|
||||
return commit.Hash() == hash
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func NewGpgHelper(c *HelperCommon) *GpgHelper {
|
||||
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
|
||||
// fix this bug, or just stop running subprocesses from within there, given that
|
||||
// we don't need to see a loading status if we're in a subprocess.
|
||||
func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, configKey git_commands.GpgConfigKey, waitingStatus string, onSuccess func() error, refreshScope []types.RefreshableView) error {
|
||||
func (self *GpgHelper) WithGpgHandling(cmdObj *oscommands.CmdObj, configKey git_commands.GpgConfigKey, waitingStatus string, onSuccess func() error, refreshScope []types.RefreshableView) error {
|
||||
useSubprocess := self.c.Git().Config.NeedsGpgSubprocess(configKey)
|
||||
if useSubprocess {
|
||||
success, err := self.c.RunSubprocess(cmdObj)
|
||||
@@ -42,7 +42,7 @@ func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, configKey git_
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GpgHelper) runAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error, refreshScope []types.RefreshableView) error {
|
||||
func (self *GpgHelper) runAndStream(cmdObj *oscommands.CmdObj, waitingStatus string, onSuccess func() error, refreshScope []types.RefreshableView) error {
|
||||
return self.c.WithWaitingStatus(waitingStatus, func(gocui.Task) error {
|
||||
if err := cmdObj.StreamOutput().Run(); err != nil {
|
||||
_ = self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: refreshScope})
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
@@ -18,17 +17,14 @@ import (
|
||||
)
|
||||
|
||||
type MergeAndRebaseHelper struct {
|
||||
c *HelperCommon
|
||||
refsHelper *RefsHelper
|
||||
c *HelperCommon
|
||||
}
|
||||
|
||||
func NewMergeAndRebaseHelper(
|
||||
c *HelperCommon,
|
||||
refsHelper *RefsHelper,
|
||||
) *MergeAndRebaseHelper {
|
||||
return &MergeAndRebaseHelper{
|
||||
c: c,
|
||||
refsHelper: refsHelper,
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +47,7 @@ func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error {
|
||||
{option: REBASE_OPTION_ABORT, key: 'a'},
|
||||
}
|
||||
|
||||
if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
||||
if self.c.Git().Status.WorkingTreeState().CanSkip() {
|
||||
options = append(options, optionAndKey{
|
||||
option: REBASE_OPTION_SKIP, key: 's',
|
||||
})
|
||||
@@ -67,13 +63,7 @@ func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error {
|
||||
}
|
||||
})
|
||||
|
||||
var title string
|
||||
if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_MERGING {
|
||||
title = self.c.Tr.MergeOptionsTitle
|
||||
} else {
|
||||
title = self.c.Tr.RebaseOptionsTitle
|
||||
}
|
||||
|
||||
title := self.c.Git().Status.WorkingTreeState().OptionsMenuTitle(self.c.Tr)
|
||||
return self.c.Menu(types.CreateMenuOptions{Title: title, Items: menuItems})
|
||||
}
|
||||
|
||||
@@ -84,12 +74,13 @@ func (self *MergeAndRebaseHelper) ContinueRebase() error {
|
||||
func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
|
||||
status := self.c.Git().Status.WorkingTreeState()
|
||||
|
||||
if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING {
|
||||
if status.None() {
|
||||
return errors.New(self.c.Tr.NotMergingOrRebasing)
|
||||
}
|
||||
|
||||
self.c.LogAction(fmt.Sprintf("Merge/Rebase: %s", command))
|
||||
if status == enums.REBASE_MODE_REBASING {
|
||||
effectiveStatus := status.Effective()
|
||||
if effectiveStatus == models.WORKING_TREE_STATE_REBASING {
|
||||
todoFile, err := os.ReadFile(
|
||||
filepath.Join(self.c.Git().RepoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"),
|
||||
)
|
||||
@@ -103,23 +94,15 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
|
||||
}
|
||||
}
|
||||
|
||||
commandType := ""
|
||||
switch status {
|
||||
case enums.REBASE_MODE_MERGING:
|
||||
commandType = "merge"
|
||||
case enums.REBASE_MODE_REBASING:
|
||||
commandType = "rebase"
|
||||
default:
|
||||
// shouldn't be possible to land here
|
||||
}
|
||||
commandType := status.CommandName()
|
||||
|
||||
// we should end up with a command like 'git merge --continue'
|
||||
|
||||
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
|
||||
needsSubprocess := (status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && self.c.UserConfig().Git.Merging.ManualCommit) ||
|
||||
needsSubprocess := (effectiveStatus == models.WORKING_TREE_STATE_MERGING && command != REBASE_OPTION_ABORT && self.c.UserConfig().Git.Merging.ManualCommit) ||
|
||||
// but we'll also use a subprocess if we have exec todos; those are likely to be lengthy build
|
||||
// tasks whose output the user will want to see in the terminal
|
||||
(status == enums.REBASE_MODE_REBASING && command != REBASE_OPTION_ABORT && self.hasExecTodos())
|
||||
(effectiveStatus == models.WORKING_TREE_STATE_REBASING && command != REBASE_OPTION_ABORT && self.hasExecTodos())
|
||||
|
||||
if needsSubprocess {
|
||||
// TODO: see if we should be calling more of the code from self.Git.Rebase.GenericMergeOrRebaseAction
|
||||
@@ -136,7 +119,7 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
|
||||
|
||||
func (self *MergeAndRebaseHelper) hasExecTodos() bool {
|
||||
for _, commit := range self.c.Model().Commits {
|
||||
if commit.Status != models.StatusRebasing {
|
||||
if !commit.IsTODO() {
|
||||
break
|
||||
}
|
||||
if commit.Action == todo.Exec {
|
||||
@@ -152,6 +135,8 @@ var conflictStrings = []string{
|
||||
"fix conflicts",
|
||||
"Resolve all conflicts manually",
|
||||
"Merge conflict in file",
|
||||
"hint: after resolving the conflicts",
|
||||
"CONFLICT (content):",
|
||||
}
|
||||
|
||||
func isMergeConflictErr(errStr string) bool {
|
||||
@@ -199,7 +184,7 @@ func (self *MergeAndRebaseHelper) CheckForConflicts(result error) error {
|
||||
}
|
||||
|
||||
func (self *MergeAndRebaseHelper) PromptForConflictHandling() error {
|
||||
mode := self.workingTreeStateNoun()
|
||||
mode := self.c.Git().Status.WorkingTreeState().CommandName()
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.FoundConflictsTitle,
|
||||
Items: []*types.MenuItem{
|
||||
@@ -224,7 +209,7 @@ func (self *MergeAndRebaseHelper) PromptForConflictHandling() error {
|
||||
|
||||
func (self *MergeAndRebaseHelper) AbortMergeOrRebaseWithConfirm() error {
|
||||
// prompt user to confirm that they want to abort, then do it
|
||||
mode := self.workingTreeStateNoun()
|
||||
mode := self.c.Git().Status.WorkingTreeState().CommandName()
|
||||
self.c.Confirm(types.ConfirmOpts{
|
||||
Title: fmt.Sprintf(self.c.Tr.AbortTitle, mode),
|
||||
Prompt: fmt.Sprintf(self.c.Tr.AbortPrompt, mode),
|
||||
@@ -236,23 +221,11 @@ func (self *MergeAndRebaseHelper) AbortMergeOrRebaseWithConfirm() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MergeAndRebaseHelper) workingTreeStateNoun() string {
|
||||
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
||||
switch workingTreeState {
|
||||
case enums.REBASE_MODE_NONE:
|
||||
return ""
|
||||
case enums.REBASE_MODE_MERGING:
|
||||
return "merge"
|
||||
default:
|
||||
return "rebase"
|
||||
}
|
||||
}
|
||||
|
||||
// PromptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress
|
||||
func (self *MergeAndRebaseHelper) PromptToContinueRebase() error {
|
||||
self.c.Confirm(types.ConfirmOpts{
|
||||
Title: self.c.Tr.Continue,
|
||||
Prompt: self.c.Tr.ConflictsResolved,
|
||||
Prompt: fmt.Sprintf(self.c.Tr.ConflictsResolved, self.c.Git().Status.WorkingTreeState().CommandName()),
|
||||
HandleConfirm: func() error {
|
||||
// By the time we get here, we might have unstaged changes again,
|
||||
// e.g. if the user had to fix build errors after resolving the
|
||||
@@ -294,14 +267,14 @@ func (self *MergeAndRebaseHelper) PromptToContinueRebase() error {
|
||||
}
|
||||
|
||||
func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
|
||||
checkedOutBranch := self.refsHelper.GetCheckedOutRef()
|
||||
checkedOutBranchName := self.refsHelper.GetCheckedOutRef().Name
|
||||
checkedOutBranch := self.c.Model().Branches[0]
|
||||
checkedOutBranchName := checkedOutBranch.Name
|
||||
var disabledReason, baseBranchDisabledReason *types.DisabledReason
|
||||
if checkedOutBranchName == ref {
|
||||
disabledReason = &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf}
|
||||
}
|
||||
|
||||
baseBranch, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(checkedOutBranch, self.refsHelper.c.Model().MainBranches)
|
||||
baseBranch, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(checkedOutBranch, self.c.Model().MainBranches)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -407,7 +380,7 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e
|
||||
if self.c.Git().Branch.IsHeadDetached() {
|
||||
return errors.New("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
|
||||
}
|
||||
checkedOutBranchName := self.refsHelper.GetCheckedOutRef().Name
|
||||
checkedOutBranchName := self.c.Model().Branches[0].Name
|
||||
if checkedOutBranchName == refName {
|
||||
return errors.New(self.c.Tr.CantMergeBranchIntoItself)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"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"
|
||||
@@ -116,12 +114,12 @@ func (self *ModeHelper) Statuses() []ModeStatus {
|
||||
},
|
||||
{
|
||||
IsActive: func() bool {
|
||||
return !self.suppressRebasingMode && self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE
|
||||
return !self.suppressRebasingMode && self.c.Git().Status.WorkingTreeState().Any()
|
||||
},
|
||||
Description: func() string {
|
||||
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
||||
return self.withResetButton(
|
||||
presentation.FormatWorkingTreeStateTitle(self.c.Tr, workingTreeState), style.FgYellow,
|
||||
workingTreeState.Title(self.c.Tr), style.FgYellow,
|
||||
)
|
||||
},
|
||||
Reset: self.mergeAndRebaseHelper.AbortMergeOrRebaseWithConfirm,
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
@@ -22,7 +21,7 @@ func NewPatchBuildingHelper(
|
||||
}
|
||||
|
||||
func (self *PatchBuildingHelper) ValidateNormalWorkingTreeState() (bool, error) {
|
||||
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
|
||||
if self.c.Git().Status.WorkingTreeState().Any() {
|
||||
return false, errors.New(self.c.Tr.CantPatchWhileRebasingError)
|
||||
}
|
||||
return true, nil
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
||||
@@ -333,6 +332,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error {
|
||||
RefForPushedStatus: checkedOutBranchName,
|
||||
All: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(),
|
||||
MainBranches: self.c.Model().MainBranches,
|
||||
HashPool: self.c.Model().HashPool,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -361,6 +361,7 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error {
|
||||
RefToShowDivergenceFrom: self.c.Contexts().SubCommits.GetRefToShowDivergenceFrom(),
|
||||
RefForPushedStatus: self.c.Contexts().SubCommits.GetRef().FullRefName(),
|
||||
MainBranches: self.c.Model().MainBranches,
|
||||
HashPool: self.c.Model().HashPool,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -407,7 +408,7 @@ func (self *RefreshHelper) refreshRebaseCommits() error {
|
||||
self.c.Mutexes().LocalCommitsMutex.Lock()
|
||||
defer self.c.Mutexes().LocalCommitsMutex.Unlock()
|
||||
|
||||
updatedCommits, err := self.c.Git().Loaders.CommitLoader.MergeRebasingCommits(self.c.Model().Commits)
|
||||
updatedCommits, err := self.c.Git().Loaders.CommitLoader.MergeRebasingCommits(self.c.Model().HashPool, self.c.Model().Commits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -456,7 +457,7 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
|
||||
// which allows us to order them correctly. So if we're filtering we'll just
|
||||
// manually load all the reflog commits here
|
||||
var err error
|
||||
reflogCommits, _, err = self.c.Git().Loaders.ReflogCommitLoader.GetReflogCommits(nil, "", "")
|
||||
reflogCommits, _, err = self.c.Git().Loaders.ReflogCommitLoader.GetReflogCommits(self.c.Model().HashPool, nil, "", "")
|
||||
if err != nil {
|
||||
self.c.Log.Error(err)
|
||||
}
|
||||
@@ -583,7 +584,7 @@ func (self *RefreshHelper) refreshStateFiles() error {
|
||||
}
|
||||
}
|
||||
|
||||
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 {
|
||||
if self.c.Git().Status.WorkingTreeState().Any() && conflictFileCount == 0 && prevConflictFileCount > 0 {
|
||||
self.c.OnUIThread(func() error { return self.mergeAndRebaseHelper.PromptToContinueRebase() })
|
||||
}
|
||||
|
||||
@@ -625,7 +626,7 @@ func (self *RefreshHelper) refreshReflogCommits() error {
|
||||
|
||||
refresh := func(stateCommits *[]*models.Commit, filterPath string, filterAuthor string) error {
|
||||
commits, onlyObtainedNewReflogCommits, err := self.c.Git().Loaders.ReflogCommitLoader.
|
||||
GetReflogCommits(lastReflogCommit, filterPath, filterAuthor)
|
||||
GetReflogCommits(self.c.Model().HashPool, lastReflogCommit, filterPath, filterAuthor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -17,13 +17,17 @@ import (
|
||||
|
||||
type RefsHelper struct {
|
||||
c *HelperCommon
|
||||
|
||||
rebaseHelper *MergeAndRebaseHelper
|
||||
}
|
||||
|
||||
func NewRefsHelper(
|
||||
c *HelperCommon,
|
||||
rebaseHelper *MergeAndRebaseHelper,
|
||||
) *RefsHelper {
|
||||
return &RefsHelper{
|
||||
c: c,
|
||||
c: c,
|
||||
rebaseHelper: rebaseHelper,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,10 +268,10 @@ func (self *RefsHelper) CreateGitResetMenu(ref string) error {
|
||||
|
||||
func (self *RefsHelper) CreateCheckoutMenu(commit *models.Commit) error {
|
||||
branches := lo.Filter(self.c.Model().Branches, func(branch *models.Branch, _ int) bool {
|
||||
return commit.Hash == branch.CommitHash && branch.Name != self.c.Model().CheckedOutBranch
|
||||
return commit.Hash() == branch.CommitHash && branch.Name != self.c.Model().CheckedOutBranch
|
||||
})
|
||||
|
||||
hash := commit.Hash
|
||||
hash := commit.Hash()
|
||||
|
||||
menuItems := []*types.MenuItem{
|
||||
{
|
||||
@@ -388,6 +392,174 @@ func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggest
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *RefsHelper) MoveCommitsToNewBranch() error {
|
||||
currentBranch := self.c.Model().Branches[0]
|
||||
baseBranchRef, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(currentBranch, self.c.Model().MainBranches)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
withNewBranchNamePrompt := func(baseBranchName string, f func(string, string) error) {
|
||||
prompt := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.NewBranchNameBranchOff,
|
||||
map[string]string{
|
||||
"branchName": baseBranchName,
|
||||
},
|
||||
)
|
||||
|
||||
self.c.Prompt(types.PromptOpts{
|
||||
Title: prompt,
|
||||
HandleConfirm: func(response string) error {
|
||||
self.c.LogAction(self.c.Tr.MoveCommitsToNewBranch)
|
||||
newBranchName := SanitizedBranchName(response)
|
||||
return self.c.WithWaitingStatus(self.c.Tr.MovingCommitsToNewBranchStatus, func(gocui.Task) error {
|
||||
return f(currentBranch.Name, newBranchName)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
isMainBranch := lo.Contains(self.c.UserConfig().Git.MainBranches, currentBranch.Name)
|
||||
if isMainBranch {
|
||||
prompt := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.MoveCommitsToNewBranchFromMainPrompt,
|
||||
map[string]string{
|
||||
"baseBranchName": currentBranch.Name,
|
||||
},
|
||||
)
|
||||
self.c.Confirm(types.ConfirmOpts{
|
||||
Title: self.c.Tr.MoveCommitsToNewBranch,
|
||||
Prompt: prompt,
|
||||
HandleConfirm: func() error {
|
||||
withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
shortBaseBranchName := ShortBranchName(baseBranchRef)
|
||||
prompt := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.MoveCommitsToNewBranchMenuPrompt,
|
||||
map[string]string{
|
||||
"baseBranchName": shortBaseBranchName,
|
||||
},
|
||||
)
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.MoveCommitsToNewBranch,
|
||||
Prompt: prompt,
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
Label: fmt.Sprintf(self.c.Tr.MoveCommitsToNewBranchFromBaseItem, shortBaseBranchName),
|
||||
OnPress: func() error {
|
||||
withNewBranchNamePrompt(shortBaseBranchName, func(currentBranch string, newBranchName string) error {
|
||||
return self.moveCommitsToNewBranchOffOfMainBranch(currentBranch, newBranchName, baseBranchRef)
|
||||
})
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: fmt.Sprintf(self.c.Tr.MoveCommitsToNewBranchStackedItem, currentBranch.Name),
|
||||
OnPress: func() error {
|
||||
withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RefsHelper) moveCommitsToNewBranchStackedOnCurrentBranch(currentBranch string, newBranchName string) error {
|
||||
if err := self.c.Git().Branch.NewWithoutCheckout(newBranchName, "HEAD"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mustStash := IsWorkingTreeDirty(self.c.Model().Files)
|
||||
if mustStash {
|
||||
if err := self.c.Git().Stash.Push(self.c.Tr.StashPrefix + currentBranch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.c.Git().Commit.ResetToCommit("@{u}", "hard", []string{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.c.Git().Branch.Checkout(newBranchName, git_commands.CheckoutOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mustStash {
|
||||
if err := self.c.Git().Stash.Pop(0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
self.c.Contexts().LocalCommits.SetSelection(0)
|
||||
self.c.Contexts().Branches.SetSelection(0)
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI, KeepBranchSelectionIndex: true})
|
||||
}
|
||||
|
||||
func (self *RefsHelper) moveCommitsToNewBranchOffOfMainBranch(currentBranch string, newBranchName string, baseBranchRef string) error {
|
||||
commitsToCherryPick := lo.Filter(self.c.Model().Commits, func(commit *models.Commit, _ int) bool {
|
||||
return commit.Status == models.StatusUnpushed
|
||||
})
|
||||
|
||||
mustStash := IsWorkingTreeDirty(self.c.Model().Files)
|
||||
if mustStash {
|
||||
if err := self.c.Git().Stash.Push(self.c.Tr.StashPrefix + currentBranch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.c.Git().Commit.ResetToCommit("@{u}", "hard", []string{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.c.Git().Branch.NewWithoutTracking(newBranchName, baseBranchRef); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := self.c.Git().Rebase.CherryPickCommits(commitsToCherryPick)
|
||||
err = self.rebaseHelper.CheckMergeOrRebaseWithRefreshOptions(err, types.RefreshOptions{Mode: types.SYNC})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mustStash {
|
||||
if err := self.c.Git().Stash.Pop(0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
self.c.Contexts().LocalCommits.SetSelection(0)
|
||||
self.c.Contexts().Branches.SetSelection(0)
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI, KeepBranchSelectionIndex: true})
|
||||
}
|
||||
|
||||
func (self *RefsHelper) CanMoveCommitsToNewBranch() *types.DisabledReason {
|
||||
if len(self.c.Model().Branches) == 0 {
|
||||
return &types.DisabledReason{Text: self.c.Tr.NoBranchesThisRepo}
|
||||
}
|
||||
currentBranch := self.GetCheckedOutRef()
|
||||
if currentBranch.DetachedHead {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CannotMoveCommitsFromDetachedHead, ShowErrorInPanel: true}
|
||||
}
|
||||
if !currentBranch.RemoteBranchStoredLocally() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CannotMoveCommitsNoUpstream, ShowErrorInPanel: true}
|
||||
}
|
||||
if currentBranch.IsBehindForPull() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CannotMoveCommitsBehindUpstream, ShowErrorInPanel: true}
|
||||
}
|
||||
if !currentBranch.IsAheadForPull() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CannotMoveCommitsNoUnpushedCommits, ShowErrorInPanel: true}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SanitizedBranchName will remove all spaces in favor of a dash "-" to meet
|
||||
// git's branch naming requirement.
|
||||
func SanitizedBranchName(input string) string {
|
||||
|
||||
@@ -45,6 +45,7 @@ func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error {
|
||||
RefForPushedStatus: opts.Ref.FullRefName(),
|
||||
RefToShowDivergenceFrom: opts.RefToShowDivergenceFrom,
|
||||
MainBranches: self.c.Model().MainBranches,
|
||||
HashPool: self.c.Model().HashPool,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user