Compare commits

..
Author SHA1 Message Date
Jesse Duffield b146c05578 WIP 2023-05-27 13:11:25 +10:00
Jesse Duffield 6c7f893f7a WIP 2023-05-27 12:08:27 +10:00
Jesse Duffield 8b15f22379 WIP 2023-05-26 19:05:22 +10:00
Jesse Duffield 9c7db3a37b Utilise file instead of stderr for integration tests
Windows for some reason is not properly making use of stderr so instead
we're going to write our result to a file explicitly
2023-05-26 19:05:22 +10:00
Jesse Duffield c961a5833b WIP 2023-05-26 19:05:22 +10:00
Jesse Duffield 6509875009 WIP 2023-05-26 19:05:22 +10:00
Jesse Duffield 3bf1be65b3 Run integration tests on windows 2023-05-26 19:04:29 +10:00
236 changed files with 12738 additions and 3488 deletions
+20 -7
View File
@@ -43,12 +43,23 @@ jobs:
restore-keys: |
${{runner.os}}-go-
- name: Test code
# we're passing -short so that we skip the integration tests, which will be run in parallel below
# we're passing -short so that we skip the integration tests, which will be run below
run: |
go test ./... -short
integration-tests:
runs-on: ubuntu-latest
name: "Integration Tests"
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
include:
- os: ubuntu-latest
cache_path: ~/.cache/go-build
- os: windows-latest
cache_path: ~\AppData\Local\go-build
runs-on: ${{matrix.os}}
name: "Integration Tests - ${{matrix.os}}"
env:
GOFLAGS: -mod=vendor
steps:
@@ -59,18 +70,20 @@ jobs:
with:
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
${{matrix.cache_path}}
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Test code
# LONG_WAIT_BEFORE_FAIL means that for a given test assertion, we'll wait longer before failing
env:
# LONG_WAIT_BEFORE_FAIL means that for a given test assertion, we'll wait longer before failing
LONG_WAIT_BEFORE_FAIL: true
run: |
LONG_WAIT_BEFORE_FAIL=true go test pkg/integration/clients/*.go
go test ./pkg/integration/clients
build:
runs-on: ubuntu-latest
env:
+81
View File
@@ -0,0 +1,81 @@
name: Build PR Image
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
build-application:
name: Build and Push `lazygit`
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
outputs:
tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_APP_TAG=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: registry.uffizzi.com/${{ env.UUID_APP_TAG }}
tags: type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
uses: docker/build-push-action@v3
with:
push: true
context: ./
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./uffizzi/DockerfileTtyd
cache-from: type=gha
cache-to: type=gha,mode=max
render-compose-file:
name: Render Docker Compose File
# Pass output of this workflow to another triggered by `workflow_run` event.
runs-on: ubuntu-latest
needs:
- build-application
outputs:
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Render Compose File
run: |
APP_IMAGE=$(echo ${{ needs.build-application.outputs.tags }})
export APP_IMAGE
# Render simple template from environment variables.
envsubst < ./uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: ${{ github.event_path }}
retention-days: 2
delete-preview:
name: Call for Preview Deletion
runs-on: ubuntu-latest
if: ${{ github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: ${{ github.event_path }}
retention-days: 2
+90
View File
@@ -0,0 +1,90 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "Build PR Image"
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
pr-number: ${{ env.PR_NUMBER }}
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
if (matchArtifact === undefined) {
throw TypeError('Build Artifact not found!');
}
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo -e '\nEOF' >> $GITHUB_ENV
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
with:
path: docker-compose.rendered.yml
key: ${{ env.COMPOSE_FILE_HASH }}
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Compose file hash: ${{ env.COMPOSE_FILE_HASH }}"
cat event.json
deploy-uffizzi-preview:
name: Use Remote Workflow to Preview on Uffizzi
needs:
- cache-compose-file
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:
contents: read
pull-requests: write
id-token: write
+3 -3
View File
@@ -24,7 +24,7 @@
{
"label": "Run current file integration test",
"type": "shell",
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli ${relativeFile}",
"command": "go run cmd/integration_test/main.go cli ${relativeFile}",
"problemMatcher": [],
"group": {
"kind": "test",
@@ -37,7 +37,7 @@
{
"label": "Run current file integration test (slow)",
"type": "shell",
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli --slow ${relativeFile}",
"command": "go run cmd/integration_test/main.go cli --slow ${relativeFile}",
"problemMatcher": [],
"group": {
"kind": "test",
@@ -49,7 +49,7 @@
{
"label": "Run current file integration test (sandbox)",
"type": "shell",
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli --sandbox ${relativeFile}",
"command": "go run cmd/integration_test/main.go cli --sandbox ${relativeFile}",
"problemMatcher": [],
"group": {
"kind": "test",
+1 -1
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -6,6 +6,8 @@ import (
"os"
"github.com/jesseduffield/lazygit/pkg/integration/clients"
"github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/samber/lo"
)
var usage = `
@@ -49,6 +51,11 @@ func main() {
}
}
// allowing full test paths to be passed for convenience
testNames = lo.Map(testNames, func(name string, _ int) string {
return components.TestNameFromFilePath(name)
})
clients.RunCLI(testNames, slow, sandbox)
case "tui":
clients.RunTUI()
-2
View File
@@ -58,7 +58,6 @@ gui:
showFileTree: true # for rendering changes files in a tree format
showListFooter: true # for seeing the '5 of 20' message in list panels
showRandomTip: true
showBranchCommitHash: false # show commit hashes alongside branch names
experimentalShowBranchHeads: false # visualize branch heads with (*) in commits list
showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
showCommandLog: true
@@ -95,7 +94,6 @@ git:
mainBranches: [master, main]
autoFetch: true
autoRefresh: true
fetchAll: true # Pass --all flag when running git fetch. Set to false to fetch only origin (or the current branch's upstream remote if there is one)
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'
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
+72 -216
View File
@@ -5,22 +5,20 @@ You can add custom command keybindings in your config.yml (accessible by pressin
```yml
customCommands:
- key: '<c-r>'
context: 'commits'
command: 'hub browse -- "commit/{{.SelectedLocalCommit.Sha}}"'
context: 'commits'
- key: 'a'
context: 'files'
command: "git {{if .SelectedFile.HasUnstagedChanges}} add {{else}} reset {{end}} {{.SelectedFile.Name | quote}}"
description: 'Toggle file staged'
context: 'files'
description: 'toggle file staged'
- key: 'C'
context: 'global'
command: "git commit"
context: 'global'
subprocess: true
- key: 'n'
context: 'localBranches'
prompts:
- type: 'menu'
title: 'What kind of branch is it?'
key: 'BranchType'
options:
- name: 'feature'
description: 'a feature branch'
@@ -33,252 +31,110 @@ customCommands:
value: 'release'
- type: 'input'
title: 'What is the new branch name?'
key: 'BranchName'
initialValue: ''
command: "git flow {{.Form.BranchType}} start {{.Form.BranchName}}"
loadingText: 'Creating branch'
command: "git flow {{index .PromptResponses 0}} start {{index .PromptResponses 1}}"
context: 'localBranches'
loadingText: 'creating branch'
- key : 'r'
description: 'Checkout a remote branch as FETCH_HEAD'
command: "git fetch {{index .PromptResponses 0}} {{index .PromptResponses 1}} && git checkout FETCH_HEAD"
context: 'remotes'
prompts:
- type: 'input'
title: 'Remote:'
initialValue: "{{index .SelectedRemote.Name }}"
- type: 'menuFromCommand'
title: 'Remote branch:'
command: 'git branch -r --list {{index .PromptResponses 0}}/*'
filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)'
valueFormat: '{{ .branch }}'
labelFormat: '{{ .branch | green }}'
- key: '<f1>'
command: 'git reset --soft {{.CheckedOutBranch.UpstreamRemote}}'
context: 'files'
prompts:
- type: 'confirm'
title: "Confirm:"
body: "Are you sure you want to reset HEAD to {{.CheckedOutBranch.UpstreamRemote}}?"
```
Looking at the command assigned to the 'n' key, here's what the result looks like:
![](../../assets/custom-command-keybindings.gif)
Custom command keybindings will appear alongside inbuilt keybindings when you view the keybindings menu by pressing '?':
Custom command keybindings will appear alongside inbuilt keybindings when you view the options menu by pressing 'x':
![](https://i.imgur.com/QB21FPx.png)
For a given custom command, here are the allowed fields:
| _field_ | _description_ | required |
|-----------------|----------------------|-|
| 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) | yes |
| 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 |
| 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) | yes |
| command | the command to run | yes |
| context | the context in which to listen for the key (see below) | yes |
| subprocess | whether you want the command to run in a subprocess (necessary if you want to view the output of the command or provide 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 | text to display in the keybindings menu that appears when you press 'x' | 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 gui prompt | no |
## Contexts
### Contexts
The permitted contexts are:
| _context_ | _description_ |
| -------------- | -------------------------------------------------------------------------------------------------------- |
| status | The 'Status' tab |
| files | The 'Files' tab |
| localBranches | The 'Local Branches' tab |
| remotes | The 'Remotes' tab |
| remoteBranches | The context you get when pressing enter on a remote in the remotes tab |
| tags | The 'Tags' tab |
| commits | The 'Commits' tab |
| reflogCommits | The 'Reflog' tab |
| subCommits | The context you see when pressing enter on a branch |
| commitFiles | The context you see when pressing enter on a commit or stash entry (warning, might be renamed in future) |
| stash | The 'Stash' tab |
| global | This keybinding will take affect everywhere |
| status | the 'Status' tab |
| files | the 'Files' tab |
| localBranches | the 'Local Branches' tab |
| remotes | the 'Remotes' tab |
| remoteBranches | the context you get when pressing enter on a remote in the remotes tab |
| tags | the 'Tags' tab |
| commits | the 'Commits' tab |
| reflogCommits | the 'Reflog' tab |
| subCommits | the context you see when pressing enter on a branch |
| commitFiles | the context you see when pressing enter on a commit or stash entry (warning, might be renamed in future) |
| stash | the 'Stash' tab |
| global | this keybinding will take affect everywhere |
## Prompts
### Prompts
### Common fields
These fields are applicable to all prompts.
The permitted prompt fields are:
| _field_ | _description_ | _required_ |
| ------------ | -----------------------------------------------------------------------------------------------| ---------- |
| type | One of 'input', 'confirm', 'menu', 'menuFromCommand' | yes |
| title | The title to display in the popup panel | no |
| key | Used to reference the entered value from within the custom command. E.g. a prompt with `key: 'Branch'` can be referred to as `{{.Form.Branch}}` in the command | yes |
### Input
| _field_ | _description_ | _required_ |
| ------------ | -----------------------------------------------------------------------------------------------| ---------- |
| initialValue | The initial value to appear in the text box | no |
| suggestions | Shows suggestions as the input is entered. See below for details | no |
The permitted suggestions fields are:
| _field_ | _description_ | _required_ |
|-----------------|----------------------|-|
| preset | Uses built-in logic to obtain the suggestions. One of 'files', 'branches', 'remotes', 'remoteBranches', 'refs' | no |
| command | Command to run such that each line in the output becomes a suggestion. Mutually exclusive with 'preset' field. | no |
Here's an example of passing a preset:
```yml
customCommands:
- key: 'a'
command: 'echo {{.Form.Branch | quote}}'
context: 'commits'
prompts:
- type: 'input'
title: 'Which branch?'
key: 'Branch'
suggestions:
preset: 'branches' # use built-in logic for obtaining branches
```
Here's an example of passing a command directly:
```yml
customCommands:
- key: 'a'
command: 'echo {{.Form.Branch | quote}}'
context: 'commits'
prompts:
- type: 'input'
title: 'Which branch?'
key: 'Branch'
suggestions:
command: "git branch --format='%(refname:short)'"
```
Here's an example of passing an initial value for the input:
```yml
customCommands:
- key: 'a'
command: 'echo {{.Form.Remote | quote}}'
context: 'commits'
prompts:
- type: 'input'
title: 'Remote:'
key: 'Remote'
initialValue: "{{.SelectedRemote.Name}}"
```
### Confirm
| _field_ | _description_ | _required_ |
| ------------ | -----------------------------------------------------------------------------------------------| ---------- |
| body | The immutable body text to appear in the text box | no |
Example:
```yml
customCommands:
- key: 'a'
command: 'echo "pushing to remote"'
context: 'commits'
prompts:
- type: 'confirm'
title: 'Push to remote'
body: 'Are you sure you want to push to the remote?'
```
### Menu
| _field_ | _description_ | _required_ |
| ------------ | -----------------------------------------------------------------------------------------------| ---------- |
| options | The options to display in the menu | yes |
| type | one of 'input', 'menu', or 'confirm' | yes |
| title | the title to display in the popup panel | no |
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
| body | (only applicable to 'confirm' prompts) the immutable body text to appear in the text box | no |
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
| command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes |
| | menu options | |
| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying groups which are going to be kept from the command's output | yes |
| valueFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from the filter to construct a menu item's value (What gets appended to prompt responses when the item is selected). You can use named groups, or `{{ .group_GROUPID }}`. PS: named groups keep first match only | yes |
| labelFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from the filter to construct the item's label (What's shown on screen). You can use named groups, or `{{ .group_GROUPID }}`. You can also color each match with `{{ .group_GROUPID \| colorname }}` (Color names from [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md)). If `labelFormat` is not specified, `valueFormat` is shown instead. PS: named groups keep first match only | no |
The permitted option fields are:
| _field_ | _description_ | _required_ |
|-----------------|----------------------|-|
| name | The first part of the label | no |
| description | The second part of the label | no |
| value | the value that will be used in the command | yes |
| name | the string which will appear first on the line | no |
| description | the string which will appear second on the line | no |
| value | the value that will be stored in `.PromptResponses` if the option is selected | yes |
If an option has no name the value will be displayed to the user in place of the name, so you're allowed to only include the value like so:
```yml
customCommands:
- key: 'a'
command: 'echo {{.Form.BranchType | quote}}'
context: 'commits'
prompts:
- type: 'menu'
title: 'What kind of branch is it?'
key: 'BranchType'
options:
- value: 'feature'
- value: 'hotfix'
- value: 'release'
```
Here's an example of supplying more detail for each option:
```yml
customCommands:
- key: 'a'
command: 'echo {{.Form.BranchType | quote}}'
context: 'commits'
prompts:
- type: 'menu'
title: 'What kind of branch is it?'
key: 'BranchType'
options:
- value: 'feature'
name: 'feature branch'
description: 'branch based off develop'
- value: 'hotfix'
name: 'hotfix branch'
description: 'branch based off main for fast bug fixes'
- value: 'release'
name: 'release branch'
description: 'branch for a release'
```
### Menu-from-command
| _field_ | _description_ | _required_ |
| ------------ | -----------------------------------------------------------------------------------------------| ---------- |
| command | The command to run to generate menu options | yes |
| filter | The regexp to run specifying groups which are going to be kept from the command's output | no |
| valueFormat | How to format matched groups from the filter to construct a menu item's value | no |
| labelFormat | Like valueFormat but for the labels. If `labelFormat` is not specified, `valueFormat` is shown instead. | no |
Here's an example using named groups in the regex. Notice how we can pipe the label to a colour function for coloured output (available colours [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md))
```yml
- key : 'a'
description: 'Checkout a remote branch as FETCH_HEAD'
command: "git fetch {{.Form.Remote}} {{.Form.Branch}} && git checkout FETCH_HEAD"
context: 'remotes'
prompts:
- type: 'menuFromCommand'
title: 'Remote branch:'
key: 'Branch'
command: 'git branch -r --list {{.SelectedRemote.Name }}/*'
filter: '.*{{.SelectedRemote.Name }}/(?P<branch>.*)'
valueFormat: '{{ .branch }}'
labelFormat: '{{ .branch | green }}'
```
Here's an example using unnamed groups:
```yml
- key : 'a'
description: 'Checkout a remote branch as FETCH_HEAD'
command: "git fetch {{.Form.Remote}} {{.Form.Branch}} && git checkout FETCH_HEAD"
context: 'remotes'
prompts:
- type: 'menuFromCommand'
title: 'Remote branch:'
key: 'Branch'
command: 'git branch -r --list {{.SelectedRemote.Name }}/*'
filter: '.*{{.SelectedRemote.Name }}/(.*)'
valueFormat: '{{ .group_1 }}'
labelFormat: '{{ .group_1 | green }}'
```
Here's an example using a command but not specifying anything else: so each line from the command becomes the value and label of the menu items
```yml
- key : 'a'
description: 'Checkout a remote branch as FETCH_HEAD'
command: "open {{.Form.File | quote}}"
context: 'global'
prompts:
- type: 'menuFromCommand'
title: 'File:'
key: 'File'
command: 'ls'
```
## Placeholder values
### Placeholder values
Your commands can contain placeholder strings using Go's [template syntax](https://jan.newmarch.name/golang/template/chapter-template.html). The template syntax is pretty powerful, letting you do things like conditionals if you want, but for the most part you'll simply want to be accessing the fields on the following objects:
@@ -297,16 +153,16 @@ SelectedCommitFile
CheckedOutBranch
```
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit Lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedLocalBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedLocalBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
## Keybinding collisions
### Keybinding collisions
If your custom keybinding collides with an inbuilt keybinding that is defined for the same context, only the custom keybinding will be executed. This also applies to the global context. However, one caveat is that if you have a custom keybinding defined on the global context for some key, and there is an in-built keybinding defined for the same key and for a specific context (say the 'files' context), then the in-built keybinding will take precedence. See how to change in-built keybindings [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#keybindings)
## Debugging
### 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 `showOutput: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. Alternatively you can run lazygit in debug mode with `lazygit --debug` and in another terminal window run `lazygit --logs` to see which commands are actually run
## More Examples
### More Examples
See the [wiki](https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium) page for more examples, and feel free to add your own custom commands to this page so others can benefit!
-320
View File
@@ -1,320 +0,0 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
# Lazygit 鍵盤快捷鍵
_說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
## 全局快捷鍵
<pre>
<kbd>&lt;c-r&gt;</kbd>: 切換到最近使用的版本庫
<kbd>&lt;pgup&gt;</kbd>: 向上捲動主面板 (fn+up/shift+k)
<kbd>&lt;pgdown&gt;</kbd>: 向下捲動主面板 (fn+down/shift+j)
<kbd>@</kbd>: 開啟命令記錄選單
<kbd>}</kbd>: 增加差異檢視中顯示變更周圍上下文的大小
<kbd>{</kbd>: 減小差異檢視中顯示變更周圍上下文的大小
<kbd>:</kbd>: 執行自訂命令
<kbd>&lt;c-p&gt;</kbd>: 檢視自訂補丁選項
<kbd>m</kbd>: 查看合併/變基選項
<kbd>R</kbd>: 重新整理
<kbd>+</kbd>: 下一個螢幕模式(常規/半螢幕/全螢幕)
<kbd>_</kbd>: 上一個螢幕模式
<kbd>?</kbd>: 開啟選單
<kbd>&lt;c-s&gt;</kbd>: 檢視篩選路徑選項
<kbd>W</kbd>: 開啟差異比較選單
<kbd>&lt;c-e&gt;</kbd>: 開啟差異比較選單
<kbd>&lt;c-w&gt;</kbd>: 切換是否在差異檢視中顯示空格變更
<kbd>z</kbd>: 復原
<kbd>&lt;c-z&gt;</kbd>: 取消復原
<kbd>P</kbd>: 推送
<kbd>p</kbd>: 拉取
</pre>
## 列表面板導航
<pre>
<kbd>,</kbd>: 上一頁
<kbd>.</kbd>: 下一頁
<kbd>&lt;</kbd>: 捲動到頂部
<kbd>/</kbd>: 開始搜尋
<kbd>&gt;</kbd>: 捲動到底部
<kbd>H</kbd>: 向左捲動
<kbd>L</kbd>: 向右捲動
<kbd>]</kbd>: 下一個索引標籤
<kbd>[</kbd>: 上一個索引標籤
</pre>
## Reflog
<pre>
<kbd>&lt;c-o&gt;</kbd>: 複製提交 SHA 到剪貼簿
<kbd>&lt;space&gt;</kbd>: 檢出提交
<kbd>y</kbd>: 複製提交屬性
<kbd>o</kbd>: 在瀏覽器中開啟提交
<kbd>n</kbd>: 從提交建立新分支
<kbd>g</kbd>: 檢視重設選項
<kbd>c</kbd>: 複製提交 (揀選)
<kbd>C</kbd>: 複製提交範圍 (揀選)
<kbd>&lt;c-r&gt;</kbd>: 重設選定的揀選 (複製) 提交
<kbd>&lt;enter&gt;</kbd>: 檢視提交
</pre>
## 主視窗 (一般)
<pre>
<kbd>mouse wheel down</kbd>: 向下捲動 (fn+up)
<kbd>mouse wheel up</kbd>: 向上捲動 (fn+down)
</pre>
## 主視窗 (合併中)
<pre>
<kbd>e</kbd>: 編輯檔案
<kbd>o</kbd>: 開啟檔案
<kbd>&lt;left&gt;</kbd>: 選擇上一個衝突
<kbd>&lt;right&gt;</kbd>: 選擇下一個衝突
<kbd>&lt;up&gt;</kbd>: 選擇上一段
<kbd>&lt;down&gt;</kbd>: 選擇下一段
<kbd>z</kbd>: 復原
<kbd>M</kbd>: 開啟外部合併工具 (git mergetool)
<kbd>&lt;space&gt;</kbd>: 挑選程式碼片段
<kbd>b</kbd>: 挑選所有程式碼片段
<kbd>&lt;esc&gt;</kbd>: 返回檔案面板
</pre>
## 主視窗 (預存中)
<pre>
<kbd>&lt;left&gt;</kbd>: 選擇上一段
<kbd>&lt;right&gt;</kbd>: 選擇下一段
<kbd>v</kbd>: 切換拖曳選擇
<kbd>V</kbd>: 切換拖曳選擇
<kbd>a</kbd>: 切換選擇程式碼塊
<kbd>&lt;c-o&gt;</kbd>: 複製所選文本至剪貼簿
<kbd>o</kbd>: 開啟檔案
<kbd>e</kbd>: 編輯檔案
<kbd>&lt;esc&gt;</kbd>: 返回檔案面板
<kbd>&lt;tab&gt;</kbd>: 切換至另一個面板 (已預存/未預存更改)
<kbd>&lt;space&gt;</kbd>: 切換現有行的狀態 (已預存/未預存)
<kbd>d</kbd>: 刪除變更 (git reset)
<kbd>E</kbd>: 編輯程式碼塊
<kbd>c</kbd>: 提交變更
<kbd>w</kbd>: 沒有預提交 hook 就提交更改
<kbd>C</kbd>: 使用 git 編輯器提交變更
</pre>
## 主面板 (補丁生成)
<pre>
<kbd>&lt;left&gt;</kbd>: 選擇上一段
<kbd>&lt;right&gt;</kbd>: 選擇下一段
<kbd>v</kbd>: 切換拖曳選擇
<kbd>V</kbd>: 切換拖曳選擇
<kbd>a</kbd>: 切換選擇程式碼塊
<kbd>&lt;c-o&gt;</kbd>: 複製所選文本至剪貼簿
<kbd>o</kbd>: 開啟檔案
<kbd>e</kbd>: 編輯檔案
<kbd>&lt;space&gt;</kbd>: 向 (或從) 補丁中添加/刪除行
<kbd>&lt;esc&gt;</kbd>: 退出自訂補丁建立器
</pre>
## 功能表
<pre>
<kbd>&lt;enter&gt;</kbd>: 執行
<kbd>&lt;esc&gt;</kbd>: 關閉
</pre>
## 子提交
<pre>
<kbd>&lt;c-o&gt;</kbd>: 複製提交 SHA 到剪貼簿
<kbd>&lt;space&gt;</kbd>: 檢出提交
<kbd>y</kbd>: 複製提交屬性
<kbd>o</kbd>: 在瀏覽器中開啟提交
<kbd>n</kbd>: 從提交建立新分支
<kbd>g</kbd>: 檢視重設選項
<kbd>c</kbd>: 複製提交 (揀選)
<kbd>C</kbd>: 複製提交範圍 (揀選)
<kbd>&lt;c-r&gt;</kbd>: 重設選定的揀選 (複製) 提交
<kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
</pre>
## 子模組
<pre>
<kbd>&lt;c-o&gt;</kbd>: 複製子模組名稱到剪貼簿
<kbd>&lt;enter&gt;</kbd>: 進入子模組
<kbd>d</kbd>: 移除子模組
<kbd>u</kbd>: 更新子模組
<kbd>n</kbd>: 新增子模組
<kbd>e</kbd>: 更新子模組 URL
<kbd>i</kbd>: 初始化子模組
<kbd>b</kbd>: 查看批量子模組選項
</pre>
## 提交
<pre>
<kbd>&lt;c-o&gt;</kbd>: 複製提交 SHA 到剪貼簿
<kbd>&lt;c-r&gt;</kbd>: 重設選定的揀選 (複製) 提交
<kbd>b</kbd>: 查看二分選項
<kbd>s</kbd>: 向下壓縮
<kbd>f</kbd>: 修復提交 (Fixup)
<kbd>r</kbd>: 改寫提交
<kbd>R</kbd>: 使用編輯器改寫提交
<kbd>d</kbd>: 刪除提交
<kbd>e</kbd>: 編輯提交
<kbd>p</kbd>: 挑選提交 (於變基過程中)
<kbd>F</kbd>: 為此提交建立修復提交
<kbd>S</kbd>: 壓縮上方所有的“fixup!”提交 (自動壓縮)
<kbd>&lt;c-j&gt;</kbd>: 向下移動提交
<kbd>&lt;c-k&gt;</kbd>: 向上移動提交
<kbd>v</kbd>: 貼上提交 (揀選)
<kbd>A</kbd>: 使用已預存的更改修正提交
<kbd>a</kbd>: 設置/重設提交作者
<kbd>t</kbd>: 還原提交
<kbd>T</kbd>: 打標籤到提交
<kbd>&lt;c-l&gt;</kbd>: 開啟記錄選單
<kbd>&lt;space&gt;</kbd>: 檢出提交
<kbd>y</kbd>: 複製提交屬性
<kbd>o</kbd>: 在瀏覽器中開啟提交
<kbd>n</kbd>: 從提交建立新分支
<kbd>g</kbd>: 檢視重設選項
<kbd>c</kbd>: 複製提交 (揀選)
<kbd>C</kbd>: 複製提交範圍 (揀選)
<kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
</pre>
## 提交摘要
<pre>
<kbd>&lt;enter&gt;</kbd>: 確認
<kbd>&lt;esc&gt;</kbd>: 關閉
</pre>
## 提交檔案
<pre>
<kbd>&lt;c-o&gt;</kbd>: 複製提交的檔案名稱到剪貼簿
<kbd>c</kbd>: 檢出檔案
<kbd>d</kbd>: 捨棄此提交對此檔案的更改
<kbd>o</kbd>: 開啟檔案
<kbd>e</kbd>: 編輯檔案
<kbd>&lt;space&gt;</kbd>: 切換檔案是否包含在補丁中
<kbd>a</kbd>: 切換所有檔案是否包含在補丁中
<kbd>&lt;enter&gt;</kbd>: 輸入檔案以將選定的行添加至補丁(或切換目錄折疊)
<kbd>`</kbd>: 切換檔案樹狀視圖
</pre>
## 收藏 (Stash)
<pre>
<kbd>&lt;space&gt;</kbd>: 套用
<kbd>g</kbd>: 還原
<kbd>d</kbd>: 捨棄
<kbd>n</kbd>: 新分支
<kbd>r</kbd>: 重新命名收藏
<kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
</pre>
## 本地分支
<pre>
<kbd>&lt;c-o&gt;</kbd>: 複製分支名稱到剪貼簿
<kbd>i</kbd>: 顯示 git-flow 選項
<kbd>&lt;space&gt;</kbd>: 檢出
<kbd>n</kbd>: 新分支
<kbd>o</kbd>: 建立拉取請求
<kbd>O</kbd>: 建立拉取請求選項
<kbd>&lt;c-y&gt;</kbd>: 複製拉取請求的 URL 到剪貼板
<kbd>c</kbd>: 根據名稱檢出
<kbd>F</kbd>: 強制檢出
<kbd>d</kbd>: 刪除分支
<kbd>r</kbd>: 將已檢出的分支變基至此分支
<kbd>M</kbd>: 合併到當前檢出的分支
<kbd>f</kbd>: 從上游快進此分支
<kbd>T</kbd>: 建立標籤
<kbd>g</kbd>: 檢視重設選項
<kbd>R</kbd>: 重新命名分支
<kbd>u</kbd>: 設定/取消設定上游
<kbd>&lt;enter&gt;</kbd>: 檢視提交
</pre>
## 標籤
<pre>
<kbd>&lt;space&gt;</kbd>: 檢出
<kbd>d</kbd>: 刪除標籤
<kbd>P</kbd>: 推送標籤
<kbd>n</kbd>: 建立標籤
<kbd>g</kbd>: 檢視重設選項
<kbd>&lt;enter&gt;</kbd>: 檢視提交
</pre>
## 檔案
<pre>
<kbd>&lt;c-o&gt;</kbd>: 複製檔案名稱到剪貼簿
<kbd>d</kbd>: 檢視“捨棄更改”的選項
<kbd>&lt;space&gt;</kbd>: 切換預存
<kbd>&lt;c-b&gt;</kbd>: 篩選檔案 (預存/未預存)
<kbd>c</kbd>: 提交變更
<kbd>w</kbd>: 沒有預提交 hook 就提交更改
<kbd>A</kbd>: 修正上次提交
<kbd>C</kbd>: 使用 git 編輯器提交變更
<kbd>e</kbd>: 編輯檔案
<kbd>o</kbd>: 開啟檔案
<kbd>i</kbd>: 忽略或排除檔案
<kbd>r</kbd>: 重新整理檔案
<kbd>s</kbd>: 收藏所有變更
<kbd>S</kbd>: 檢視收藏選項
<kbd>a</kbd>: 全部預存/取消預存
<kbd>&lt;enter&gt;</kbd>: 選擇檔案中的單個程式碼塊/行,或展開/折疊目錄
<kbd>g</kbd>: 檢視上游重設選項
<kbd>D</kbd>: 檢視重設選項
<kbd>`</kbd>: 切換檔案樹狀視圖
<kbd>M</kbd>: 開啟外部合併工具 (git mergetool)
<kbd>f</kbd>: 擷取
</pre>
## 狀態
<pre>
<kbd>o</kbd>: 開啟設定檔案
<kbd>e</kbd>: 編輯設定檔案
<kbd>u</kbd>: 檢查更新
<kbd>&lt;enter&gt;</kbd>: 切換到最近使用的版本庫
<kbd>a</kbd>: 顯示所有分支日誌
</pre>
## 確認面板
<pre>
<kbd>&lt;enter&gt;</kbd>: 確認
<kbd>&lt;esc&gt;</kbd>: 關閉/取消
</pre>
## 遠端
<pre>
<kbd>f</kbd>: 擷取遠端
<kbd>n</kbd>: 新增遠端
<kbd>d</kbd>: 移除遠端
<kbd>e</kbd>: 編輯遠端
</pre>
## 遠端分支
<pre>
<kbd>&lt;c-o&gt;</kbd>: 複製分支名稱到剪貼簿
<kbd>&lt;space&gt;</kbd>: 檢出
<kbd>n</kbd>: 新分支
<kbd>M</kbd>: 合併到當前檢出的分支
<kbd>r</kbd>: 將已檢出的分支變基至此分支
<kbd>d</kbd>: 刪除分支
<kbd>u</kbd>: 將此分支設為當前分支之上游
<kbd>&lt;esc&gt;</kbd>: 返回遠端列表
<kbd>g</kbd>: 檢視重設選項
<kbd>&lt;enter&gt;</kbd>: 檢視提交
</pre>
+8 -5
View File
@@ -18,10 +18,11 @@ require (
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20230601121845-cb89273fdd4e
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/kyokomi/emoji/v2 v2.2.8
github.com/lucasb-eyer/go-colorful v1.2.0
@@ -66,9 +67,11 @@ require (
github.com/xanzy/ssh-agent v0.2.1 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
replace github.com/creack/pty => github.com/photostorm/pty v1.1.19-0.20230324012736-6794a5ba0ba0
+14 -13
View File
@@ -15,9 +15,6 @@ github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -72,14 +69,16 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20230601121845-cb89273fdd4e h1:NpsrRAbYUmMkxDgNAVSlu3LxtfwdDe140vWVo/VldgA=
github.com/jesseduffield/gocui v0.3.1-0.20230601121845-cb89273fdd4e/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa h1:E9G1mj94rMal1YLaABwdxLUUgKq+xGbElFjHRNaDJUg=
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
@@ -127,6 +126,8 @@ github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
github.com/photostorm/pty v1.1.19-0.20230324012736-6794a5ba0ba0 h1:ahg2J7KgBYMH1KJNvde12jjJZGXwGq0AdRPztBgX2Rc=
github.com/photostorm/pty v1.1.19-0.20230324012736-6794a5ba0ba0/go.mod h1:KO+FcPtyLAiRC0hJwreJVvfwc7vnNz77UxBTIGHdPVk=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -183,9 +184,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -204,23 +204,24 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+1 -4
View File
@@ -45,7 +45,6 @@ type Loaders struct {
BranchLoader *git_commands.BranchLoader
CommitFileLoader *git_commands.CommitFileLoader
CommitLoader *git_commands.CommitLoader
CommitStoreLoader *git_commands.CommitStoreLoader
FileLoader *git_commands.FileLoader
ReflogCommitLoader *git_commands.ReflogCommitLoader
RemoteLoader *git_commands.RemoteLoader
@@ -129,10 +128,9 @@ func NewGitCommandAux(
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
bisectCommands := git_commands.NewBisectCommands(gitCommon)
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
branchLoader := git_commands.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchInfo, configCommands)
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode)
commitStoreLoader := git_commands.NewCommitStoreLoader(cmn, cmd)
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
stashLoader := git_commands.NewStashLoader(cmn, cmd)
@@ -160,7 +158,6 @@ func NewGitCommandAux(
BranchLoader: branchLoader,
CommitFileLoader: commitFileLoader,
CommitLoader: commitLoader,
CommitStoreLoader: commitStoreLoader,
FileLoader: fileLoader,
ReflogCommitLoader: reflogCommitLoader,
RemoteLoader: remoteLoader,
+10
View File
@@ -188,6 +188,16 @@ func (self *BranchCommands) Rename(oldName string, newName string) error {
return self.cmd.New(cmdArgs).Run()
}
func (self *BranchCommands) GetRawBranches() (string, error) {
cmdArgs := NewGitCmd("for-each-ref").
Arg("--sort=-committerdate").
Arg(`--format=%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)`).
Arg("refs/heads").
ToArgv()
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
}
type MergeOpts struct {
FastForwardOnly bool
}
+33 -70
View File
@@ -1,7 +1,6 @@
package git_commands
import (
"fmt"
"regexp"
"strings"
@@ -9,10 +8,8 @@ import (
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/go-git/v5/config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
// context:
@@ -39,20 +36,20 @@ type BranchInfo struct {
// BranchLoader returns a list of Branch objects for the current repo
type BranchLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getRawBranches func() (string, error)
getCurrentBranchInfo func() (BranchInfo, error)
config BranchLoaderConfigCommands
}
func NewBranchLoader(
cmn *common.Common,
cmd oscommands.ICmdObjBuilder,
getRawBranches func() (string, error),
getCurrentBranchInfo func() (BranchInfo, error),
config BranchLoaderConfigCommands,
) *BranchLoader {
return &BranchLoader{
Common: cmn,
cmd: cmd,
getRawBranches: getRawBranches,
getCurrentBranchInfo: getCurrentBranchInfo,
config: config,
}
@@ -131,8 +128,8 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
}
split := strings.Split(line, "\x00")
if len(split) != len(branchFields) {
// Ignore line if it isn't separated into the expected number of parts
if len(split) != 4 {
// Ignore line if it isn't separated into 4 parts
// This is probably a warning message, for more info see:
// https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439
return nil, false
@@ -142,81 +139,47 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
})
}
func (self *BranchLoader) getRawBranches() (string, error) {
format := strings.Join(
lo.Map(branchFields, func(thing string, _ int) string {
return "%(" + thing + ")"
}),
"%00",
)
cmdArgs := NewGitCmd("for-each-ref").
Arg("--sort=-committerdate").
Arg(fmt.Sprintf("--format=%s", format)).
Arg("refs/heads").
ToArgv()
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
}
var branchFields = []string{
"HEAD",
"refname:short",
"upstream:short",
"upstream:track",
"subject",
"objectname",
}
// Obtain branch information from parsed line output of getRawBranches()
// split contains the '|' separated tokens in the line of output
func obtainBranch(split []string) *models.Branch {
headMarker := split[0]
fullName := split[1]
upstreamName := split[2]
track := split[3]
subject := split[4]
commitHash := split[5]
name := strings.TrimPrefix(fullName, "heads/")
pushables, pullables, gone := parseUpstreamInfo(upstreamName, track)
return &models.Branch{
Name: name,
Pushables: pushables,
Pullables: pullables,
UpstreamGone: gone,
Head: headMarker == "*",
Subject: subject,
CommitHash: commitHash,
name := strings.TrimPrefix(split[1], "heads/")
branch := &models.Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
}
func parseUpstreamInfo(upstreamName string, track string) (string, string, bool) {
upstreamName := split[2]
if upstreamName == "" {
// if we're here then it means we do not have a local version of the remote.
// The branch might still be tracking a remote though, we just don't know
// how many commits ahead/behind it is
return "?", "?", false
return branch
}
track := split[3]
if track == "[gone]" {
return "?", "?", true
}
pushables := parseDifference(track, `ahead (\d+)`)
pullables := parseDifference(track, `behind (\d+)`)
return pushables, pullables, false
}
func parseDifference(track string, regexStr string) string {
re := regexp.MustCompile(regexStr)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
return match[1]
branch.UpstreamGone = true
} else {
return "0"
re := regexp.MustCompile(`ahead (\d+)`)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pushables = match[1]
} else {
branch.Pushables = "0"
}
re = regexp.MustCompile(`behind (\d+)`)
match = re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pullables = match[1]
} else {
branch.Pullables = "0"
}
}
return branch
}
// TODO: only look at the new reflog commits, and otherwise store the recencies in
+16 -52
View File
@@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestObtainBranch(t *testing.T) {
func TestObtainBanch(t *testing.T) {
type scenario struct {
testName string
input []string
@@ -17,65 +17,29 @@ func TestObtainBranch(t *testing.T) {
scenarios := []scenario{
{
testName: "TrimHeads",
input: []string{"", "heads/a_branch", "", "", "subject", "123"},
expectedBranch: &models.Branch{
Name: "a_branch",
Pushables: "?",
Pullables: "?",
Head: false,
Subject: "subject",
CommitHash: "123",
},
testName: "TrimHeads",
input: []string{"", "heads/a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
},
{
testName: "NoUpstream",
input: []string{"", "a_branch", "", "", "subject", "123"},
expectedBranch: &models.Branch{
Name: "a_branch",
Pushables: "?",
Pullables: "?",
Head: false,
Subject: "subject",
CommitHash: "123",
},
testName: "NoUpstream",
input: []string{"", "a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
},
{
testName: "IsHead",
input: []string{"*", "a_branch", "", "", "subject", "123"},
expectedBranch: &models.Branch{
Name: "a_branch",
Pushables: "?",
Pullables: "?",
Head: true,
Subject: "subject",
CommitHash: "123",
},
testName: "IsHead",
input: []string{"*", "a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: true},
},
{
testName: "IsBehindAndAhead",
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "subject", "123"},
expectedBranch: &models.Branch{
Name: "a_branch",
Pushables: "3",
Pullables: "2",
Head: false,
Subject: "subject",
CommitHash: "123",
},
testName: "IsBehindAndAhead",
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]"},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "3", Pullables: "2", Head: false},
},
{
testName: "RemoteBranchIsGone",
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "subject", "123"},
expectedBranch: &models.Branch{
Name: "a_branch",
UpstreamGone: true,
Pushables: "?",
Pullables: "?",
Head: false,
Subject: "subject",
CommitHash: "123",
},
testName: "RemoteBranchIsGone",
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]"},
expectedBranch: &models.Branch{Name: "a_branch", UpstreamGone: true, Pushables: "?", Pullables: "?", Head: false},
},
}
@@ -1,65 +0,0 @@
package git_commands
import (
"strings"
"time"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
// CommitStoreLoader populates a commit store with commits from the git log.
type CommitStoreLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewCommitStoreLoader(
cmn *common.Common,
cmd oscommands.ICmdObjBuilder,
) *CommitStoreLoader {
return &CommitStoreLoader{
Common: cmn,
cmd: cmd,
}
}
// mutates the given commit store to add commits from the git log
func (self *CommitStoreLoader) Load(commitStore *models.CommitStore) error {
t := time.Now()
err := self.getLogCmd().RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line)
commitStore.Add(commit)
return false, nil
})
self.Log.Warnf("CommitStoreLoader Load took %s", time.Since(t))
return err
}
// getLog gets the git log.
func (self *CommitStoreLoader) getLogCmd() oscommands.ICmdObj {
cmdArgs := NewGitCmd("log").
Arg("--all").
Arg(`--pretty=format:%H%x00%P`).
ToArgv()
return self.cmd.New(cmdArgs).DontLog()
}
func (self *CommitStoreLoader) extractCommitFromLine(line string) models.ImmutableCommit {
split := strings.SplitN(line, "\x00", 2)
sha := split[0]
parentsStr := split[1]
parents := []string{}
if len(parentsStr) > 0 {
parents = strings.Split(parentsStr, " ")
}
return models.NewImmutableCommit(sha, parents)
}
@@ -1,99 +0,0 @@
package git_commands
import (
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestCommitStoreLoaderLoad(t *testing.T) {
commitsOutputStore := strings.Replace(`a|b
b|c d
c|e
d|e
e|`, "|", "\x00", -1)
args := []string{"log", "--all", "--pretty=format:%H%x00%P"}
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
startingCommits []models.ImmutableCommit
expectedCommits []models.ImmutableCommit
expectedError error
}
scenarios := []scenario{
{
testName: "should return no commits if there are none",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs(args, "", nil),
startingCommits: []models.ImmutableCommit{},
expectedCommits: []models.ImmutableCommit{},
expectedError: nil,
},
{
testName: "properly processes commits",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs(args, commitsOutputStore, nil),
startingCommits: []models.ImmutableCommit{},
expectedCommits: []models.ImmutableCommit{
models.NewImmutableCommit("a", []string{"b"}),
models.NewImmutableCommit("b", []string{"c", "d"}),
models.NewImmutableCommit("c", []string{"e"}),
models.NewImmutableCommit("d", []string{"e"}),
models.NewImmutableCommit("e", []string{}),
},
expectedError: nil,
},
{
testName: "merges into exising commits",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs(args, commitsOutputStore, nil),
startingCommits: []models.ImmutableCommit{
// this one is present in the command output
models.NewImmutableCommit("a", []string{"b"}),
// this one isn't
models.NewImmutableCommit("f", []string{"g"}),
},
expectedCommits: []models.ImmutableCommit{
models.NewImmutableCommit("a", []string{"b"}),
models.NewImmutableCommit("b", []string{"c", "d"}),
models.NewImmutableCommit("c", []string{"e"}),
models.NewImmutableCommit("d", []string{"e"}),
models.NewImmutableCommit("e", []string{}),
models.NewImmutableCommit("f", []string{"g"}),
},
expectedError: nil,
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
common := utils.NewDummyCommon()
builder := &CommitStoreLoader{
Common: common,
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
commitStore := models.NewCommitStore()
commitStore.AddSlice(scenario.startingCommits)
err := builder.Load(commitStore)
assert.EqualValues(t, scenario.expectedCommits, commitStore.Slice())
assert.Equal(t, scenario.expectedError, err)
scenario.runner.CheckForMissingCalls()
})
}
}
+6 -8
View File
@@ -50,12 +50,15 @@ func (self *SyncCommands) Push(opts PushOpts) error {
type FetchOptions struct {
Background bool
RemoteName string
BranchName string
}
// Fetch fetch git repo
func (self *SyncCommands) FetchCmdObj(opts FetchOptions) oscommands.ICmdObj {
func (self *SyncCommands) Fetch(opts FetchOptions) error {
cmdArgs := NewGitCmd("fetch").
ArgIf(self.UserConfig.Git.FetchAll, "--all").
ArgIf(opts.RemoteName != "", opts.RemoteName).
ArgIf(opts.BranchName != "", opts.BranchName).
ToArgv()
cmdObj := self.cmd.New(cmdArgs)
@@ -64,12 +67,7 @@ func (self *SyncCommands) FetchCmdObj(opts FetchOptions) oscommands.ICmdObj {
} else {
cmdObj.PromptOnCredentialRequest()
}
return cmdObj.WithMutex(self.syncMutex)
}
func (self *SyncCommands) Fetch(opts FetchOptions) error {
cmdObj := self.FetchCmdObj(opts)
return cmdObj.Run()
return cmdObj.WithMutex(self.syncMutex).Run()
}
type PullOptions struct {
-61
View File
@@ -92,64 +92,3 @@ func TestSyncPush(t *testing.T) {
})
}
}
func TestSyncFetch(t *testing.T) {
type scenario struct {
testName string
opts FetchOptions
fetchAllConfig bool
test func(oscommands.ICmdObj)
}
scenarios := []scenario{
{
testName: "Fetch in foreground (all=false)",
opts: FetchOptions{Background: false},
fetchAllConfig: false,
test: func(cmdObj oscommands.ICmdObj) {
assert.True(t, cmdObj.ShouldLog())
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.PROMPT)
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch"})
},
},
{
testName: "Fetch in foreground (all=true)",
opts: FetchOptions{Background: false},
fetchAllConfig: true,
test: func(cmdObj oscommands.ICmdObj) {
assert.True(t, cmdObj.ShouldLog())
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.PROMPT)
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch", "--all"})
},
},
{
testName: "Fetch in background (all=false)",
opts: FetchOptions{Background: true},
fetchAllConfig: false,
test: func(cmdObj oscommands.ICmdObj) {
assert.False(t, cmdObj.ShouldLog())
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.FAIL)
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch"})
},
},
{
testName: "Fetch in background (all=true)",
opts: FetchOptions{Background: true},
fetchAllConfig: true,
test: func(cmdObj oscommands.ICmdObj) {
assert.False(t, cmdObj.ShouldLog())
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.FAIL)
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch", "--all"})
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildSyncCommands(commonDeps{})
instance.UserConfig.Git.FetchAll = s.fetchAllConfig
s.test(instance.FetchCmdObj(s.opts))
})
}
}
+4 -13
View File
@@ -5,16 +5,11 @@ package models
type Branch struct {
Name string
// the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf'
DisplayName string
// indicator of when the branch was last checked out e.g. '2d', '3m'
Recency string
// how many commits ahead we are from the remote branch (how many commits we can push)
Pushables string
// how many commits behind we are from the remote branch (how many commits we can pull)
Pullables string
// whether the remote branch is 'gone' i.e. we're tracking a remote branch that has been deleted
DisplayName string
Recency string
Pushables string
Pullables string
UpstreamGone bool
// whether this is the current branch. Exactly one branch should have this be true
Head bool
DetachedHead bool
// if we have a named remote locally this will be the name of that remote e.g.
@@ -22,10 +17,6 @@ type Branch struct {
// 'git@github.com:tiwood/lazygit.git'
UpstreamRemote string
UpstreamBranch string
// subject line in commit message
Subject string
// commit hash
CommitHash string
}
func (b *Branch) FullRefName() string {
+7 -9
View File
@@ -18,6 +18,7 @@ const (
StatusPushed
StatusMerged
StatusRebasing
StatusSelected
StatusReflog
)
@@ -29,21 +30,18 @@ const (
// Commit : A git commit
type Commit struct {
// TODO: rename to hash
Sha string
// Commit message subject line
Sha 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
// SHAs of parent commits (will be multiple if it's a merge commit)
Parents []string
// mutable fields
Status CommitStatus
Action todo.TodoCommand
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
}
func (c *Commit) ShortSha() string {
-164
View File
@@ -1,164 +0,0 @@
package models
import (
"sync"
"github.com/jesseduffield/generics/slices"
"github.com/samber/lo"
)
// This stores immutable commits as a graph for fast lookup
type CommitStore struct {
commitsMap map[string]ImmutableCommit
mutex *sync.RWMutex
}
func NewCommitStore() *CommitStore {
return &CommitStore{commitsMap: make(map[string]ImmutableCommit), mutex: &sync.RWMutex{}}
}
func (self *CommitStore) Add(commit ImmutableCommit) {
self.mutex.Lock()
defer self.mutex.Unlock()
self.commitsMap[commit.Hash()] = commit
}
func (self *CommitStore) AddSlice(commit []ImmutableCommit) {
self.mutex.Lock()
defer self.mutex.Unlock()
for _, commit := range commit {
self.commitsMap[commit.Hash()] = commit
}
}
func (self *CommitStore) GetCommit(hash string) (ImmutableCommit, bool) {
self.mutex.RLock()
defer self.mutex.RUnlock()
commit, ok := self.commitsMap[hash]
return commit, ok
}
func (self *CommitStore) GetParents(hash string) ([]ImmutableCommit, bool) {
commit, ok := self.GetCommit(hash)
if !ok {
return nil, false
}
parents := make([]ImmutableCommit, len(commit.ParentHashes()))
for i, parentHash := range commit.ParentHashes() {
parents[i], ok = self.GetCommit(parentHash)
if !ok {
return nil, false
}
}
return parents, true
}
type IsAncestorResponse int
const (
IsAncestorResponseYes IsAncestorResponse = iota
IsAncestorResponseNo
// returned when we can't find the commit, or if one of the ancestors along the chain can't be found
IsAncestorResponseUnknown
)
var IsAncestorResponseStrings = []string{
"yes",
"no",
"unknown",
}
func (self *CommitStore) IsAncestor(hash string, ancestorHash string) IsAncestorResponse {
if hash == ancestorHash {
return IsAncestorResponseYes
}
commit, ok := self.GetCommit(hash)
if !ok {
return IsAncestorResponseUnknown
}
if commit.IsRoot() {
return IsAncestorResponseNo
}
parentHashes := commit.ParentHashes()
// first check the parent hashes themselves: spares us attempting a lookup of the actual parent structs
for _, parentHash := range parentHashes {
if parentHash == ancestorHash {
return IsAncestorResponseYes
}
}
unknown := false
for _, parentHash := range parentHashes {
response := self.IsAncestor(parentHash, ancestorHash)
if response == IsAncestorResponseYes {
return IsAncestorResponseYes
}
if response == IsAncestorResponseUnknown {
unknown = true
}
}
if unknown {
return IsAncestorResponseUnknown
}
return IsAncestorResponseNo
}
// used for testing
func (self *CommitStore) Slice() []ImmutableCommit {
self.mutex.RLock()
defer self.mutex.RUnlock()
commits := lo.Values(self.commitsMap)
// sort by hash for deterministic result
slices.SortFunc(commits, func(a, b ImmutableCommit) bool {
return a.Hash() < b.Hash()
})
return commits
}
func (self *CommitStore) Size() int {
return len(self.commitsMap)
}
func (self *CommitStore) dfs(hash string, visited map[string]bool) {
visited[hash] = true
commit, ok := self.GetCommit(hash)
if !ok {
return
}
// Traverse the parent hashes
for _, parentHash := range commit.ParentHashes() {
if !visited[parentHash] {
self.dfs(parentHash, visited)
}
}
}
// returns subset of candidates which are ancestors of the given hash
func (self *CommitStore) FindAncestors(hash string, candidates []string) map[string]bool {
visited := make(map[string]bool)
self.dfs(hash, visited)
ancestors := map[string]bool{}
for _, commitHash := range candidates {
if visited[commitHash] {
ancestors[commitHash] = true
}
}
return ancestors
}
-88
View File
@@ -1,88 +0,0 @@
package models
import (
"testing"
)
func TestCommitStore(t *testing.T) {
scenarios := []struct {
testName string
commits []ImmutableCommit
hash string
ancestorHash string
expected IsAncestorResponse
}{
{
testName: "Empty commit store, same hash",
commits: []ImmutableCommit{},
hash: "a",
ancestorHash: "a",
expected: IsAncestorResponseYes,
},
{
testName: "Empty commit store, different hash",
commits: []ImmutableCommit{},
hash: "a",
ancestorHash: "b",
expected: IsAncestorResponseUnknown,
},
{
testName: "Hash found, ancestor not found",
commits: []ImmutableCommit{
NewImmutableCommit("a", []string{"c"}),
NewImmutableCommit("c", []string{}),
},
hash: "a",
ancestorHash: "b",
expected: IsAncestorResponseNo,
},
{
testName: "Hash found, ancestor is parent",
commits: []ImmutableCommit{
NewImmutableCommit("a", []string{"b"}),
NewImmutableCommit("b", []string{"c"}),
},
hash: "a",
ancestorHash: "b",
expected: IsAncestorResponseYes,
},
{
testName: "Hash found, ancestor is grandparent",
commits: []ImmutableCommit{
NewImmutableCommit("a", []string{"b"}),
NewImmutableCommit("b", []string{"c"}),
},
hash: "a",
ancestorHash: "c",
expected: IsAncestorResponseYes,
},
{
testName: "Hash found, not an ancestor",
commits: []ImmutableCommit{
NewImmutableCommit("a", []string{"b"}),
NewImmutableCommit("b", []string{"d"}),
NewImmutableCommit("c", []string{"d"}),
NewImmutableCommit("d", []string{}),
},
hash: "a",
ancestorHash: "c",
expected: IsAncestorResponseNo,
},
}
for _, scenario := range scenarios {
t.Run(scenario.testName, func(t *testing.T) {
commitStore := NewCommitStore()
commitStore.AddSlice(scenario.commits)
response := commitStore.IsAncestor(scenario.hash, scenario.ancestorHash)
if response != scenario.expected {
responseStr := IsAncestorResponseStrings[response]
expectedStr := IsAncestorResponseStrings[scenario.expected]
t.Errorf("Expected %s, got %s", expectedStr, responseStr)
}
})
}
}
-30
View File
@@ -1,30 +0,0 @@
package models
// This model contains the information that is intrinsic to a commit, meaning
// that we can depend on it not changing over time. Git commits are immutable,
// but our other Commit model has extra fields added for ease of use.
type ImmutableCommit struct {
hash string
// hashes of parent commits (will be multiple if it's a merge commit)
parentHashes []string
}
func NewImmutableCommit(hash string, parentHashes []string) ImmutableCommit {
return ImmutableCommit{
hash: hash,
parentHashes: parentHashes,
}
}
func (self *ImmutableCommit) Hash() string {
return self.hash
}
func (self *ImmutableCommit) ParentHashes() []string {
return self.parentHashes
}
func (self *ImmutableCommit) IsRoot() bool {
return len(self.parentHashes) == 0
}
+6 -34
View File
@@ -4,48 +4,20 @@
package oscommands
import (
"bytes"
"io"
"os/exec"
"github.com/sasha-s/go-deadlock"
"github.com/creack/pty"
)
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 {
ptmx, err := pty.Start(cmd)
if 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 },
stdoutPipe: ptmx,
stdinPipe: ptmx,
close: ptmx.Close,
}, nil
}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"strings"
"github.com/OpenPeeDeeP/xdg"
"gopkg.in/yaml.v3"
yaml "github.com/jesseduffield/yaml"
)
// AppConfig contains the base configuration fields required for lazygit.
+1 -1
View File
@@ -1,7 +1,7 @@
package config
import (
"gopkg.in/yaml.v3"
yaml "github.com/jesseduffield/yaml"
)
// NewDummyAppConfig creates a new dummy AppConfig for testing
+6 -14
View File
@@ -49,7 +49,6 @@ type GuiConfig struct {
ShowCommandLog bool `yaml:"showCommandLog"`
ShowBottomLine bool `yaml:"showBottomLine"`
ShowIcons bool `yaml:"showIcons"`
ShowBranchCommitHash bool `yaml:"showBranchCommitHash"`
ExperimentalShowBranchHeads bool `yaml:"experimentalShowBranchHeads"`
CommandLogSize int `yaml:"commandLogSize"`
SplitDiff string `yaml:"splitDiff"`
@@ -82,7 +81,6 @@ type GitConfig struct {
SkipHookPrefix string `yaml:"skipHookPrefix"`
AutoFetch bool `yaml:"autoFetch"`
AutoRefresh bool `yaml:"autoRefresh"`
FetchAll bool `yaml:"fetchAll"`
BranchLogCmd string `yaml:"branchLogCmd"`
AllBranchesLogCmd string `yaml:"allBranchesLogCmd"`
OverrideGpg bool `yaml:"overrideGpg"`
@@ -360,14 +358,15 @@ type CustomCommand struct {
}
type CustomCommandPrompt struct {
Key string `yaml:"key"`
// one of 'input', 'menu', 'confirm', or 'menuFromCommand'
Type string `yaml:"type"`
Key string `yaml:"key"`
Type string `yaml:"type"`
Title string `yaml:"title"`
// these only apply to input prompts
InitialValue string `yaml:"initialValue"`
Suggestions CustomCommandSuggestions `yaml:"suggestions"`
// this only apply to input prompts
InitialValue string `yaml:"initialValue"`
// this only applies to confirm prompts
Body string `yaml:"body"`
@@ -382,11 +381,6 @@ type CustomCommandPrompt struct {
LabelFormat string `yaml:"labelFormat"`
}
type CustomCommandSuggestions struct {
Preset string `yaml:"preset"`
Command string `yaml:"command"`
}
type CustomCommandMenuOption struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
@@ -427,7 +421,6 @@ func GetDefaultConfig() *UserConfig {
ShowRandomTip: true,
ShowIcons: false,
ExperimentalShowBranchHeads: false,
ShowBranchCommitHash: false,
CommandLogSize: 8,
SplitDiff: "auto",
SkipRewordInEditorWarning: false,
@@ -456,7 +449,6 @@ func GetDefaultConfig() *UserConfig {
MainBranches: []string{"master", "main"},
AutoFetch: true,
AutoRefresh: true,
FetchAll: 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",
DisableForcePushing: false,
-2
View File
@@ -25,8 +25,6 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().Diffing.Ref,
c.Tr,
c.UserConfig,
c.Model().CommitStore,
)
}
+2 -6
View File
@@ -21,7 +21,7 @@ type ListContextTrait struct {
// TODO: now that we allow scrolling, we should be smarter about what gets refreshed:
// we should find out exactly which lines are now part of the path and refresh those.
// We should also keep track of the previous path and refresh those lines too.
refreshViewportOnChange bool
refreshViewportOnLineFocus bool
}
func (self *ListContextTrait) IsListContext() {}
@@ -34,7 +34,7 @@ func (self *ListContextTrait) FocusLine() {
self.GetViewTrait().FocusPoint(self.list.GetSelectedLineIdx())
self.setFooter()
if self.refreshViewportOnChange {
if self.refreshViewportOnLineFocus {
self.refreshViewport()
}
}
@@ -65,10 +65,6 @@ func (self *ListContextTrait) HandleFocus(opts types.OnFocusOpts) error {
func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) error {
self.GetViewTrait().SetOriginX(0)
if self.refreshViewportOnChange {
self.refreshViewport()
}
return self.Context.HandleFocusLost(opts)
}
+4 -4
View File
@@ -67,10 +67,10 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
Kind: types.SIDE_CONTEXT,
Focusable: true,
})),
list: viewModel,
getDisplayStrings: getDisplayStrings,
c: c,
refreshViewportOnChange: true,
list: viewModel,
getDisplayStrings: getDisplayStrings,
c: c,
refreshViewportOnLineFocus: true,
},
}
}
+4 -4
View File
@@ -72,10 +72,10 @@ func NewSubCommitsContext(
Focusable: true,
Transient: true,
})),
list: viewModel,
getDisplayStrings: getDisplayStrings,
c: c,
refreshViewportOnChange: true,
list: viewModel,
getDisplayStrings: getDisplayStrings,
c: c,
refreshViewportOnLineFocus: true,
},
}
}
@@ -138,26 +138,15 @@ func (self *MergeAndRebaseHelper) CheckMergeOrRebase(result error) error {
// assume in this case that we're already done
return nil
} else if isMergeConflictErr(result.Error()) {
mode := self.workingTreeStateNoun()
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.FoundConflictsTitle,
Items: []*types.MenuItem{
{
Label: self.c.Tr.ViewConflictsMenuItem,
OnPress: func() error {
return self.c.PushContext(self.c.Contexts().Files)
},
Key: 'v',
},
{
Label: fmt.Sprintf(self.c.Tr.AbortMenuItem, mode),
OnPress: func() error {
return self.genericMergeCommand(REBASE_OPTION_ABORT)
},
Key: 'a',
},
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.FoundConflictsTitle,
Prompt: self.c.Tr.FoundConflicts,
HandleConfirm: func() error {
return self.c.PushContext(self.c.Contexts().Files)
},
HandleClose: func() error {
return self.genericMergeCommand(REBASE_OPTION_ABORT)
},
HideCancel: true,
})
} else {
return self.c.ErrorMsg(result.Error())
+1 -17
View File
@@ -222,7 +222,7 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
// e.g. in the case of switching branches.
func (self *RefreshHelper) refreshCommits() {
wg := sync.WaitGroup{}
wg.Add(3)
wg.Add(2)
go utils.Safe(func() {
self.refreshReflogCommitsConsideringStartup()
@@ -231,12 +231,6 @@ func (self *RefreshHelper) refreshCommits() {
wg.Done()
})
go utils.Safe(func() {
self.refreshCommitStore()
wg.Done()
})
go utils.Safe(func() {
_ = self.refreshCommitsWithLimit()
ctx, ok := self.c.Contexts().CommitFiles.GetParentContext()
@@ -260,16 +254,6 @@ func (self *RefreshHelper) refreshCommits() {
wg.Wait()
}
func (self *RefreshHelper) refreshCommitStore() {
self.c.Mutexes().CommitStoreMutex.Lock()
defer self.c.Mutexes().CommitStoreMutex.Unlock()
err := self.c.Git().Loaders.CommitStoreLoader.Load(self.c.Model().CommitStore)
if err != nil {
_ = self.c.Error(err)
}
}
func (self *RefreshHelper) refreshCommitsWithLimit() error {
self.c.Mutexes().LocalCommitsMutex.Lock()
defer self.c.Mutexes().LocalCommitsMutex.Unlock()
+4 -4
View File
@@ -8,6 +8,7 @@ import (
"strings"
"sync"
"github.com/creack/pty"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazycore/pkg/boxlayout"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
@@ -75,7 +76,7 @@ type Gui struct {
// holds a mapping of view names to ptmx's. This is for rendering command outputs
// from within a pty. The point of keeping track of them is so that if we re-size
// the window, we can tell the pty it needs to resize accordingly.
viewPtmxMap map[string]*os.File
viewPtmxMap map[string]pty.Pty
stopChan chan struct{}
// when lazygit is opened outside a git directory we want to open to the most
@@ -342,7 +343,6 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types.
CommitFiles: nil,
Files: make([]*models.File, 0),
Commits: make([]*models.Commit, 0),
CommitStore: models.NewCommitStore(),
StashEntries: make([]*models.StashEntry, 0),
FilteredReflogCommits: make([]*models.Commit, 0),
ReflogCommits: make([]*models.Commit, 0),
@@ -437,7 +437,7 @@ func NewGui(
Updater: updater,
statusManager: status.NewStatusManager(),
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
viewPtmxMap: map[string]*os.File{},
viewPtmxMap: map[string]pty.Pty{},
showRecentRepos: showRecentRepos,
RepoPathStack: &utils.StringStack{},
RepoStateMap: map[Repo]*GuiRepoState{},
@@ -452,7 +452,6 @@ func NewGui(
RefreshingStatusMutex: &deadlock.Mutex{},
SyncMutex: &deadlock.Mutex{},
LocalCommitsMutex: &deadlock.Mutex{},
CommitStoreMutex: &deadlock.Mutex{},
SubCommitsMutex: &deadlock.Mutex{},
SubprocessMutex: &deadlock.Mutex{},
PopupMutex: &deadlock.Mutex{},
@@ -618,6 +617,7 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
gui.c.Log.Info("starting main loop")
defer gui.handlePanicInTest(startArgs.IntegrationTest)
gui.handleTestMode(startArgs.IntegrationTest)
return gui.g.MainLoop()
+1
View File
@@ -70,6 +70,7 @@ func (self *GuiDriver) Fail(message string) {
self.gui.g.Close()
// need to give the gui time to close
time.Sleep(time.Millisecond * 100)
self.Log("test failed")
panic(fullMessage)
}
+4 -91
View File
@@ -3,55 +3,27 @@ package presentation
import (
"fmt"
"strings"
"time"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/logs"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
var branchPrefixColorCache = make(map[string]style.TextStyle)
func GetBranchListDisplayStrings(
branches []*models.Branch,
fullDescription bool,
diffName string,
tr *i18n.TranslationSet,
userConfig *config.UserConfig,
commitStore *models.CommitStore,
) [][]string {
isContainedInMainBranch := isContainedInMainBranchFn(
userConfig.Git.MainBranches,
branches,
commitStore,
)
if commitStore.Size() > 10 {
logs.Global.Warnf("commitStore.Slice()[0:10]: %v", commitStore.Slice()[0:10])
}
func GetBranchListDisplayStrings(branches []*models.Branch, fullDescription bool, diffName string, tr *i18n.TranslationSet) [][]string {
return slices.Map(branches, func(branch *models.Branch) []string {
diffed := branch.Name == diffName
return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig, isContainedInMainBranch)
return getBranchDisplayStrings(branch, fullDescription, diffed, tr)
})
}
// getBranchDisplayStrings returns the display string of branch
func getBranchDisplayStrings(
b *models.Branch,
fullDescription bool,
diffed bool,
tr *i18n.TranslationSet,
userConfig *config.UserConfig,
isContainedInMainBranch func(string) bool,
) []string {
func getBranchDisplayStrings(b *models.Branch, fullDescription bool, diffed bool, tr *i18n.TranslationSet) []string {
displayName := b.Name
if b.DisplayName != "" {
displayName = b.DisplayName
@@ -71,26 +43,12 @@ func getBranchDisplayStrings(
recencyColor = style.FgGreen
}
res := make([]string, 0, 6)
res := make([]string, 0, 4)
res = append(res, recencyColor.Sprint(b.Recency))
if icons.IsIconEnabled() {
res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b)))
}
if fullDescription || userConfig.Gui.ShowBranchCommitHash {
var hashStyle style.TextStyle
if isContainedInMainBranch(b.CommitHash) {
hashStyle = style.FgGreen
} else {
hashStyle = style.FgYellow
}
coloredHash := hashStyle.Sprint(utils.ShortSha(b.CommitHash))
res = append(res, coloredHash)
}
res = append(res, coloredName)
if fullDescription {
res = append(
res,
@@ -98,7 +56,6 @@ func getBranchDisplayStrings(
style.FgYellow.Sprint(b.UpstreamRemote),
style.FgYellow.Sprint(b.UpstreamBranch),
),
utils.TruncateWithEllipsis(b.Subject, 60),
)
}
return res
@@ -167,47 +124,3 @@ func BranchStatus(branch *models.Branch, tr *i18n.TranslationSet) string {
func SetCustomBranches(customBranchColors map[string]string) {
branchPrefixColorCache = utils.SetCustomColors(customBranchColors)
}
// returns a function that tells us if a given branch's commit is contained in any of the main branches
func isContainedInMainBranchFn(mainBranches []string, branches []*models.Branch, commitStore *models.CommitStore) func(string) bool {
mainBranchHashes := []string{}
for _, branch := range branches {
if lo.Contains(mainBranches, branch.Name) {
mainBranchHashes = append(mainBranchHashes, branch.CommitHash)
}
}
logs.Global.Warnf("mainBranchHashes: %v", mainBranchHashes)
t := time.Now()
ancestorSlices := lo.Map(mainBranchHashes, func(hash string, _ int) map[string]bool {
return commitStore.FindAncestors(
hash,
lo.Map(branches, func(branch *models.Branch, _ int) string {
return branch.CommitHash
}),
)
})
ancestors := lo.Reduce(ancestorSlices, mergeMaps, map[string]bool{})
logs.Global.Warnf("isContainedInMainBranchFn took %v", time.Since(t))
return func(commitHash string) bool {
return ancestors[commitHash]
}
}
func mergeMaps(a, b map[string]bool, _ int) map[string]bool {
res := map[string]bool{}
for k, v := range a {
res[k] = v
}
for k, v := range b {
res[k] = v
}
return res
}
// it's faster to first check if master is a proper ancestor of my commit, because it likely is and it will be faster to find out. If I go the other way around, I need to traverse from master to the root for every branch potentially. Is there a way to speed that up?
+1 -10
View File
@@ -139,7 +139,7 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
// a traversed spot is one where a current pipe is starting on, ending on, or passing through
traversedSpots := set.New[int]()
if len(commit.Parents) > 0 { // merge commit
if len(commit.Parents) > 0 {
newPipes = append(newPipes, &Pipe{
fromPos: pos,
toPos: pos,
@@ -148,15 +148,6 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
kind: STARTS,
style: getStyle(commit),
})
} else if len(commit.Parents) == 0 { // root commit
newPipes = append(newPipes, &Pipe{
fromPos: pos,
toPos: pos,
fromSha: commit.Sha,
toSha: models.EmptyTreeCommitHash,
kind: STARTS,
style: getStyle(commit),
})
}
traversedSpotsForContinuingPipes := set.New[int]()
-12
View File
@@ -509,18 +509,6 @@ func TestGetNextPipes(t *testing.T) {
{fromPos: 1, toPos: 1, fromSha: "d", toSha: "e", kind: STARTS, style: style.FgDefault},
},
},
{
prevPipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "root", kind: TERMINATES, style: style.FgDefault},
},
commit: &models.Commit{
Sha: "root",
Parents: []string{},
},
expected: []*Pipe{
{fromPos: 1, toPos: 1, fromSha: "root", toSha: models.EmptyTreeCommitHash, kind: STARTS, style: style.FgDefault},
},
},
}
for _, test := range tests {
+1 -2
View File
@@ -5,7 +5,6 @@ package gui
import (
"io"
"os"
"os/exec"
"strings"
@@ -57,7 +56,7 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
manager := gui.getManager(view)
var ptmx *os.File
var ptmx pty.Pty
start := func() (*exec.Cmd, io.Reader) {
var err error
ptmx, err = pty.StartWithSize(cmd, gui.desiredPtySize())
+1 -1
View File
@@ -19,7 +19,7 @@ func NewClient(
helpers *helpers.Helpers,
) *Client {
sessionStateLoader := NewSessionStateLoader(c, helpers.Refs)
handlerCreator := NewHandlerCreator(c, sessionStateLoader, helpers.Suggestions)
handlerCreator := NewHandlerCreator(c, sessionStateLoader)
keybindingCreator := NewKeybindingCreator(c)
customCommands := c.UserConfig.CustomCommands
@@ -1,7 +1,6 @@
package custom_commands
import (
"fmt"
"strings"
"text/template"
@@ -11,7 +10,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
// takes a custom command and returns a function that will be called when the corresponding user-defined keybinding is pressed
@@ -20,13 +18,11 @@ type HandlerCreator struct {
sessionStateLoader *SessionStateLoader
resolver *Resolver
menuGenerator *MenuGenerator
suggestionsHelper *helpers.SuggestionsHelper
}
func NewHandlerCreator(
c *helpers.HelperCommon,
sessionStateLoader *SessionStateLoader,
suggestionsHelper *helpers.SuggestionsHelper,
) *HandlerCreator {
resolver := NewResolver(c.Common)
menuGenerator := NewMenuGenerator(c.Common)
@@ -36,7 +32,6 @@ func NewHandlerCreator(
sessionStateLoader: sessionStateLoader,
resolver: resolver,
menuGenerator: menuGenerator,
suggestionsHelper: suggestionsHelper,
}
}
@@ -109,81 +104,15 @@ func (self *HandlerCreator) call(customCommand config.CustomCommand) func() erro
}
func (self *HandlerCreator) inputPrompt(prompt *config.CustomCommandPrompt, wrappedF func(string) error) error {
findSuggestionsFn, err := self.generateFindSuggestionsFunc(prompt)
if err != nil {
return self.c.Error(err)
}
return self.c.Prompt(types.PromptOpts{
Title: prompt.Title,
InitialContent: prompt.InitialValue,
FindSuggestionsFunc: findSuggestionsFn,
Title: prompt.Title,
InitialContent: prompt.InitialValue,
HandleConfirm: func(str string) error {
return wrappedF(str)
},
})
}
func (self *HandlerCreator) generateFindSuggestionsFunc(prompt *config.CustomCommandPrompt) (func(string) []*types.Suggestion, error) {
if prompt.Suggestions.Preset != "" && prompt.Suggestions.Command != "" {
return nil, fmt.Errorf(
fmt.Sprintf(
"Custom command prompt cannot have both a preset and a command for suggestions. Preset: '%s', Command: '%s'",
prompt.Suggestions.Preset,
prompt.Suggestions.Command,
),
)
} else if prompt.Suggestions.Preset != "" {
return self.getPresetSuggestionsFn(prompt.Suggestions.Preset)
} else if prompt.Suggestions.Command != "" {
return self.getCommandSuggestionsFn(prompt.Suggestions.Command)
}
return nil, nil
}
func (self *HandlerCreator) getCommandSuggestionsFn(command string) (func(string) []*types.Suggestion, error) {
lines := []*types.Suggestion{}
err := self.c.OS().Cmd.NewShell(command).RunAndProcessLines(func(line string) (bool, error) {
lines = append(lines, &types.Suggestion{Value: line, Label: line})
return false, nil
})
if err != nil {
return nil, err
}
return func(currentWord string) []*types.Suggestion {
return lo.Filter(lines, func(suggestion *types.Suggestion, _ int) bool {
return strings.Contains(strings.ToLower(suggestion.Value), strings.ToLower(currentWord))
})
}, nil
}
func (self *HandlerCreator) getPresetSuggestionsFn(preset string) (func(string) []*types.Suggestion, error) {
switch preset {
case "files":
return self.suggestionsHelper.GetFilePathSuggestionsFunc(), nil
case "branches":
return self.suggestionsHelper.GetBranchNameSuggestionsFunc(), nil
case "remotes":
return self.suggestionsHelper.GetRemoteSuggestionsFunc(), nil
case "remoteBranches":
return self.suggestionsHelper.GetRemoteBranchesSuggestionsFunc("/"), nil
case "refs":
return self.suggestionsHelper.GetRefsSuggestionsFunc(), nil
default:
return nil, fmt.Errorf("Unknown value for suggestionsPreset in custom command: %s. Valid values: files, branches, remotes, remoteBranches, refs", preset)
}
}
func (self *HandlerCreator) confirmPrompt(prompt *config.CustomCommandPrompt, handleConfirm func() error) error {
return self.c.Confirm(types.ConfirmOpts{
Title: prompt.Title,
Prompt: prompt.Body,
HandleConfirm: handleConfirm,
})
}
func (self *HandlerCreator) menuPrompt(prompt *config.CustomCommandPrompt, wrappedF func(string) error) error {
menuItems := slices.Map(prompt.Options, func(option config.CustomCommandMenuOption) *types.MenuItem {
return &types.MenuItem{
@@ -197,6 +126,14 @@ func (self *HandlerCreator) menuPrompt(prompt *config.CustomCommandPrompt, wrapp
return self.c.Menu(types.CreateMenuOptions{Title: prompt.Title, Items: menuItems})
}
func (self *HandlerCreator) confirmPrompt(prompt *config.CustomCommandPrompt, handleConfirm func() error) error {
return self.c.Confirm(types.ConfirmOpts{
Title: prompt.Title,
Prompt: prompt.Body,
HandleConfirm: handleConfirm,
})
}
func (self *HandlerCreator) menuPromptFromCommand(prompt *config.CustomCommandPrompt, wrappedF func(string) error) error {
// Run and save output
message, err := self.c.Git().Custom.RunWithOutput(prompt.Command)
@@ -210,7 +147,7 @@ func (self *HandlerCreator) menuPromptFromCommand(prompt *config.CustomCommandPr
return self.c.Error(err)
}
menuItems := slices.Map(candidates, func(candidate *commandMenuItem) *types.MenuItem {
menuItems := slices.Map(candidates, func(candidate *commandMenuEntry) *types.MenuItem {
return &types.MenuItem{
LabelColumns: []string{candidate.label},
OnPress: func() error {
@@ -22,41 +22,12 @@ func NewMenuGenerator(c *common.Common) *MenuGenerator {
return &MenuGenerator{c: c}
}
type commandMenuItem struct {
type commandMenuEntry struct {
label string
value string
}
func (self *MenuGenerator) call(commandOutput, filter, valueFormat, labelFormat string) ([]*commandMenuItem, error) {
menuItemFromLine, err := self.getMenuItemFromLinefn(filter, valueFormat, labelFormat)
if err != nil {
return nil, err
}
menuItems := []*commandMenuItem{}
for _, line := range strings.Split(commandOutput, "\n") {
if line == "" {
continue
}
menuItem, err := menuItemFromLine(line)
if err != nil {
return nil, err
}
menuItems = append(menuItems, menuItem)
}
return menuItems, nil
}
func (self *MenuGenerator) getMenuItemFromLinefn(filter string, valueFormat string, labelFormat string) (func(line string) (*commandMenuItem, error), error) {
if filter == "" && valueFormat == "" && labelFormat == "" {
// showing command output lines as-is in suggestions panel
return func(line string) (*commandMenuItem, error) {
return &commandMenuItem{label: line, value: line}, nil
}, nil
}
func (self *MenuGenerator) call(commandOutput, filter, valueFormat, labelFormat string) ([]*commandMenuEntry, error) {
regex, err := regexp.Compile(filter)
if err != nil {
return nil, errors.New("unable to parse filter regex, error: " + err.Error())
@@ -80,25 +51,37 @@ func (self *MenuGenerator) getMenuItemFromLinefn(filter string, valueFormat stri
labelTemplate = valueTemplate
}
return func(line string) (*commandMenuItem, error) {
return self.generateMenuItem(
candidates := []*commandMenuEntry{}
for _, line := range strings.Split(commandOutput, "\n") {
if line == "" {
continue
}
candidate, err := self.generateMenuCandidate(
line,
regex,
valueTemplate,
labelTemplate,
)
}, nil
if err != nil {
return nil, err
}
candidates = append(candidates, candidate)
}
return candidates, err
}
func (self *MenuGenerator) generateMenuItem(
func (self *MenuGenerator) generateMenuCandidate(
line string,
regex *regexp.Regexp,
valueTemplate *TrimmerTemplate,
labelTemplate *TrimmerTemplate,
) (*commandMenuItem, error) {
) (*commandMenuEntry, error) {
tmplData := self.parseLine(line, regex)
entry := &commandMenuItem{}
entry := &commandMenuEntry{}
var err error
entry.value, err = valueTemplate.execute(tmplData)
@@ -14,7 +14,7 @@ func TestMenuGenerator(t *testing.T) {
filter string
valueFormat string
labelFormat string
test func([]*commandMenuItem, error)
test func([]*commandMenuEntry, error)
}
scenarios := []scenario{
@@ -24,7 +24,7 @@ func TestMenuGenerator(t *testing.T) {
"(?P<remote>[a-z_]+)/(?P<branch>.*)",
"{{ .branch }}",
"Remote: {{ .remote }}",
func(actualEntry []*commandMenuItem, err error) {
func(actualEntry []*commandMenuEntry, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "pr-1", actualEntry[0].value)
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
@@ -36,7 +36,7 @@ func TestMenuGenerator(t *testing.T) {
"(?P<remote>[a-z]*)/(?P<branch>.*)",
"{{ .branch }}|{{ .remote }}",
"",
func(actualEntry []*commandMenuItem, err error) {
func(actualEntry []*commandMenuEntry, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].label)
@@ -48,36 +48,12 @@ func TestMenuGenerator(t *testing.T) {
"(?P<remote>[a-z]*)/(?P<branch>.*)",
"{{ .group_2 }}|{{ .group_1 }}",
"Remote: {{ .group_1 }}",
func(actualEntry []*commandMenuItem, err error) {
func(actualEntry []*commandMenuEntry, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
},
},
{
"No named groups",
"upstream/pr-1",
"([a-z]*)/(.*)",
"{{ .group_2 }}|{{ .group_1 }}",
"Remote: {{ .group_1 }}",
func(actualEntry []*commandMenuItem, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
},
},
{
"No filter",
"upstream/pr-1",
"",
"",
"",
func(actualEntry []*commandMenuItem, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "upstream/pr-1", actualEntry[0].value)
assert.EqualValues(t, "upstream/pr-1", actualEntry[0].label)
},
},
}
for _, s := range scenarios {
@@ -34,16 +34,6 @@ func (self *Resolver) resolvePrompt(
return nil, err
}
result.Suggestions.Preset, err = resolveTemplate(prompt.Suggestions.Preset)
if err != nil {
return nil, err
}
result.Suggestions.Command, err = resolveTemplate(prompt.Suggestions.Command)
if err != nil {
return nil, err
}
result.Body, err = resolveTemplate(prompt.Body)
if err != nil {
return nil, err
+23
View File
@@ -1,6 +1,8 @@
package style
import (
"os"
"github.com/gookit/color"
)
@@ -25,6 +27,23 @@ import (
// a string, we derive it when a new TextStyle is created and store it in the
// `style` field.
// See https://github.com/xtermjs/xterm.js/issues/4238
// VSCode is soon to fix this in an upcoming update.
// Once that's done, we can scrap the HIDE_UNDERSCORES variable
var (
underscoreEnvChecked bool
hideUnderscores bool
)
func hideUnderScores() bool {
if !underscoreEnvChecked {
hideUnderscores = os.Getenv("TERM_PROGRAM") == "vscode"
underscoreEnvChecked = true
}
return hideUnderscores
}
type TextStyle struct {
fg *Color
bg *Color
@@ -64,6 +83,10 @@ func (b TextStyle) SetBold() TextStyle {
}
func (b TextStyle) SetUnderline() TextStyle {
if hideUnderScores() {
return b
}
b.decoration.SetUnderline()
b.Style = b.deriveStyle()
return b
+49 -15
View File
@@ -1,12 +1,15 @@
package gui
import (
"fmt"
"log"
"os"
"runtime"
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/result"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -16,29 +19,60 @@ type IntegrationTest interface {
}
func (gui *Gui) handleTestMode(test integrationTypes.IntegrationTest) {
if os.Getenv(components.SANDBOX_ENV_VAR) == "true" {
if os.Getenv(components.SANDBOX_ENV_VAR) == "true" || test == nil {
return
}
if test != nil {
go func() {
time.Sleep(time.Millisecond * 100)
go func() {
defer gui.handlePanicInTest(test)
test.Run(&GuiDriver{gui: gui})
time.Sleep(time.Millisecond * 100)
gui.g.Update(func(*gocui.Gui) error {
return gocui.ErrQuit
})
guiDriver := &GuiDriver{gui: gui}
test.Run(guiDriver)
time.Sleep(time.Second * 1)
// if we're here then the test must have passed: it panics upon failure
gui.Log.Warnf("test %s logging success", test.Name())
if err := result.LogSuccess(); err != nil {
gui.Log.Warnf("test %s failed to log success!", test.Name())
panic(err)
}
log.Fatal("gocui should have already exited")
}()
go utils.Safe(func() {
time.Sleep(time.Second * 40)
log.Fatal("40 seconds is up, lazygit recording took too long to complete")
gui.g.Update(func(*gocui.Gui) error {
return gocui.ErrQuit
})
time.Sleep(time.Second * 1)
log.Fatal("gocui should have already exited")
}()
go utils.Safe(func() {
defer gui.handlePanicInTest(test)
time.Sleep(time.Second * 40)
panic("40 seconds is up, lazygit recording took too long to complete")
})
}
func (gui *Gui) handlePanicInTest(test integrationTypes.IntegrationTest) {
if test == nil {
return
}
if r := recover(); r != nil {
buf := make([]byte, 4096*4) // arbitrarily large buffer size
stackSize := runtime.Stack(buf, false)
stackTrace := string(buf[:stackSize])
gui.Log.Warnf("test %s panicked!", test.Name())
if err := result.LogFailure(fmt.Sprintf("%v\n%s", r, stackTrace)); err != nil {
panic(err)
}
// Re-panic
panic(r)
}
}
-2
View File
@@ -192,7 +192,6 @@ type Model struct {
Submodules []*models.SubmoduleConfig
Branches []*models.Branch
Commits []*models.Commit
CommitStore *models.CommitStore
StashEntries []*models.StashEntry
SubCommits []*models.Commit
Remotes []*models.Remote
@@ -221,7 +220,6 @@ type Mutexes struct {
RefreshingStatusMutex *deadlock.Mutex
SyncMutex *deadlock.Mutex
LocalCommitsMutex *deadlock.Mutex
CommitStoreMutex *deadlock.Mutex
SubCommitsMutex *deadlock.Mutex
SubprocessMutex *deadlock.Mutex
PopupMutex *deadlock.Mutex
+1
View File
@@ -175,6 +175,7 @@ func chineseTranslationSet() TranslationSet {
ReturnToFilesPanel: `返回文件面板`,
FastForward: `从上游快进此分支`,
Fetching: "抓取并快进 {{.from}} -> {{.to}} ...",
FoundConflicts: "冲突!需要中止操作请按‘esc’, 否则按‘enter’",
FoundConflictsTitle: "自动合并失败",
ViewMergeRebaseOptions: "查看 合并/变基 选项",
NotMergingOrRebasing: "您目前既不进行变基也不进行合并",
+2 -1
View File
@@ -140,7 +140,8 @@ func dutchTranslationSet() TranslationSet {
ReturnToFilesPanel: `Ga terug naar het bestanden paneel`,
FastForward: `Fast-forward deze branch vanaf zijn upstream`,
Fetching: "Fetching en fast-forwarding {{.from}} -> {{.to}} ...",
FoundConflictsTitle: "Conflicten!",
FoundConflicts: "Conflicten!, Om af te breken druk 'esc', anders druk op 'enter'",
FoundConflictsTitle: "Auto-merge mislukt",
ViewMergeRebaseOptions: "Bekijk merge/rebase opties",
NotMergingOrRebasing: "Je bent momenteel niet aan het rebasen of mergen",
RecentRepos: "Recente repositories",
+5 -7
View File
@@ -2,8 +2,8 @@
Todo list when making a new translation
- Copy this file and rename it to the language you want to translate to like someLanguage.go
- Change the EnglishTranslationSet() name to the language you want to translate to like SomeLanguageTranslationSet()
- Add an entry of someLanguage in GetTranslationSets()
- Change the addEnglish() name to the language you want to translate to like addSomeLanguage()
- change the first function argument of i18nObject.AddMessages( to the language you want to translate to like language.SomeLanguage
- Remove this todo and the about section
*/
@@ -181,9 +181,8 @@ type TranslationSet struct {
ReturnToFilesPanel string
FastForward string
Fetching string
FoundConflicts string
FoundConflictsTitle string
ViewConflictsMenuItem string
AbortMenuItem string
PickHunk string
PickAllHunks string
ViewMergeRebaseOptions string
@@ -868,9 +867,8 @@ func EnglishTranslationSet() TranslationSet {
ReturnToFilesPanel: `Return to files panel`,
FastForward: `Fast-forward this branch from its upstream`,
Fetching: "Fetching and fast-forwarding {{.from}} -> {{.to}} ...",
FoundConflictsTitle: "Conflicts!",
ViewConflictsMenuItem: "View conflicts",
AbortMenuItem: "Abort the %s",
FoundConflicts: "Conflicts! To abort press 'esc', otherwise press 'enter'",
FoundConflictsTitle: "Auto-merge failed",
ViewMergeRebaseOptions: "View merge/rebase options",
NotMergingOrRebasing: "You are currently neither rebasing nor merging",
AlreadyRebasing: "Can't perform this action during a rebase",
+7 -8
View File
@@ -17,7 +17,7 @@ type Localizer struct {
func NewTranslationSetFromConfig(log *logrus.Entry, configLanguage string) (*TranslationSet, error) {
if configLanguage == "auto" {
language := detectLanguage(jibber_jabber.DetectIETF)
language := detectLanguage(jibber_jabber.DetectLanguage)
return NewTranslationSet(log, language), nil
}
@@ -46,13 +46,12 @@ func NewTranslationSet(log *logrus.Entry, language string) *TranslationSet {
// GetTranslationSets gets all the translation sets, keyed by language code
func GetTranslationSets() map[string]TranslationSet {
return map[string]TranslationSet{
"pl": polishTranslationSet(),
"nl": dutchTranslationSet(),
"en": EnglishTranslationSet(),
"zh-CN": chineseTranslationSet(), // Simplified Chinese
"zh-TW": traditionalChineseTranslationSet(),
"ja": japaneseTranslationSet(),
"ko": koreanTranslationSet(),
"pl": polishTranslationSet(),
"nl": dutchTranslationSet(),
"en": EnglishTranslationSet(),
"zh": chineseTranslationSet(),
"ja": japaneseTranslationSet(),
"ko": koreanTranslationSet(),
}
}
+1
View File
@@ -176,6 +176,7 @@ func koreanTranslationSet() TranslationSet {
ReturnToFilesPanel: `파일 목록으로 돌아가기`,
FastForward: `Fast-forward this branch from its upstream`,
Fetching: "Fetching and fast-forwarding {{.from}} -> {{.to}} ...",
FoundConflicts: "Conflicts! To abort press 'esc', otherwise press 'enter'",
FoundConflictsTitle: "Auto-merge failed",
ViewMergeRebaseOptions: "View merge/rebase options",
NotMergingOrRebasing: "You are currently neither rebasing nor merging",
+2 -1
View File
@@ -125,7 +125,8 @@ func polishTranslationSet() TranslationSet {
MoveUpCommit: "Przenieś commit 1 w górę",
EditCommit: "Edytuj commit",
AmendToCommit: "Popraw commit zmianami z poczekalni",
FoundConflictsTitle: "Konflikty!",
FoundConflicts: "Konflikty! Wciśnij 'esc' żeby przerwać, w przeciwnym razie wciśnij 'enter'",
FoundConflictsTitle: "Automatyczne scalenie nie powiodło się",
ViewMergeRebaseOptions: "Widok scalenia/opcje zmiany bazy",
NotMergingOrRebasing: "W tej chwili nie scalasz ani nie zmieniasz bazy",
RecentRepos: "Ostatnie repozytoria",
-713
View File
@@ -1,713 +0,0 @@
/*
繁體中文翻譯詞彙選擇主要依據下列來源:
The selection of Traditional Chinese translation vocabulary is mainly based on
the following sources:
1. GitLab: 其介面有相當完整的繁體中文翻譯,但缺少一些本地端功能的對照,例如 stash。
2. Pro Git: Git 的權威參考用書,可惜繁中部分翻譯僅約一半。
https://git-scm.com/book/zh-tw/v2
3. Microsoft 語言入口網站 (Visual Studio)
https://www.microsoft.com/zh-tw/language/
### Glossary ###
譯文中括號內文字會依語境添加或省略。
Repository 版本庫
Amend 修正
Checkout 檢出
Cherry-pick 揀選
Diff 差異
Discard 捨棄
Drop [stash] 捨棄
Fast-forward 快進式 (Fast-forward)
Fetch 擷取
Fixup 修復 (Fixup)
Patch 補丁
Pop [stash] 還原
Rebase 變基 (Rebase)
Reset 重設
Revert 還原
Reword 改寫
Squash 壓縮 (Squash)
Stage 預存 (Stage)
Stash 收藏 (Stash)
*/
package i18n
const traditionalChineseIntroPopupMessage = `
感謝你使用 lazygit,你真的很厲害。有三件事要與你分享:
1) 如果你想了解 lazygit 的功能,請看這個影片:
https://youtu.be/CPLdltN7wgE
2) 請務必閱讀最新的發布說明:
https://github.com/jesseduffield/lazygit/releases
3) 如果你正在使用 git,那麼就是一個程式設計師!在你的幫助下,
我們可以使 lazygit 變得更好,請考慮成為貢獻者並一起同樂:
https://github.com/jesseduffield/lazygit
你也可以贊助我,並告訴我有什麼要改進的,點擊右下方的捐贈按鈕即可。
就算是只在版本庫中點個星星分享愛也很棒!
`
const traditionalChineseDeprecatedEditConfigWarning = `
### Deprecated config warning ###
以下設定已被取代並將於未來版本中刪除:
{{configs}}
請參考以下連結獲取關於如何設定編輯器的最新資訊:
https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#configuring-file-editing
`
// exporting this so we can use it in tests
func traditionalChineseTranslationSet() TranslationSet {
return TranslationSet{
NotEnoughSpace: "沒有足夠空間可以繪製面板",
DiffTitle: "差異",
FilesTitle: "檔案",
BranchesTitle: "分支",
CommitsTitle: "提交",
StashTitle: "收藏 (Stash)",
SnakeTitle: "貪食蛇",
EasterEgg: "彩蛋",
UnstagedChanges: "未預存變更",
StagedChanges: "已預存變更",
MainTitle: "主視窗",
MergeConfirmTitle: "合併",
StagingTitle: "主視窗 (預存中)",
MergingTitle: "主視窗 (合併中)",
NormalTitle: "主視窗 (一般)",
LogTitle: "版本記錄",
CommitSummary: "提交摘要",
CredentialsUsername: "使用者名稱",
CredentialsPassword: "密碼",
CredentialsPassphrase: "輸入 SSH 金鑰的密碼",
CredentialsPIN: "輸入 SSH 金鑰的 PIN 碼",
PassUnameWrong: "密碼、密語或使用者名稱錯誤",
CommitChanges: "提交變更",
AmendLastCommit: "修正上次提交",
AmendLastCommitTitle: "修正上次提交",
SureToAmend: "是否確定要修正上次提交?之後你可以從提交面板中再次更改此次提交的訊息。",
NoCommitToAmend: "沒有可以修正的提交。",
CommitChangesWithEditor: "使用 git 編輯器提交變更",
StatusTitle: "狀態",
Menu: "功能表",
Execute: "執行",
ToggleStaged: "切換預存",
ToggleStagedAll: "全部預存/取消預存",
ToggleTreeView: "切換檔案樹狀視圖",
OpenMergeTool: "開啟外部合併工具 (git mergetool)",
Refresh: "重新整理",
Push: "推送",
Pull: "拉取",
Scroll: "捲動",
MergeConflictsTitle: "合併衝突",
Checkout: "檢出",
FileFilter: "篩選檔案 (預存/未預存)",
FilterStagedFiles: "僅顯示預存的檔案",
FilterUnstagedFiles: "僅顯示未預存的檔案",
ResetCommitFilterState: "重設篩選",
NoChangedFiles: "沒有變更的檔案",
PullWait: "拉取...",
PushWait: "推送...",
FetchWait: "擷取...",
SoftReset: "軟重設",
AlreadyCheckedOutBranch: "你已經檢出這個分支了",
SureForceCheckout: "你確定要強制檢出嗎?這將會使你失去本地的所有更改",
ForceCheckoutBranch: "強制檢出分支",
BranchName: "分支名稱",
NewBranchNameBranchOff: "新的分支名稱 (根據 '{{.branchName}}' 分支創建)",
CantDeleteCheckOutBranch: "你不能刪除已檢出的分支!",
DeleteBranch: "刪除分支",
DeleteBranchMessage: "你確定要刪除 '{{.selectedBranchName}}' 分支嗎?",
ForceDeleteBranchMessage: "'{{.selectedBranchName}}' 分支尚未完全合併。你確定要刪除嗎?",
RebaseBranch: "將已檢出的分支變基至此分支",
CantRebaseOntoSelf: "你不能將分支變基至自己",
CantMergeBranchIntoItself: "你不能將一個分支合併至自己",
ForceCheckout: "強制檢出",
CheckoutByName: "根據名稱檢出",
NewBranch: "新分支",
NoBranchesThisRepo: "這個版本庫中沒有分支",
CommitWithoutMessageErr: "沒有提交訊息,無法提交",
Close: "關閉",
CloseCancel: "關閉/取消",
Confirm: "確認",
Quit: "結束",
SquashDown: "向下壓縮",
FixupCommit: "修復提交 (Fixup)",
NoCommitsThisBranch: "這個分支沒有提交",
UpdateRefHere: "在這裡更新 '{{.ref}}' 分支",
CannotSquashOrFixupFirstCommit: "沒有可以壓縮的提交",
Fixup: "修復 (Fixup)",
SureFixupThisCommit: "你確定要對這個提交進行 '修復' 嗎? 它將被合併到下面的提交中",
SureSquashThisCommit: "你確定要把這個提交壓縮到下面的提交中嗎?",
Squash: "壓縮 (Squash)",
PickCommit: "挑選提交 (於變基過程中)",
RevertCommit: "還原提交",
RewordCommit: "改寫提交",
DeleteCommit: "刪除提交",
MoveDownCommit: "向下移動提交",
MoveUpCommit: "向上移動提交",
EditCommit: "編輯提交",
AmendToCommit: "使用已預存的更改修正提交",
ResetAuthor: "重設作者",
SetAuthor: "設置作者",
SetResetCommitAuthor: "設置/重設提交作者",
SetAuthorPromptTitle: "設置作者 (格式如 '姓名 <電子郵件>')",
SureResetCommitAuthor: "此提交的作者欄位將被更新以符合已配置的使用者。它也會更新作者的時間戳。繼續嗎?",
RenameCommitEditor: "使用編輯器改寫提交",
Error: "錯誤",
PickHunk: "挑選程式碼片段",
PickAllHunks: "挑選所有程式碼片段",
Undo: "復原",
UndoReflog: "復原",
RedoReflog: "取消復原",
UndoTooltip: "將使用 reflog 確定要運行哪個 git 命令以復原上一個 git 命令。這不包括工作區的更改;只考慮提交。",
RedoTooltip: "將使用 reflog 確定要運行哪個 git 命令以重作上一個 git 命令。這不包括工作區的更改;只考慮提交。",
DiscardAllTooltip: "捨棄 '{{.path}}' 中的預存和未預存的更改。",
DiscardUnstagedTooltip: "捨棄 '{{.path}}' 中的未預存的更改。",
Pop: "還原",
Drop: "捨棄",
Apply: "套用",
NoStashEntries: "沒有收藏記錄",
StashDrop: "放棄收藏記錄",
SureDropStashEntry: "你確定要捨棄這條收藏記錄嗎?",
StashPop: "還原收藏記錄",
SurePopStashEntry: "你確定要從收藏中還原這個記錄嗎?",
StashApply: "套用收藏記錄",
SureApplyStashEntry: "你確定要套用這個收藏記錄嗎?",
NoTrackedStagedFilesStash: "你沒有被追蹤的、預存的檔案可進行收藏",
NoFilesToStash: "沒有檔案可以進行收藏",
StashChanges: "安置現有變更到收藏中",
RenameStash: "重新命名收藏",
RenameStashPrompt: "重新命名收藏:{{.stashName}}",
OpenConfig: "開啟設定檔案",
EditConfig: "編輯設定檔案",
ForcePush: "強制推送",
ForcePushPrompt: "你的分支與遠端分支分岔。按 'ESC' 取消,或按 'Enter' 強制推送。",
ForcePushDisabled: "你的分支與遠端分支分岔,你已禁用強制推送",
UpdatesRejectedAndForcePushDisabled: "更新被拒絕,你已禁用強制推送",
CheckForUpdate: "檢查更新",
CheckingForUpdates: "正在檢查更新...",
UpdateAvailableTitle: "有可用的更新!",
UpdateAvailable: "下載並安裝版本 {{.newVersion}}?",
UpdateInProgressWaitingStatus: "更新中",
UpdateCompletedTitle: "更新已完成!",
UpdateCompleted: "更新已成功安裝。為了使其生效,請重新啟動 lazygit。",
FailedToRetrieveLatestVersionErr: "無法獲取版本資訊",
OnLatestVersionErr: "你已經有最新版本",
MajorVersionErr: "新版本({{.newVersion}})與當前版本({{.currentVersion}})存在不向後兼容的更改",
CouldNotFindBinaryErr: "找不到 {{.url}} 路徑下的任何二進位檔",
UpdateFailedErr: "更新失敗:{{.errMessage}}",
ConfirmQuitDuringUpdateTitle: "正在更新中",
ConfirmQuitDuringUpdate: "正在進行更新,你確定要結束?",
MergeToolTitle: "合併工具",
MergeToolPrompt: "你要開啟 'git mergetool' 嗎?",
IntroPopupMessage: traditionalChineseIntroPopupMessage,
DeprecatedEditConfigWarning: traditionalChineseDeprecatedEditConfigWarning,
GitconfigParseErr: `Gogit 無法解析你的 gitconfig 檔案,因為存在未引用的 '\' 字符,刪除它們應該可以解決這個問題。`,
EditFile: `編輯檔案`,
OpenFile: `開啟檔案`,
IgnoreFile: `添加到 .gitignore`,
ExcludeFile: `添加到 .git/info/exclude`,
RefreshFiles: `重新整理檔案`,
MergeIntoCurrentBranch: `合併到當前檢出的分支`,
ConfirmQuit: `你確定要結束嗎?`,
SwitchRepo: `切換到最近使用的版本庫`,
AllBranchesLogGraph: `顯示所有分支日誌`,
UnsupportedGitService: `不支持的 Git 服務`,
CreatePullRequest: `建立拉取請求`,
CopyPullRequestURL: `複製拉取請求的 URL 到剪貼板`,
NoBranchOnRemote: `這個分支在遠端不存在。需要先將其推送至遠端。`,
Fetch: `擷取`,
NoAutomaticGitFetchTitle: `不自動 git 擷取`,
NoAutomaticGitFetchBody: `lazygit 無法在私有庫使用 "git 擷取";在檔案面板中使用 'f' 手動執行 "git 擷取"`,
FileEnter: `選擇檔案中的單個程式碼塊/行,或展開/折疊目錄`,
FileStagingRequirements: `只能選擇跟踪檔案中的單個行`,
StageSelection: `切換現有行的狀態 (已預存/未預存)`,
ResetSelection: `刪除變更 (git reset)`,
ToggleDragSelect: `切換拖曳選擇`,
ToggleSelectHunk: `切換選擇程式碼塊`,
ToggleSelectionForPatch: `向 (或從) 補丁中添加/刪除行`,
EditHunk: `編輯程式碼塊`,
ToggleStagingPanel: `切換至另一個面板 (已預存/未預存更改)`,
ReturnToFilesPanel: `返回檔案面板`,
FastForward: `從上游快進此分支`,
Fetching: "{{.from}} -> {{.to}} 的擷取和快進中...",
FoundConflictsTitle: "自動合併失敗",
ViewMergeRebaseOptions: "查看合併/變基選項",
NotMergingOrRebasing: "你當前既不在變基也不在合併中",
AlreadyRebasing: "無法在變基期間執行此操作",
RecentRepos: "最近的版本庫",
MergeOptionsTitle: "合併選項",
RebaseOptionsTitle: "變基選項",
CommitMessageTitle: "提交摘要",
CommitDescriptionTitle: "提交描述",
CommitDescriptionSubTitle: "按 tab 切換焦點",
LocalBranchesTitle: "本地分支",
SearchTitle: "搜尋",
TagsTitle: "標籤",
MenuTitle: "功能表",
RemotesTitle: "遠端",
RemoteBranchesTitle: "遠端分支",
PatchBuildingTitle: "主面板 (補丁生成)",
InformationTitle: "資訊",
SecondaryTitle: "次要",
ReflogCommitsTitle: "Reflog",
GlobalTitle: "全局快捷鍵",
ConflictsResolved: "所有合併衝突都已解決。繼續嗎?",
Continue: "繼續",
Keybindings: "鍵盤快捷鍵",
RebasingTitle: "將 '{{.checkedOutBranch}}' 變基至 '{{.ref}}'",
SimpleRebase: "簡單變基",
InteractiveRebase: "互動變基",
InteractiveRebaseTooltip: "開始一個互動變基,以中斷開始,這樣你可以在繼續之前更新TODO提交",
ConfirmMerge: "確定要將 '{{.selectedBranch}}' 合併至 '{{.checkedOutBranch}}' 嗎?",
FwdNoUpstream: "無法快進無上游分支",
FwdNoLocalUpstream: "無法快進尚未在本地註冊的遠端分支",
FwdCommitsToPush: "無法快進帶有尚未推送的提交的分支",
ErrorOccurred: "發生錯誤!請在以下建立議題:",
NoRoom: "沒有足夠的空間",
YouAreHere: "你在這裡",
YouDied: "你已經死了!",
RewordNotSupported: "在互動變基期間改寫提交目前不支持",
ChangingThisActionIsNotAllowed: "不允許更改此類變基待辦事項",
CherryPickCopy: "複製提交 (揀選)",
CherryPickCopyRange: "複製提交範圍 (揀選)",
PasteCommits: "貼上提交 (揀選)",
SureCherryPick: "你確定要將複製的提交揀選到此分支嗎?",
CherryPick: "揀選 (Cherry-pick)",
Donate: "贊助",
AskQuestion: "問問題",
PrevLine: "選擇上一行",
NextLine: "選擇下一行",
PrevHunk: "選擇上一段",
NextHunk: "選擇下一段",
PrevConflict: "選擇上一個衝突",
NextConflict: "選擇下一個衝突",
SelectPrevHunk: "選擇上一段",
SelectNextHunk: "選擇下一段",
ScrollDown: "向下捲動",
ScrollUp: "向上捲動",
ScrollUpMainPanel: "向上捲動主面板",
ScrollDownMainPanel: "向下捲動主面板",
AmendCommitTitle: "修正提交",
AmendCommitPrompt: "你確定要使用預存的檔案修正此提交嗎?",
DeleteCommitTitle: "刪除提交",
DeleteCommitPrompt: "你確定要刪除此提交嗎?",
SquashingStatus: "壓縮中",
FixingStatus: "修復中",
DeletingStatus: "刪除中",
MovingStatus: "移動中",
RebasingStatus: "變基中",
MergingStatus: "合併中",
LowercaseRebasingStatus: "變基", // lowercase because it shows up in parentheses
LowercaseMergingStatus: "合併", // lowercase because it shows up in parentheses
AmendingStatus: "修正中",
CherryPickingStatus: "揀選中",
UndoingStatus: "復原中",
RedoingStatus: "取消復原中",
CheckingOutStatus: "檢出中",
CommittingStatus: "提交中",
CommitFiles: "提交檔案",
SubCommitsDynamicTitle: "提交 (共 %s項)",
CommitFilesDynamicTitle: "差異檔案 (共 %s項)",
RemoteBranchesDynamicTitle: "遠端分支 (共 %s項)",
ViewItemFiles: "檢視所選項目的檔案",
CommitFilesTitle: "提交檔案",
CheckoutCommitFile: "檢出檔案",
DiscardOldFileChange: "捨棄此提交對此檔案的更改",
DiscardFileChangesTitle: "捨棄檔案更改",
DiscardFileChangesPrompt: "你確定要捨棄此提交對此檔案的更改嗎?如果這個檔案是在此提交中創建的,它將被刪除",
DisabledForGPG: "此功能不適用於使用GPG的使用者",
CreateRepo: "未在git版本庫中. 建立一個新的git版本庫? (y/n): ",
BareRepo: "你嘗試在裸版本庫中開啟Lazygit,但Lazygit尚未支持裸版本庫。是否開啟最近的版本庫? (y/n) ",
InitialBranch: "分支名稱?(留空使用git的默認值):",
NoRecentRepositories: "必須在git版本庫中開啟lazygit。沒有有效的最近版本庫。退出。",
IncorrectNotARepository: "“notARepository”的值不正確。它應該是“prompt”、“create”、“skip”或“quit”中的一個。",
AutoStashTitle: "自動儲存?",
AutoStashPrompt: "你必須儲存和彈出你的更改才能帶它們到其他地方。是否自動執行此操作?(enter/esc)",
StashPrefix: "正在自動儲存更改:",
ViewDiscardOptions: "檢視“捨棄更改”的選項",
Cancel: "取消",
DiscardAllChanges: "捨棄所有更改",
DiscardUnstagedChanges: "捨棄未預存的更改",
DiscardAllChangesToAllFiles: "清空工作區",
DiscardAnyUnstagedChanges: "捨棄未預存的更改",
DiscardUntrackedFiles: "捨棄未追蹤的檔案",
DiscardStagedChanges: "捨棄已預存的更改",
HardReset: "硬重設",
ViewResetOptions: "檢視重設選項",
CreateFixupCommitDescription: "為此提交建立修復提交",
SquashAboveCommits: "壓縮上方所有的“fixup!”提交 (自動壓縮)",
SureSquashAboveCommits: "你確定要壓縮{{.commit}}上方所有的fixup!提交嗎?",
CreateFixupCommit: "建立修復提交",
SureCreateFixupCommit: "你確定要為提交{{.commit}}建立fixup!提交嗎?",
ExecuteCustomCommand: "執行自訂命令",
CustomCommand: "自訂命令:",
CommitChangesWithoutHook: "沒有預提交 hook 就提交更改",
SkipHookPrefixNotConfigured: "你尚未配置略過 hook 的提交訊息前綴,請在設定中設置 `git.skipHookPrefix = 'WIP'`",
ResetTo: `重設至`,
PressEnterToReturn: "按 Enter 返回到 lazygit",
ViewStashOptions: "檢視收藏選項",
StashAllChanges: "收藏所有變更",
StashStagedChanges: "收藏已預存變更",
StashAllChangesKeepIndex: "收藏所有變更並保留預存區",
StashUnstagedChanges: "收藏未預存變更",
StashIncludeUntrackedChanges: "收藏所有變更,包括未追蹤檔案",
StashOptions: "收藏選項",
NotARepository: "錯誤:必須在 git 版本庫中執行",
Jump: "跳轉至面板",
ScrollLeftRight: "左右捲動",
ScrollLeft: "向左捲動",
ScrollRight: "向右捲動",
DiscardPatch: "捨棄補丁",
DiscardPatchConfirm: "你只能從單一提交或收藏項目建立一個補丁。是否捨棄當前補丁?",
CantPatchWhileRebasingError: "在合併或變基狀態下,你不能建立或運行補丁命令",
ToggleAddToPatch: "切換檔案是否包含在補丁中",
ToggleAllInPatch: "切換所有檔案是否包含在補丁中",
UpdatingPatch: "正在更新補丁",
ViewPatchOptions: "檢視自訂補丁選項",
PatchOptionsTitle: "補丁選項",
NoPatchError: "尚未建立補丁。要開始建立補丁,請在提交檔案上使用空格或輸入以添加特定行",
EnterFile: "輸入檔案以將選定的行添加至補丁(或切換目錄折疊)",
ExitCustomPatchBuilder: `退出自訂補丁建立器`,
EnterUpstream: `輸入上游為 '<remote> <branchname>'`,
InvalidUpstream: "無效的上游。必須符合 '<remote> <branchname>' 的格式",
ReturnToRemotesList: `返回遠端列表`,
AddNewRemote: `新增遠端`,
NewRemoteName: `新遠端名稱:`,
NewRemoteUrl: `新遠端 URL:`,
EditRemoteName: `輸入更新 {{.remoteName}} 遠端名稱:`,
EditRemoteUrl: `輸入更新 {{.remoteName}} 遠端 URL:`,
RemoveRemote: `移除遠端`,
RemoveRemotePrompt: "你確定要移除遠端嗎?",
DeleteRemoteBranch: "刪除遠端分支",
DeleteRemoteBranchMessage: "你確定要刪除遠端分支嗎?",
SetAsUpstream: "將此分支設為當前分支之上游",
SetUpstream: "設定所選分支之上游",
UnsetUpstream: "取消設定選定分支之上游",
SetUpstreamTitle: "設定上游分支",
SetUpstreamMessage: "你確定要將 '{{. selected}}' 設為 '{{.checkedOut}}' 的上游分支嗎?",
EditRemote: "編輯遠端",
TagCommit: "打標籤到提交",
TagMenuTitle: "建立標籤",
TagNameTitle: "標籤名稱:",
TagMessageTitle: "標籤訊息:",
AnnotatedTag: "附註標籤",
LightweightTag: "輕量標籤",
DeleteTag: "刪除標籤",
DeleteTagTitle: "刪除標籤",
DeleteTagPrompt: "你確定要刪除 '{{.tagName}}' 標籤嗎?",
PushTagTitle: "推送標籤 '{{.tagName}}' 至遠端:",
PushTag: "推送標籤",
CreateTag: "建立標籤",
CreateTagTitle: "標籤名稱:",
FetchRemote: "擷取遠端",
FetchingRemoteStatus: "正在擷取遠端",
CheckoutCommit: "檢出提交",
SureCheckoutThisCommit: "你確定要檢出這個提交嗎?",
GitFlowOptions: "顯示 git-flow 選項",
NotAGitFlowBranch: "這似乎不是一個 git flow 分支",
NewGitFlowBranchPrompt: "{{.branchType}} 名稱:",
IgnoreTracked: "忽略已追蹤檔案",
IgnoreTrackedPrompt: "你確定要忽略一個已追蹤的檔案嗎?",
ExcludeTracked: "排除已追蹤檔案",
ExcludeTrackedPrompt: "你確定要排除一個已追蹤的檔案嗎?",
ViewResetToUpstreamOptions: "檢視上游重設選項",
NextScreenMode: "下一個螢幕模式(常規/半螢幕/全螢幕)",
PrevScreenMode: "上一個螢幕模式",
StartSearch: "開始搜尋",
Panel: "面板",
KeybindingsLegend: "說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B",
RenameBranch: "重新命名分支",
SetUnsetUpstream: "設定/取消設定上游",
NewBranchNamePrompt: "為分支輸入新名稱",
RenameBranchWarning: "此分支正在追蹤遠端分支。此操作僅會重新命名本地分支名稱,而不是遠端分支的名稱。是否繼續?",
OpenMenu: "開啟選單",
ResetCherryPick: "重設選定的揀選 (複製) 提交",
NextTab: "下一個索引標籤",
PrevTab: "上一個索引標籤",
CantUndoWhileRebasing: "在變基時無法復原",
CantRedoWhileRebasing: "在變基時無法取消復原",
MustStashWarning: "將補丁提取到索引中需要收藏並取消收藏你的變更。如果出現問題,你可以從收藏中訪問你的檔案。是否繼續?",
MustStashTitle: "必須收藏",
ConfirmationTitle: "確認面板",
PrevPage: "上一頁",
NextPage: "下一頁",
GotoTop: "捲動到頂部",
GotoBottom: "捲動到底部",
FilteringBy: "篩選方式",
ResetInParentheses: "(已重設)",
OpenFilteringMenu: "檢視篩選路徑選項",
FilterBy: "篩選路徑",
ExitFilterMode: "停止按路徑篩選",
FilterPathOption: "輸入要依路徑篩選的路徑",
EnterFileName: "輸入路徑:",
FilteringMenuTitle: "篩選",
MustExitFilterModeTitle: "命令不可用",
MustExitFilterModePrompt: "在按路徑篩選的模式下,該命令不可用。是否退出按路徑篩選的模式?",
Diff: "差異",
EnterRefToDiff: "輸入要比較的 Ref",
EnteRefName: "輸入 Ref:",
ExitDiffMode: "退出差異模式",
DiffingMenuTitle: "差異比較",
SwapDiff: "反轉差異方向",
OpenDiffingMenu: "開啟差異比較選單",
// the actual view is the extras view which I intend to give more tabs in future but for now we'll only mention the command log part
OpenExtrasMenu: "開啟命令記錄選單",
ShowingGitDiff: "顯示輸出:",
CommitDiff: "提交差異",
CopyCommitShaToClipboard: "複製提交 SHA 到剪貼簿",
CommitSha: "提交 SHA",
CommitURL: "提交 URL",
CopyCommitMessageToClipboard: "複製提交訊息到剪貼簿",
CommitMessage: "提交訊息",
CommitAuthor: "提交者",
CopyCommitAttributeToClipboard: "複製提交屬性",
CopyBranchNameToClipboard: "複製分支名稱到剪貼簿",
CopyFileNameToClipboard: "複製檔案名稱到剪貼簿",
CopyCommitFileNameToClipboard: "複製提交的檔案名稱到剪貼簿",
CopySelectedTexToClipboard: "複製所選文本至剪貼簿",
CommitPrefixPatternError: "commitPrefix 模式錯誤",
NoFilesStagedTitle: "沒有檔案預存",
NoFilesStagedPrompt: "你沒有預存任何檔案。提交所有檔案嗎?",
BranchNotFoundTitle: "找不到分支",
BranchNotFoundPrompt: "找不到分支。是否創建一個名為",
BranchUnknown: "分支未知",
UnstageLinesTitle: "取消預存行",
UnstageLinesPrompt: "你確定要刪除所選行嗎(git reset)?此操作是不可逆的。\n要禁用此對話框,請將“gui.skipUnstageLineWarning”設置為true。",
CreateNewBranchFromCommit: "從提交建立新分支",
BuildingPatch: "正在建立補丁",
ViewCommits: "檢視提交",
MinGitVersionError: "Git 版本至少為 2.20(即從 2018 年開始)。請升級你的 Git 版本。或者在 https://github.com/jesseduffield/lazygit/issues 上提出問題,讓 lazygit 更加向後兼容。",
RunningCustomCommandStatus: "正在執行自訂命令",
SubmoduleStashAndReset: "收藏未提交的子模組變更並更新",
AndResetSubmodules: "以及重設子模組",
EnterSubmodule: "進入子模組",
CopySubmoduleNameToClipboard: "複製子模組名稱到剪貼簿",
RemoveSubmodule: "移除子模組",
RemoveSubmodulePrompt: "是否確定要刪除子模組 '%s' 以及它相應的目錄?此操作是不可逆的。",
ResettingSubmoduleStatus: "重設子模型中",
NewSubmoduleName: "新子模組名稱:",
NewSubmoduleUrl: "新子模組 URL:",
NewSubmodulePath: "新子模組路徑:",
AddSubmodule: "新增子模組",
AddingSubmoduleStatus: "正在新增子模組",
UpdateSubmoduleUrl: "更新子模組 '%s' 的 URL",
UpdatingSubmoduleUrlStatus: "正在更新 URL",
EditSubmoduleUrl: "更新子模組 URL",
InitializingSubmoduleStatus: "正在初始化子模組",
InitSubmodule: "初始化子模組",
SubmoduleUpdate: "更新子模組",
UpdatingSubmoduleStatus: "正在更新子模組",
BulkInitSubmodules: "批量初始化子模組",
BulkUpdateSubmodules: "批量更新子模組",
BulkDeinitSubmodules: "批量解除子模組初始化",
ViewBulkSubmoduleOptions: "查看批量子模組選項",
BulkSubmoduleOptions: "批量子模組選項",
RunningCommand: "正在執行命令",
SubCommitsTitle: "子提交",
SubmodulesTitle: "子模組",
NavigationTitle: "列表面板導航",
SuggestionsCheatsheetTitle: "提示",
SuggestionsTitle: "提示(按 %s 進入焦點)",
ExtrasTitle: "命令記錄",
PushingTagStatus: "正在推送標籤",
PullRequestURLCopiedToClipboard: "複製拉取請求 URL 至剪貼簿",
CommitDiffCopiedToClipboard: "已複製提交差異至剪貼簿",
CommitSHACopiedToClipboard: "已複製提交 SHA 至剪貼簿",
CommitURLCopiedToClipboard: "已複製提交 URL 至剪貼簿",
CommitMessageCopiedToClipboard: "已複製提交訊息至剪貼簿",
CommitAuthorCopiedToClipboard: "已複製提交者至剪貼簿",
PatchCopiedToClipboard: "已複製補丁至剪貼簿",
CopiedToClipboard: "已複製至剪貼簿",
ErrCannotEditDirectory: "無法編輯目錄:你只能編輯單獨的檔案",
ErrStageDirWithInlineMergeConflicts: "不能預存/取消預存包含具備內嵌合併衝突的檔案的目錄。請先解決合併衝突",
ErrRepositoryMovedOrDeleted: "找不到版本庫。它可能已被移動或刪除 ¯\\_(ツ)_/¯",
CommandLog: "命令記錄",
ToggleShowCommandLog: "切換顯示/隱藏命令記錄",
FocusCommandLog: "聚焦命令記錄",
CommandLogHeader: "你可以按 '%s' 隱藏/聚焦此面板\n",
RandomTip: "隨機提示",
SelectParentCommitForMerge: "選擇合併的父提交",
ToggleWhitespaceInDiffView: "切換是否在差異檢視中顯示空格變更",
IgnoreWhitespaceDiffViewSubTitle: "(忽略空格)",
IgnoreWhitespaceNotSupportedHere: "在此檢視中不支持忽略空格",
IncreaseContextInDiffView: "增加差異檢視中顯示變更周圍上下文的大小",
DecreaseContextInDiffView: "減小差異檢視中顯示變更周圍上下文的大小",
CreatePullRequestOptions: "建立拉取請求選項",
DefaultBranch: "預設分支",
SelectBranch: "選擇分支",
SelectConfigFile: "選擇設定檔",
NoConfigFileFoundErr: "找不到設定檔",
LoadingFileSuggestions: "正在加載檔案建議",
LoadingCommits: "正在加載提交",
MustSpecifyOriginError: "如果指定分支,必須指定遠端",
GitOutput: "Git 輸出:",
GitCommandFailed: "Git 命令失敗。請查看命令記錄以獲取詳細資訊(按 %s 開啟)",
AbortTitle: "中止%s",
AbortPrompt: "是否確定要中止當前的%s?",
OpenLogMenu: "開啟記錄選單",
LogMenuTitle: "提交記錄選項",
ToggleShowGitGraphAll: "切換顯示整個 Git 圖表(將 `--all` 標誌傳遞給 `git log`)",
ShowGitGraph: "顯示 Git 圖表",
SortCommits: "提交排序順序",
CantChangeContextSizeError: "在製作補丁期間無法更改上下文大小,因為當發布功能時我們太懒了以至於沒有支援它。如果你真的需要它,請告訴我們!",
OpenCommitInBrowser: "在瀏覽器中開啟提交",
ViewBisectOptions: "查看二分選項",
ConfirmRevertCommit: "確定要還原 {{.selectedCommit}} 嗎?",
RewordInEditorTitle: "在編輯器中改寫",
RewordInEditorPrompt: "你確定要在編輯器中改寫此提交嗎?",
HardResetAutostashPrompt: "你確定要硬重設為 '%s' 嗎?如果必要會進行自動存儲。",
CheckoutPrompt: "你確定要檢出 '%s' 嗎?",
UpstreamGone: "(上游已經不存在)",
NukeDescription: "如果你想讓所有工作樹上的變更消失,這就是要做的方式。如果有未提交的子模組變更,它將把這些變更藏在子模組中。",
DiscardStagedChangesDescription: "這將創建一個新的存儲條目,其中只包含預存檔案,然後如果存儲條目不需要,將其刪除,因此工作樹僅保留未預存的變更。",
EmptyOutput: "<空輸出>",
Patch: "補丁",
CustomPatch: "自定義補丁",
CommitsCopied: "提交已複製", // lowercase because it's used in a sentence
CommitCopied: "提交已複製", // lowercase because it's used in a sentence
ResetPatch: "重設補丁",
ApplyPatch: "套用補丁",
ApplyPatchInReverse: "反向套用補丁",
RemovePatchFromOriginalCommit: "從原始提交中刪除補丁(%s)",
MovePatchOutIntoIndex: "將補丁移到預存區",
MovePatchIntoNewCommit: "將補丁移到新的提交",
MovePatchToSelectedCommit: "將補丁移到選定的提交(%s)",
CopyPatchToClipboard: "將補丁複製到剪貼簿",
NoMatchesFor: "沒有找到符合 '%s' %s 的結果",
ExitSearchMode: "%s:退出搜尋模式",
MatchesFor: "符合 '%s' 的結果(%d/%d)%s", // lowercase because it's after other text
SearchKeybindings: "%s:下一個結果,%s:上一個結果,%s:退出搜尋模式",
SearchPrefix: "搜尋: ",
Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "檢出提交",
CheckoutTag: "檢出標籤",
CheckoutBranch: "檢出分支",
ForceCheckoutBranch: "強制檢出分支",
DeleteBranch: "刪除分支",
Merge: "合併",
RebaseBranch: "變基分支",
RenameBranch: "重新命名分支",
SetUnsetUpstream: "設置/取消上游",
CreateBranch: "建立分支",
CherryPick: "(Cherry-pick)粘貼提交",
CheckoutFile: "檢出檔案",
DiscardOldFileChange: "放棄舊檔案更改",
SquashCommitDown: "下列次方執行 Squash",
FixupCommit: "修復提交",
RewordCommit: "改寫提交",
DropCommit: "捨棄提交",
EditCommit: "編輯提交",
AmendCommit: "修正提交",
ResetCommitAuthor: "重設提交作者",
SetCommitAuthor: "設置提交作者",
RevertCommit: "還原提交",
CreateFixupCommit: "建立修正提交",
SquashAllAboveFixupCommits: "Squash 所有上面的修正提交",
CreateLightweightTag: "建立輕量標籤",
CreateAnnotatedTag: "建立附註標籤",
CopyCommitMessageToClipboard: "將提交訊息複製到剪貼簿",
CopyCommitDiffToClipboard: "將提交差異複製到剪貼簿",
CopyCommitSHAToClipboard: "將提交 SHA 複製到剪貼簿",
CopyCommitURLToClipboard: "將提交 URL 複製到剪貼簿",
CopyCommitAuthorToClipboard: "將提交作者複製到剪貼簿",
CopyCommitAttributeToClipboard: "複製到剪貼簿",
CopyPatchToClipboard: "將補丁複製到剪貼簿",
MoveCommitUp: "上移提交",
MoveCommitDown: "下移提交",
CustomCommand: "自定義命令",
DiscardAllChangesInDirectory: "捨棄目錄中的所有更改",
DiscardUnstagedChangesInDirectory: "捨棄目錄中未預存的更改",
DiscardAllChangesInFile: "捨棄檔案中的所有更改",
DiscardAllUnstagedChangesInFile: "捨棄檔案中未預存的所有更改",
StageFile: "預存檔案",
StageResolvedFiles: "預存已解決合併衝突的檔案",
UnstageFile: "取消預存檔案",
UnstageAllFiles: "取消預存所有檔案",
StageAllFiles: "預存所有檔案",
IgnoreExcludeFile: "忽略或排除檔案",
IgnoreFileErr: "無法忽略 .gitignore 檔案",
ExcludeFile: "排除檔案",
ExcludeFileErr: "無法排除 .git/info/exclude 檔案",
ExcludeGitIgnoreErr: "無法排除 .gitignore 檔案",
Commit: "提交",
EditFile: "編輯檔案",
Push: "推送",
Pull: "拉取",
OpenFile: "開啟檔案",
StashAllChanges: "收藏所有更改",
StashAllChangesKeepIndex: "收藏所有更改並保留索引",
StashStagedChanges: "收藏已預存的更改",
StashUnstagedChanges: "收藏未預存的更改",
StashIncludeUntrackedChanges: "收藏所有更改,包括未追蹤的檔案",
GitFlowFinish: "git flow 完成",
GitFlowStart: "git flow 開始",
CopyToClipboard: "複製到剪貼簿",
CopySelectedTextToClipboard: "複製所選文本到剪貼簿",
RemovePatchFromCommit: "從提交中刪除補丁",
MovePatchToSelectedCommit: "將補丁移動到所選提交",
MovePatchIntoIndex: "將補丁移動到索引中",
MovePatchIntoNewCommit: "將補丁移動到新提交中",
DeleteRemoteBranch: "刪除遠端分支",
SetBranchUpstream: "設置分支上游",
AddRemote: "添加遠端",
RemoveRemote: "移除遠端",
UpdateRemote: "更新遠端",
ApplyPatch: "套用補丁",
Stash: "收藏 (Stash)",
RenameStash: "重命名暫存",
RemoveSubmodule: "移除子模塊",
ResetSubmodule: "重設子模塊",
AddSubmodule: "添加子模塊",
UpdateSubmoduleUrl: "更新子模塊 URL",
InitialiseSubmodule: "初始化子模塊",
BulkInitialiseSubmodules: "批量初始化子模塊",
BulkUpdateSubmodules: "批量更新子模塊",
BulkDeinitialiseSubmodules: "批量取消初始化子模塊",
UpdateSubmodule: "更新子模塊",
DeleteTag: "刪除標籤",
PushTag: "推送標籤",
NukeWorkingTree: "清空工作樹",
DiscardUnstagedFileChanges: "放棄未預存的檔案更改",
RemoveUntrackedFiles: "移除未追蹤的檔案",
RemoveStagedFiles: "移除已預存的檔案",
SoftReset: "軟重設",
MixedReset: "混合重設",
HardReset: "硬重設",
FastForwardBranch: "快進分支",
Undo: "撤銷",
Redo: "重做",
CopyPullRequestURL: "複製拉取請求的 URL",
OpenMergeTool: "開啟合併工具",
OpenCommitInBrowser: "在瀏覽器中開啟提交",
OpenPullRequest: "在瀏覽器中開啟拉取請求",
StartBisect: "開始二分查找",
ResetBisect: "重設二分查找",
BisectSkip: "二分查找跳過",
BisectMark: "二分查找標記",
},
Bisect: Bisect{
Mark: "將 %s 標記為 %s",
MarkStart: "將 %s 標記為 %s(開始二分查找)",
Skip: "跳過 %s",
ResetTitle: "重設 'git bisect'",
ResetPrompt: "你確定要重設 'git bisect' 嗎?",
ResetOption: "重設二分查找",
BisectMenuTitle: "二分查找",
CompleteTitle: "二分查找完成",
CompletePrompt: "二分查找完成!以下提交引入了更改:\n\n%s\n\n你現在要重設 'git bisect' 嗎?",
CompletePromptIndeterminate: "二分查找完成!有一些提交被跳過,因此以下任何提交都可能引入了更改:\n\n%s\n\n你現在要重設 'git bisect' 嗎?",
Bisecting: "二分查找中",
},
}
}
+5 -10
View File
@@ -4,11 +4,9 @@ import (
"log"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests"
"github.com/samber/lo"
@@ -30,6 +28,11 @@ func RunCLI(testNames []string, slow bool, sandbox bool) {
keyPressDelay = SLOW_KEY_PRESS_DELAY
}
// replace backslashes with forward slashes for windows compatibility
testNames = lo.Map(testNames, func(name string, _ int) string {
return strings.ReplaceAll(name, "\\", "/")
})
err := components.RunTests(
getTestsToRun(testNames),
log.Printf,
@@ -58,14 +61,6 @@ func getTestsToRun(testNames []string) []*components.IntegrationTest {
return allIntegrationTests
}
testNames = slices.Map(testNames, func(name string) string {
// allowing full test paths to be passed for convenience
return strings.TrimSuffix(
regexp.MustCompile(`.*pkg/integration/tests/`).ReplaceAllString(name, ""),
".go",
)
})
if lo.SomeBy(testNames, func(name string) bool {
return strings.HasSuffix(name, "/shared")
}) {
+25 -12
View File
@@ -1,6 +1,3 @@
//go:build !windows
// +build !windows
package clients
// This file allows you to use `go test` to run integration tests.
@@ -9,14 +6,15 @@ package clients
import (
"bytes"
"errors"
"io"
"io/ioutil"
"fmt"
"os"
"os/exec"
"testing"
"time"
"github.com/creack/pty"
"github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/result"
"github.com/jesseduffield/lazygit/pkg/integration/tests"
"github.com/stretchr/testify/assert"
)
@@ -30,8 +28,10 @@ func TestIntegration(t *testing.T) {
parallelIndex := tryConvert(os.Getenv("PARALLEL_INDEX"), 0)
testNumber := 0
tests := tests.GetTests()
err := components.RunTests(
tests.GetTests(),
tests,
t.Logf,
runCmdHeadless,
func(test *components.IntegrationTest, f func() error) {
@@ -40,6 +40,10 @@ func TestIntegration(t *testing.T) {
return
}
// if test.Name() != "commit/commit" {
// return
// }
t.Run(test.Name(), func(t *testing.T) {
t.Parallel()
err := f()
@@ -63,6 +67,9 @@ func runCmdHeadless(cmd *exec.Cmd) error {
"TERM=xterm",
)
resultPath := result.GetResultPath()
result.SetResultPathEnvVar(cmd, resultPath)
// not writing stderr to the pty because we want to capture a panic if
// there is one. But some commands will not be in tty mode if stderr is
// not a terminal. We'll need to keep an eye out for that.
@@ -72,17 +79,23 @@ func runCmdHeadless(cmd *exec.Cmd) error {
// these rows and columns are ignored because internally we use tcell's
// simulation screen. However we still need the pty for the sake of
// running other commands in a pty.
f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 300, Cols: 300})
_, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 300, Cols: 300})
if err != nil {
return err
}
_, _ = io.Copy(ioutil.Discard, f)
_ = cmd.Wait()
if cmd.Wait() != nil {
// return an error with the stderr output
return errors.New(stderr.String())
time.Sleep(time.Second)
result, err := result.ReadResult(resultPath)
if err != nil {
return fmt.Errorf("Error reading integration test result: %w", err)
}
if !result.Success {
return errors.New(result.Message)
}
return f.Close()
return nil
}
@@ -18,7 +18,7 @@ func retryWaitTimes() []int {
// give it more leeway compared to when we're running things locally.
return []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200, 500, 1000, 2000, 4000}
} else {
return []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200}
return []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200, 500, 1000, 2000, 4000}
}
}
+3 -3
View File
@@ -19,9 +19,9 @@ func (self *Common) ContinueRebase() {
}
func (self *Common) AcknowledgeConflicts() {
self.t.ExpectPopup().Menu().
Title(Equals("Conflicts!")).
Select(Contains("View conflicts")).
self.t.ExpectPopup().Confirmation().
Title(Equals("Auto-merge failed")).
Content(Contains("Conflicts!")).
Confirm()
}
+8 -2
View File
@@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
@@ -122,7 +123,7 @@ func prepareTestDir(
err := os.Mkdir(paths.ActualRepo(), 0o777)
if err != nil {
return err
panic(err)
}
return createFixture(test, paths, rootDir)
@@ -198,7 +199,12 @@ func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandb
}
func tempLazygitPath() string {
return filepath.Join("/tmp", "lazygit", "test_lazygit")
filename := "test_lazygit"
if runtime.GOOS == "windows" {
filename += ".exe"
}
return filepath.Join(os.TempDir(), "lazygit", filename)
}
func findOrCreateDir(path string) {
+9 -3
View File
@@ -2,6 +2,8 @@ package components
import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
@@ -170,10 +172,14 @@ func testNameFromCurrentFilePath() string {
return TestNameFromFilePath(path)
}
// takes either a path or a test name and returns a test name
// so commit/commit -> commit/commit
// and /my/path/to/lazygit/pkg/integration/tests/commit/commit -> commit/commit
func TestNameFromFilePath(path string) string {
name := strings.Split(path, "integration/tests/")[1]
return name[:len(name)-len(".go")]
return strings.TrimSuffix(
regexp.MustCompile(`.*pkg/integration/tests/`).ReplaceAllString(filepath.ToSlash(path), ""),
".go",
)
}
// this is the delay in milliseconds between keypresses
-28
View File
@@ -133,34 +133,6 @@ func (self *ViewDriver) ContainsLines(matchers ...*Matcher) *ViewDriver {
return self
}
func (self *ViewDriver) ContainsColoredText(fgColorStr string, text string) *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
view := self.getView()
ok := self.getView().ContainsColoredText(fgColorStr, text)
if !ok {
return false, fmt.Sprintf("expected view '%s' to contain colored text '%s' but it didn't", view.Name(), text)
}
return true, ""
})
return self
}
func (self *ViewDriver) DoesNotContainColoredText(fgColorStr string, text string) *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
view := self.getView()
ok := !self.getView().ContainsColoredText(fgColorStr, text)
if !ok {
return false, fmt.Sprintf("expected view '%s' to NOT contain colored text '%s' but it didn't", view.Name(), text)
}
return true, ""
})
return self
}
// asserts on the lines that are selected in the view. Don't use the `IsSelected` matcher with this because it's redundant.
func (self *ViewDriver) SelectedLines(matchers ...*Matcher) *ViewDriver {
self.validateMatchersPassed(matchers)
+103
View File
@@ -0,0 +1,103 @@
package result
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
)
// On windows the pty package is failing to obtain stderr text when a test fails,
// so this is our workaround: when a test fails, it writes the result to a file,
// and then the test runner reads the file and checks the result
const PathEnvVar = "LAZYGIT_INTEGRATION_TEST_RESULT_PATH"
type IntegrationTestResult struct {
Success bool
Message string
}
func LogFailure(message string) error {
return writeResult(failure(message))
}
func LogSuccess() error {
return writeResult(success())
}
func failure(message string) IntegrationTestResult {
return IntegrationTestResult{Success: false, Message: message}
}
func success() IntegrationTestResult {
return IntegrationTestResult{Success: true}
}
func writeResult(result IntegrationTestResult) error {
resultPath := os.Getenv(PathEnvVar)
if resultPath == "" {
// path env var not set so we'll assume we don't need to write the result to a file
return nil
}
file, err := os.Create(resultPath)
if err != nil {
return fmt.Errorf("Error creating file: %w", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
err = encoder.Encode(result)
if err != nil {
return fmt.Errorf("Error encoding JSON: %w", err)
}
return nil
}
// Reads the result file stored by the lazygit test, and deletes the file
func ReadResult(resultPath string) (IntegrationTestResult, error) {
file, err := os.Open(resultPath)
if err != nil {
return IntegrationTestResult{}, fmt.Errorf("Error reading file: %w", err)
}
decoder := json.NewDecoder(file)
var result IntegrationTestResult
err = decoder.Decode(&result)
if err != nil {
file.Close()
return IntegrationTestResult{}, fmt.Errorf("Error decoding JSON: %w", err)
}
file.Close()
// _ = os.Remove(resultPath)
return result, nil
}
func SetResultPathEnvVar(cmd *exec.Cmd, resultPath string) {
cmd.Env = append(
cmd.Env,
fmt.Sprintf("%s=%s", PathEnvVar, resultPath),
)
}
func GetResultPath() string {
return filepath.Join(os.TempDir(), fmt.Sprintf("lazygit_result_%s.json", generateRandomString(10)))
}
func generateRandomString(length int) string {
buffer := make([]byte, length)
_, err := rand.Read(buffer)
if err != nil {
panic(fmt.Sprintf("Could not generate random string: %s", err))
}
randomString := base64.URLEncoding.EncodeToString(buffer)
return randomString[:length]
}
@@ -1,49 +0,0 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests/shared"
)
var RebaseAbortOnConflict = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Rebase onto another branch, abort when there are conflicts.",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shared.MergeConflictsSetup(shell)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().TopLines(
Contains("first change"),
Contains("original"),
)
t.Views().Branches().
Focus().
Lines(
Contains("first-change-branch"),
Contains("second-change-branch"),
Contains("original-branch"),
).
SelectNextItem().
Press(keys.Branches.RebaseBranch)
t.ExpectPopup().Menu().
Title(Equals("Rebase 'first-change-branch' onto 'second-change-branch'")).
Select(Contains("Simple rebase")).
Confirm()
t.ExpectPopup().Menu().
Title(Equals("Conflicts!")).
Select(Contains("Abort the rebase")).
Confirm()
t.Views().Branches().
IsFocused()
t.Views().Files().
IsEmpty()
},
})
@@ -1,51 +0,0 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests/shared"
)
var RebaseCancelOnConflict = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Rebase onto another branch, cancel when there are conflicts.",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shared.MergeConflictsSetup(shell)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().TopLines(
Contains("first change"),
Contains("original"),
)
t.Views().Branches().
Focus().
Lines(
Contains("first-change-branch"),
Contains("second-change-branch"),
Contains("original-branch"),
).
SelectNextItem().
Press(keys.Branches.RebaseBranch)
t.ExpectPopup().Menu().
Title(Equals("Rebase 'first-change-branch' onto 'second-change-branch'")).
Select(Contains("Simple rebase")).
Confirm()
t.ExpectPopup().Menu().
Title(Equals("Conflicts!")).
Select(Contains("Abort the rebase")).
Cancel()
t.Views().Branches().
IsFocused()
t.Views().Files().
Lines(
Contains("UU file"),
)
},
})
+2 -2
View File
@@ -27,7 +27,7 @@ var Commit = NewIntegrationTest(NewIntegrationTestArgs{
PressPrimaryAction(). // stage file
Lines(
Contains("A myfile").IsSelected(),
Contains("?? myfile2"),
Contains("?? myfile23"),
).
SelectNextItem().
PressPrimaryAction(). // stage other file
@@ -55,7 +55,7 @@ var Commit = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused().
Lines(
Contains("A myfile"),
Contains("A myfile2"),
Contains("A myfile3"),
)
},
})
-37
View File
@@ -1,37 +0,0 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var Highlight = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that the commit view highlights the correct lines",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.Log.ShowGraph = "always"
config.GetUserConfig().Gui.AuthorColors = map[string]string{
"CI": "red",
}
},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("one")
shell.EmptyCommit("two")
shell.EmptyCommit("three")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
highlightedColor := "#ffffff"
t.Views().Commits().
DoesNotContainColoredText(highlightedColor, "◯").
Focus().
ContainsColoredText(highlightedColor, "◯")
t.Views().Files().
Focus()
t.Views().Commits().
DoesNotContainColoredText(highlightedColor, "◯")
},
})
@@ -1,66 +0,0 @@
package custom_commands
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SuggestionsCommand = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Using a custom command that uses a suggestions command in a prompt step",
ExtraCmdArgs: []string{},
Skip: false,
SetupRepo: func(shell *Shell) {
shell.NewBranch("branch-one")
shell.EmptyCommit("blah")
shell.NewBranch("branch-two")
shell.EmptyCommit("blah")
shell.NewBranch("branch-three")
shell.EmptyCommit("blah")
shell.NewBranch("branch-four")
shell.EmptyCommit("blah")
},
SetupConfig: func(cfg *config.AppConfig) {
cfg.UserConfig.CustomCommands = []config.CustomCommand{
{
Key: "a",
Context: "localBranches",
Command: `git checkout {{.Form.Branch}}`,
Prompts: []config.CustomCommandPrompt{
{
Key: "Branch",
Type: "input",
Title: "Enter a branch name",
Suggestions: config.CustomCommandSuggestions{
Command: "git branch --format='%(refname:short)'",
},
},
},
},
}
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("branch-four").IsSelected(),
Contains("branch-three"),
Contains("branch-two"),
Contains("branch-one"),
).
Press("a")
t.ExpectPopup().Prompt().
Title(Equals("Enter a branch name")).
Type("three").
SuggestionLines(Contains("branch-three")).
ConfirmFirstSuggestion()
t.Views().Branches().
Lines(
Contains("branch-three").IsSelected(),
Contains("branch-four"),
Contains("branch-two"),
Contains("branch-one"),
)
},
})
@@ -1,66 +0,0 @@
package custom_commands
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SuggestionsPreset = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Using a custom command that uses a suggestions preset in a prompt step",
ExtraCmdArgs: []string{},
Skip: false,
SetupRepo: func(shell *Shell) {
shell.NewBranch("branch-one")
shell.EmptyCommit("blah")
shell.NewBranch("branch-two")
shell.EmptyCommit("blah")
shell.NewBranch("branch-three")
shell.EmptyCommit("blah")
shell.NewBranch("branch-four")
shell.EmptyCommit("blah")
},
SetupConfig: func(cfg *config.AppConfig) {
cfg.UserConfig.CustomCommands = []config.CustomCommand{
{
Key: "a",
Context: "localBranches",
Command: `git checkout {{.Form.Branch}}`,
Prompts: []config.CustomCommandPrompt{
{
Key: "Branch",
Type: "input",
Title: "Enter a branch name",
Suggestions: config.CustomCommandSuggestions{
Preset: "branches",
},
},
},
},
}
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("branch-four").IsSelected(),
Contains("branch-three"),
Contains("branch-two"),
Contains("branch-one"),
).
Press("a")
t.ExpectPopup().Prompt().
Title(Equals("Enter a branch name")).
Type("three").
SuggestionLines(Contains("branch-three")).
ConfirmFirstSuggestion()
t.Views().Branches().
Lines(
Contains("branch-three").IsSelected(),
Contains("branch-four"),
Contains("branch-two"),
Contains("branch-one"),
)
},
})
-5
View File
@@ -36,9 +36,7 @@ var tests = []*components.IntegrationTest{
branch.DetachedHead,
branch.OpenWithCliArg,
branch.Rebase,
branch.RebaseAbortOnConflict,
branch.RebaseAndDrop,
branch.RebaseCancelOnConflict,
branch.RebaseDoesNotAutosquash,
branch.Reset,
branch.ResetUpstream,
@@ -53,7 +51,6 @@ var tests = []*components.IntegrationTest{
commit.CommitWithPrefix,
commit.CreateTag,
commit.DiscardOldFileChange,
commit.Highlight,
commit.History,
commit.HistoryComplex,
commit.NewBranch,
@@ -80,8 +77,6 @@ var tests = []*components.IntegrationTest{
custom_commands.MenuFromCommandsOutput,
custom_commands.MultiplePrompts,
custom_commands.OmitFromHistory,
custom_commands.SuggestionsCommand,
custom_commands.SuggestionsPreset,
diff.Diff,
diff.DiffAndApplyPatch,
diff.DiffCommits,
+1 -1
View File
@@ -27,7 +27,7 @@ func GetTests() []*components.IntegrationTest {
missingTestNames := []string{}
if err := filepath.Walk(filepath.Join(utils.GetLazyRootDirectory(), "pkg/integration/tests"), func(path string, info os.FileInfo, err error) error {
if err := filepath.Walk(filepath.Join(utils.GetLazyRootDirectory(), "pkg", "integration", "tests"), func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && strings.HasSuffix(path, ".go") {
// ignoring non-test files
if filepath.Base(path) == "tests.go" || filepath.Base(path) == "test_list.go" || filepath.Base(path) == "test_list_generator.go" {
+3
View File
@@ -16,6 +16,9 @@ var DoublePopup = NewIntegrationTest(NewIntegrationTestArgs{
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("master"),
).
// arbitrarily bringing up a popup
PressPrimaryAction()
+1
View File
@@ -13,6 +13,7 @@ import (
type IntegrationTest interface {
Run(GuiDriver)
SetupConfig(config *config.AppConfig)
Name() string
}
// this is the interface through which our integration tests interact with the lazygit gui
+2 -1
View File
@@ -21,7 +21,7 @@ import (
var Global *logrus.Entry
func init() {
logPath := os.Getenv("LAZYGIT_LOG_PATH")
logPath := `C:\Users\New\AppData\Roaming\jesseduffield\lazygit\development.log`
if logPath != "" {
Global = NewDevelopmentLogger(logPath)
}
@@ -35,6 +35,7 @@ func NewProductionLogger() *logrus.Entry {
}
func NewDevelopmentLogger(logPath string) *logrus.Entry {
logPath = `C:\Users\New\AppData\Roaming\jesseduffield\lazygit\development.log`
logger := logrus.New()
logger.SetLevel(getLogLevel())
+4 -4
View File
@@ -7,11 +7,8 @@ import (
)
var (
// DefaultTextColor is the default text color
DefaultTextColor = style.FgDefault
// GocuiDefaultTextColor does the same as DefaultTextColor but this one only colors gocui default text colors
GocuiDefaultTextColor = gocui.ColorDefault
GocuiDefaultTextColor gocui.Attribute
// ActiveBorderColor is the border color of the active frame
ActiveBorderColor gocui.Attribute
@@ -24,6 +21,9 @@ var (
OptionsColor gocui.Attribute
// DefaultTextColor is the default text color
DefaultTextColor = style.FgWhite
// SelectedLineBgColor is the background color for the selected line
SelectedLineBgColor = style.New()
+9 -46
View File
@@ -7,54 +7,17 @@ import (
func UnixToTimeAgo(timestamp int64) string {
now := time.Now().Unix()
return formatSecondsAgo(now - timestamp)
}
const (
SECONDS_IN_SECOND = 1
SECONDS_IN_MINUTE = 60
SECONDS_IN_HOUR = 3600
SECONDS_IN_DAY = 86400
SECONDS_IN_WEEK = 604800
SECONDS_IN_YEAR = 31536000
SECONDS_IN_MONTH = SECONDS_IN_YEAR / 12
)
type period struct {
label string
secondsInPeriod int64
}
var periods = []period{
{"s", SECONDS_IN_SECOND},
{"m", SECONDS_IN_MINUTE},
{"h", SECONDS_IN_HOUR},
{"d", SECONDS_IN_DAY},
{"w", SECONDS_IN_WEEK},
// we're using 'm' for both minutes and months which is ambiguous but
// disambiguating with another character feels like overkill.
{"m", SECONDS_IN_MONTH},
{"y", SECONDS_IN_YEAR},
}
func formatSecondsAgo(secondsAgo int64) string {
for i, period := range periods {
if i == 0 {
continue
}
if secondsAgo < period.secondsInPeriod {
return fmt.Sprintf("%d%s",
secondsAgo/periods[i-1].secondsInPeriod,
periods[i-1].label,
)
delta := float64(now - timestamp)
// we go seconds, minutes, hours, days, weeks, months, years
conversions := []float64{60, 60, 24, 7, 4.34524, 12}
labels := []string{"s", "m", "h", "d", "w", "m", "y"}
for i, conversion := range conversions {
if delta < conversion {
return fmt.Sprintf("%d%s", int(delta), labels[i])
}
delta /= conversion
}
return fmt.Sprintf("%d%s",
secondsAgo/periods[len(periods)-1].secondsInPeriod,
periods[len(periods)-1].label,
)
return fmt.Sprintf("%dy", int(delta))
}
// formats the date in a smart way, if the date is today, it will show the time, otherwise it will show the date
-97
View File
@@ -1,97 +0,0 @@
package utils
import (
"testing"
)
func TestFormatSecondsAgo(t *testing.T) {
tests := []struct {
name string
args int64
want string
}{
{
name: "zero",
args: 0,
want: "0s",
},
{
name: "one second",
args: 1,
want: "1s",
},
{
name: "almost a minute",
args: 59,
want: "59s",
},
{
name: "one minute",
args: 60,
want: "1m",
},
{
name: "one minute and one second",
args: 61,
want: "1m",
},
{
name: "almost one hour",
args: 3599,
want: "59m",
},
{
name: "one hour",
args: 3600,
want: "1h",
},
{
name: "almost one day",
args: 86399,
want: "23h",
},
{
name: "one day",
args: 86400,
want: "1d",
},
{
name: "almost a week",
args: 604799,
want: "6d",
},
{
name: "one week",
args: 604800,
want: "1w",
},
{
name: "six months",
args: SECONDS_IN_YEAR / 2,
want: "6m",
},
{
name: "almost one year",
args: 31535999,
want: "11m",
},
{
name: "one year",
args: SECONDS_IN_YEAR,
want: "1y",
},
{
name: "50 years",
args: SECONDS_IN_YEAR * 50,
want: "50y",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := formatSecondsAgo(tt.args); got != tt.want {
t.Errorf("formatSecondsAgo(%d) = %v, want %v", tt.args, got, tt.want)
}
})
}
}
+2 -4
View File
@@ -149,11 +149,9 @@ func SafeTruncate(str string, limit int) string {
}
}
const COMMIT_HASH_SHORT_SIZE = 8
func ShortSha(sha string) string {
if len(sha) < COMMIT_HASH_SHORT_SIZE {
if len(sha) < 8 {
return sha
}
return sha[:COMMIT_HASH_SHORT_SIZE]
return sha[:8]
}
+5 -10
View File
@@ -8,20 +8,15 @@
# this will hang if you're using git from the command line, so only enable this
# when you are testing the credentials view in lazygit
exec < /dev/tty
read -p "Username for 'github': " username
read -p "Password for 'github': " password
echo -n "Username for 'github': "
read username
echo
echo -n "Password for 'github': "
# this will print the password to the log view but real git won't do that.
# We could use read -s but that's not POSIX compliant.
read password
if [ "$username" = "username" -a "$password" = "password" ]; then
if [[ "$username" == "username" && "$password" == "password" ]]; then
echo "success"
exit 0
fi
>&2 echo "incorrect username/password"
echo "incorrect username/password" >&2
exit 1
+17
View File
@@ -0,0 +1,17 @@
FROM uffizzi/ttyd:golang1.18-alpine as build
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build
RUN apk update --quiet && \
apk add -q --no-cache libgcc tini
EXPOSE 7700/tcp
ENTRYPOINT ["tini", "--"]
CMD ["ttyd", "/bin/zsh"]
+20
View File
@@ -0,0 +1,20 @@
version: "3"
x-uffizzi:
ingress:
service: application
port: 7681
services:
application:
image: "${APP_IMAGE}"
entrypoint: ["/bin/bash", "-c"]
command: ["ttyd /usr/local/go/bin/go run /go/src/github.com/jesseduffield/lazygit/cmd/integration_test/main.go tui"]
ports:
- "7700:7700"
- "7681:7681"
deploy:
resources:
limits:
memory: 2000M
+17
View File
@@ -0,0 +1,17 @@
ARG GOVERSION=1.14
FROM golang:${GOVERSION}
# Set base env.
ARG GOOS=linux
ARG GOARCH=amd64
ENV GOOS=${GOOS} GOARCH=${GOARCH} CGO_ENABLED=0 GOFLAGS='-v -ldflags=-s -ldflags=-w'
# Pre compile the stdlib for 386/arm (32bits).
RUN go build -a std
# Add the code to the image.
WORKDIR pty
ADD . .
# Build the lib.
RUN go build
+11 -2
View File
@@ -1,3 +1,4 @@
# NOTE: Using 1.13 as a base to build the RISCV compiler, the resulting version is based on go1.6.
FROM golang:1.13
# Clone and complie a riscv compatible version of the go compiler.
@@ -8,7 +9,15 @@ ENV PATH=/riscv-go/misc/riscv:/riscv-go/bin:$PATH
RUN cd /riscv-go/src && GOROOT_BOOTSTRAP=$(go env GOROOT) ./make.bash
ENV GOROOT=/riscv-go
# Make sure we compile.
# Set the base env.
ENV GOOS=linux GOARCH=riscv CGO_ENABLED=0 GOFLAGS='-v -ldflags=-s -ldflags=-w'
# Pre compile the stdlib.
RUN go build -a std
# Add the code to the image.
WORKDIR pty
ADD . .
RUN GOOS=linux GOARCH=riscv go build
# Build the lib.
RUN go build
+16 -7
View File
@@ -1,12 +1,18 @@
# pty
Pty is a Go package for using unix pseudo-terminals.
Pty is a Go package for using unix pseudo-terminals and windows ConPty.
## Install
go get github.com/creack/pty
```sh
go get github.com/creack/pty
```
## Example
## Examples
Note that those examples are for demonstration purpose only, to showcase how to use the library. They are not meant to be used in any kind of production environment.
__NOTE:__ This package requires `ConPty` support on windows platform, please make sure your windows system meet [these requirements](https://docs.microsoft.com/en-us/windows/console/createpseudoconsole#requirements)
### Command
@@ -14,10 +20,11 @@ Pty is a Go package for using unix pseudo-terminals.
package main
import (
"github.com/creack/pty"
"io"
"os"
"os/exec"
"github.com/creack/pty"
)
func main() {
@@ -51,7 +58,7 @@ import (
"syscall"
"github.com/creack/pty"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/term"
)
func test() error {
@@ -77,15 +84,17 @@ func test() error {
}
}()
ch <- syscall.SIGWINCH // Initial resize.
defer func() { signal.Stop(ch); close(ch) }() // Cleanup signals when done.
// Set stdin in raw mode.
oldState, err := terminal.MakeRaw(int(os.Stdin.Fd()))
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
panic(err)
}
defer func() { _ = terminal.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.
defer func() { _ = term.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.
// Copy stdin to the pty and the pty to stdout.
// NOTE: The goroutine will keep reading until the next keystroke before returning.
go func() { _, _ = io.Copy(ptmx, os.Stdin) }()
_, _ = io.Copy(os.Stdout, ptmx)
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build gc
//+build gc
#include "textflag.h"
//
// System calls for amd64, Solaris are implemented in runtime/syscall_solaris.go
//
TEXT ·sysvicall6(SB),NOSPLIT,$0-88
JMP syscall·sysvicall6(SB)
TEXT ·rawSysvicall6(SB),NOSPLIT,$0-88
JMP syscall·rawSysvicall6(SB)
+356
View File
@@ -0,0 +1,356 @@
//go:build windows
// +build windows
package pty
import (
"bytes"
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"unicode/utf16"
"unsafe"
"golang.org/x/sys/windows"
)
// copied from os/exec.Cmd for platform compatibility
// we need to use startupInfoEx for pty support, but os/exec.Cmd only have
// support for startupInfo on windows, so we have to rewrite some internal
// logic for windows while keep its behavior compatible with other platforms.
// windowExecCmd represents an external command being prepared or run.
//
// A cmd cannot be reused after calling its Run, Output or CombinedOutput
// methods.
type windowExecCmd struct {
cmd *exec.Cmd
waitCalled bool
conPty *WindowsPty
attrList *windows.ProcThreadAttributeListContainer
}
var errProcessNotStarted = errors.New("exec: process has not started yet")
func (c *windowExecCmd) close() error {
c.attrList.Delete()
_ = c.conPty.Close()
return nil
}
func (c *windowExecCmd) Run() error {
err := c.Start()
if err != nil {
return err
}
return c.Wait()
}
func (c *windowExecCmd) Wait() error {
if c.cmd.Process == nil {
return errProcessNotStarted
}
var err error
if c.waitCalled {
return errors.New("exec: wait was already called")
}
c.waitCalled = true
c.cmd.ProcessState, err = c.cmd.Process.Wait()
if err != nil {
return err
}
err = c.close()
if err != nil {
return err
}
if !c.cmd.ProcessState.Success() {
return &exec.ExitError{ProcessState: c.cmd.ProcessState}
}
return nil
}
func (c *windowExecCmd) StdinPipe() (io.WriteCloser, error) {
return nil, ErrUnsupported
}
func (c *windowExecCmd) StdoutPipe() (io.ReadCloser, error) {
return nil, ErrUnsupported
}
func (c *windowExecCmd) StderrPipe() (io.ReadCloser, error) {
return nil, ErrUnsupported
}
func (c *windowExecCmd) Output() ([]byte, error) {
if c.cmd.Stdout != nil {
return nil, errors.New("exec: Stdout already set")
}
var stdout bytes.Buffer
c.cmd.Stdout = &stdout
err := c.Run()
return stdout.Bytes(), err
}
func (c *windowExecCmd) CombinedOutput() ([]byte, error) {
if c.cmd.Stdout != nil {
return nil, errors.New("exec: Stdout already set")
}
if c.cmd.Stderr != nil {
return nil, errors.New("exec: Stderr already set")
}
var b bytes.Buffer
c.cmd.Stdout = &b
c.cmd.Stderr = &b
err := c.Run()
return b.Bytes(), err
}
func (c *windowExecCmd) argv() []string {
if len(c.cmd.Args) > 0 {
return c.cmd.Args
}
return []string{c.cmd.Path}
}
//
// Helpers for working with Windows. These are exact copies of the same utilities found in the go stdlib.
//
func lookExtensions(path, dir string) (string, error) {
if filepath.Base(path) == path {
path = filepath.Join(".", path)
}
if dir == "" {
return exec.LookPath(path)
}
if filepath.VolumeName(path) != "" {
return exec.LookPath(path)
}
if len(path) > 1 && os.IsPathSeparator(path[0]) {
return exec.LookPath(path)
}
dirandpath := filepath.Join(dir, path)
// We assume that LookPath will only add file extension.
lp, err := exec.LookPath(dirandpath)
if err != nil {
return "", err
}
ext := strings.TrimPrefix(lp, dirandpath)
return path + ext, nil
}
func dedupEnvCase(caseInsensitive bool, env []string) []string {
// Construct the output in reverse order, to preserve the
// last occurrence of each key.
out := make([]string, 0, len(env))
saw := make(map[string]bool, len(env))
for n := len(env); n > 0; n-- {
kv := env[n-1]
i := strings.Index(kv, "=")
if i == 0 {
// We observe in practice keys with a single leading "=" on Windows.
// TODO(#49886): Should we consume only the first leading "=" as part
// of the key, or parse through arbitrarily many of them until a non-"="?
i = strings.Index(kv[1:], "=") + 1
}
if i < 0 {
if kv != "" {
// The entry is not of the form "key=value" (as it is required to be).
// Leave it as-is for now.
// TODO(#52436): should we strip or reject these bogus entries?
out = append(out, kv)
}
continue
}
k := kv[:i]
if caseInsensitive {
k = strings.ToLower(k)
}
if saw[k] {
continue
}
saw[k] = true
out = append(out, kv)
}
// Now reverse the slice to restore the original order.
for i := 0; i < len(out)/2; i++ {
j := len(out) - i - 1
out[i], out[j] = out[j], out[i]
}
return out
}
func addCriticalEnv(env []string) []string {
for _, kv := range env {
eq := strings.Index(kv, "=")
if eq < 0 {
continue
}
k := kv[:eq]
if strings.EqualFold(k, "SYSTEMROOT") {
// We already have it.
return env
}
}
return append(env, "SYSTEMROOT="+os.Getenv("SYSTEMROOT"))
}
func execEnvDefault(sys *syscall.SysProcAttr) (env []string, err error) {
if sys == nil || sys.Token == 0 {
return syscall.Environ(), nil
}
var block *uint16
err = windows.CreateEnvironmentBlock(&block, windows.Token(sys.Token), false)
if err != nil {
return nil, err
}
defer windows.DestroyEnvironmentBlock(block)
blockp := uintptr(unsafe.Pointer(block))
for {
// find NUL terminator
end := unsafe.Pointer(blockp)
for *(*uint16)(end) != 0 {
end = unsafe.Pointer(uintptr(end) + 2)
}
n := (uintptr(end) - uintptr(unsafe.Pointer(blockp))) / 2
if n == 0 {
// environment block ends with empty string
break
}
entry := (*[(1 << 30) - 1]uint16)(unsafe.Pointer(blockp))[:n:n]
env = append(env, string(utf16.Decode(entry)))
blockp += 2 * (uintptr(len(entry)) + 1)
}
return
}
func createEnvBlock(envv []string) *uint16 {
if len(envv) == 0 {
return &utf16.Encode([]rune("\x00\x00"))[0]
}
length := 0
for _, s := range envv {
length += len(s) + 1
}
length += 1
b := make([]byte, length)
i := 0
for _, s := range envv {
l := len(s)
copy(b[i:i+l], []byte(s))
copy(b[i+l:i+l+1], []byte{0})
i = i + l + 1
}
copy(b[i:i+1], []byte{0})
return &utf16.Encode([]rune(string(b)))[0]
}
func makeCmdLine(args []string) string {
var s string
for _, v := range args {
if s != "" {
s += " "
}
s += windows.EscapeArg(v)
}
return s
}
func isSlash(c uint8) bool {
return c == '\\' || c == '/'
}
func normalizeDir(dir string) (name string, err error) {
ndir, err := syscall.FullPath(dir)
if err != nil {
return "", err
}
if len(ndir) > 2 && isSlash(ndir[0]) && isSlash(ndir[1]) {
// dir cannot have \\server\share\path form
return "", syscall.EINVAL
}
return ndir, nil
}
func volToUpper(ch int) int {
if 'a' <= ch && ch <= 'z' {
ch += 'A' - 'a'
}
return ch
}
func joinExeDirAndFName(dir, p string) (name string, err error) {
if len(p) == 0 {
return "", syscall.EINVAL
}
if len(p) > 2 && isSlash(p[0]) && isSlash(p[1]) {
// \\server\share\path form
return p, nil
}
if len(p) > 1 && p[1] == ':' {
// has drive letter
if len(p) == 2 {
return "", syscall.EINVAL
}
if isSlash(p[2]) {
return p, nil
} else {
d, err := normalizeDir(dir)
if err != nil {
return "", err
}
if volToUpper(int(p[0])) == volToUpper(int(d[0])) {
return syscall.FullPath(d + "\\" + p[2:])
} else {
return syscall.FullPath(p)
}
}
} else {
// no drive letter
d, err := normalizeDir(dir)
if err != nil {
return "", err
}
if isSlash(p[0]) {
return windows.FullPath(d[:2] + p)
} else {
return windows.FullPath(d + "\\" + p)
}
}
}
+33 -3
View File
@@ -3,14 +3,44 @@ package pty
import (
"errors"
"os"
"io"
)
// ErrUnsupported is returned if a function is not
// available on the current platform.
var ErrUnsupported = errors.New("unsupported")
// Opens a pty and its corresponding tty.
func Open() (pty, tty *os.File, err error) {
// Open a pty and its corresponding tty.
func Open() (Pty, Tty, error) {
return open()
}
type FdHolder interface {
Fd() uintptr
}
// Pty for terminal control in current process
// for unix systems, the real type is *os.File
// for windows, the real type is a *WindowsPty for ConPTY handle
type Pty interface {
// FdHolder Fd intended to resize Tty of child process in current process
FdHolder
Name() string
// WriteString is only used to identify Pty and Tty
WriteString(s string) (n int, err error)
io.ReadWriteCloser
}
// Tty for data i/o in child process
// for unix systems, the real type is *os.File
// for windows, the real type is a *WindowsTty, which is a combination of two pipe file
type Tty interface {
// FdHolder Fd only intended for manual InheritSize from Pty
FdHolder
Name() string
io.ReadWriteCloser
}
+7 -1
View File
@@ -1,9 +1,15 @@
// +build !windows,!solaris
//go:build !windows && !solaris && !aix
// +build !windows,!solaris,!aix
package pty
import "syscall"
const (
TIOCGWINSZ = syscall.TIOCGWINSZ
TIOCSWINSZ = syscall.TIOCSWINSZ
)
func ioctl(fd, cmd, ptr uintptr) error {
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr)
if e != 0 {
+1
View File
@@ -1,3 +1,4 @@
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
// +build darwin dragonfly freebsd netbsd openbsd
package pty
+27 -9
View File
@@ -1,30 +1,48 @@
//go:build solaris
// +build solaris
package pty
import (
"golang.org/x/sys/unix"
"syscall"
"unsafe"
)
//go:cgo_import_dynamic libc_ioctl ioctl "libc.so"
//go:linkname procioctl libc_ioctl
var procioctl uintptr
const (
// see /usr/include/sys/stropts.h
I_PUSH = uintptr((int32('S')<<8 | 002))
I_STR = uintptr((int32('S')<<8 | 010))
I_FIND = uintptr((int32('S')<<8 | 013))
I_PUSH = uintptr((int32('S')<<8 | 002))
I_STR = uintptr((int32('S')<<8 | 010))
I_FIND = uintptr((int32('S')<<8 | 013))
// see /usr/include/sys/ptms.h
ISPTM = (int32('P') << 8) | 1
UNLKPT = (int32('P') << 8) | 2
PTSSTTY = (int32('P') << 8) | 3
ZONEPT = (int32('P') << 8) | 4
OWNERPT = (int32('P') << 8) | 5
// see /usr/include/sys/termios.h
TIOCSWINSZ = (uint32('T') << 8) | 103
TIOCGWINSZ = (uint32('T') << 8) | 104
)
type strioctl struct {
ic_cmd int32
ic_timout int32
ic_len int32
ic_dp unsafe.Pointer
icCmd int32
icTimeout int32
icLen int32
icDP unsafe.Pointer
}
// Defined in asm_solaris_amd64.s.
func sysvicall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
func ioctl(fd, cmd, ptr uintptr) error {
return unix.IoctlSetInt(int(fd), uint(cmd), int(ptr))
if _, _, errno := sysvicall6(uintptr(unsafe.Pointer(&procioctl)), 3, fd, cmd, ptr, 0, 0, 0); errno != 0 {
return errno
}
return nil
}
+13
View File
@@ -0,0 +1,13 @@
//go:build aix
// +build aix
package pty
const (
TIOCGWINSZ = 0
TIOCSWINSZ = 0
)
func ioctl(fd, cmd, ptr uintptr) error {
return ErrUnsupported
}
+1 -1
View File
@@ -13,7 +13,7 @@ GODEFS="go tool cgo -godefs"
$GODEFS types.go |gofmt > ztypes_$GOARCH.go
case $GOOS in
freebsd|dragonfly|openbsd)
freebsd|dragonfly|netbsd|openbsd)
$GODEFS types_$GOOS.go |gofmt > ztypes_$GOOSARCH.go
;;
esac
+4 -1
View File
@@ -1,3 +1,6 @@
//go:build darwin
// +build darwin
package pty
import (
@@ -33,7 +36,7 @@ func open() (pty, tty *os.File, err error) {
return nil, nil, err
}
t, err := os.OpenFile(sname, os.O_RDWR, 0)
t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
return nil, nil, err
}
+3
View File
@@ -1,3 +1,6 @@
//go:build dragonfly
// +build dragonfly
package pty
import (
+3
View File
@@ -1,3 +1,6 @@
//go:build freebsd
// +build freebsd
package pty
import (
+6 -3
View File
@@ -1,3 +1,6 @@
//go:build linux
// +build linux
package pty
import (
@@ -28,7 +31,7 @@ func open() (pty, tty *os.File, err error) {
return nil, nil, err
}
t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)
t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0) //nolint:gosec // Expected Open from a variable.
if err != nil {
return nil, nil, err
}
@@ -37,7 +40,7 @@ func open() (pty, tty *os.File, err error) {
func ptsname(f *os.File) (string, error) {
var n _C_uint
err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))) //nolint:gosec // Expected unsafe pointer for Syscall call.
if err != nil {
return "", err
}
@@ -47,5 +50,5 @@ func ptsname(f *os.File) (string, error) {
func unlockpt(f *os.File) error {
var u _C_int
// use TIOCSPTLCK with a pointer to zero to clear the lock
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))) //nolint:gosec // Expected unsafe pointer for Syscall call.
}
+69
View File
@@ -0,0 +1,69 @@
//go:build netbsd
// +build netbsd
package pty
import (
"errors"
"os"
"syscall"
"unsafe"
)
func open() (pty, tty *os.File, err error) {
p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
if err != nil {
return nil, nil, err
}
// In case of error after this point, make sure we close the ptmx fd.
defer func() {
if err != nil {
_ = p.Close() // Best effort.
}
}()
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
}
if err := grantpt(p); err != nil {
return nil, nil, err
}
// In NetBSD unlockpt() does nothing, so it isn't called here.
t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
return nil, nil, err
}
return p, t, nil
}
func ptsname(f *os.File) (string, error) {
/*
* from ptsname(3): The ptsname() function is equivalent to:
* struct ptmget pm;
* ioctl(fd, TIOCPTSNAME, &pm) == -1 ? NULL : pm.sn;
*/
var ptm ptmget
if err := ioctl(f.Fd(), uintptr(ioctl_TIOCPTSNAME), uintptr(unsafe.Pointer(&ptm))); err != nil {
return "", err
}
name := make([]byte, len(ptm.Sn))
for i, c := range ptm.Sn {
name[i] = byte(c)
if c == 0 {
return string(name[:i]), nil
}
}
return "", errors.New("TIOCPTSNAME string not NUL-terminated")
}
func grantpt(f *os.File) error {
/*
* from grantpt(3): Calling grantpt() is equivalent to:
* ioctl(fd, TIOCGRANTPT, 0);
*/
return ioctl(f.Fd(), uintptr(ioctl_TIOCGRANTPT), 0)
}
+16 -2
View File
@@ -1,3 +1,6 @@
//go:build openbsd
// +build openbsd
package pty
import (
@@ -6,6 +9,17 @@ import (
"unsafe"
)
func cInt8ToString(in []int8) string {
var s []byte
for _, v := range in {
if v == 0 {
break
}
s = append(s, byte(v))
}
return string(s)
}
func open() (pty, tty *os.File, err error) {
/*
* from ptm(4):
@@ -26,8 +40,8 @@ func open() (pty, tty *os.File, err error) {
return nil, nil, err
}
pty = os.NewFile(uintptr(ptm.Cfd), "/dev/ptm")
tty = os.NewFile(uintptr(ptm.Sfd), "/dev/ptm")
pty = os.NewFile(uintptr(ptm.Cfd), cInt8ToString(ptm.Cn[:]))
tty = os.NewFile(uintptr(ptm.Sfd), cInt8ToString(ptm.Sn[:]))
return pty, tty, nil
}
+83 -70
View File
@@ -1,3 +1,6 @@
//go:build solaris
// +build solaris
package pty
/* based on:
@@ -6,122 +9,134 @@ http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/pt.c
import (
"errors"
"golang.org/x/sys/unix"
"os"
"strconv"
"syscall"
"unsafe"
)
const NODEV = ^uint64(0)
func open() (pty, tty *os.File, err error) {
masterfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|unix.O_NOCTTY, 0)
//masterfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_CLOEXEC|unix.O_NOCTTY, 0)
ptmxfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
return nil, nil, err
}
p := os.NewFile(uintptr(masterfd), "/dev/ptmx")
p := os.NewFile(uintptr(ptmxfd), "/dev/ptmx")
// In case of error after this point, make sure we close the ptmx fd.
defer func() {
if err != nil {
_ = p.Close() // Best effort.
}
}()
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
}
err = grantpt(p)
if err != nil {
if err := grantpt(p); err != nil {
return nil, nil, err
}
err = unlockpt(p)
if err != nil {
if err := unlockpt(p); err != nil {
return nil, nil, err
}
slavefd, err := syscall.Open(sname, os.O_RDWR|unix.O_NOCTTY, 0)
ptsfd, err := syscall.Open(sname, os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
return nil, nil, err
}
t := os.NewFile(uintptr(slavefd), sname)
t := os.NewFile(uintptr(ptsfd), sname)
// In case of error after this point, make sure we close the pts fd.
defer func() {
if err != nil {
_ = t.Close() // Best effort.
}
}()
// pushing terminal driver STREAMS modules as per pts(7)
for _, mod := range([]string{"ptem", "ldterm", "ttcompat"}) {
err = streams_push(t, mod)
if err != nil {
for _, mod := range []string{"ptem", "ldterm", "ttcompat"} {
if err := streamsPush(t, mod); err != nil {
return nil, nil, err
}
}
return p, t, nil
}
func minor(x uint64) uint64 {
return x & 0377
}
func ptsdev(fd uintptr) uint64 {
istr := strioctl{ISPTM, 0, 0, nil}
err := ioctl(fd, I_STR, uintptr(unsafe.Pointer(&istr)))
if err != nil {
return NODEV
}
var status unix.Stat_t
err = unix.Fstat(int(fd), &status)
if err != nil {
return NODEV
}
return uint64(minor(status.Rdev))
}
func ptsname(f *os.File) (string, error) {
dev := ptsdev(f.Fd())
if dev == NODEV {
return "", errors.New("not a master pty")
dev, err := ptsdev(f.Fd())
if err != nil {
return "", err
}
fn := "/dev/pts/" + strconv.FormatInt(int64(dev), 10)
// access(2) creates the slave device (if the pty exists)
// F_OK == 0 (unistd.h)
err := unix.Access(fn, 0)
if err != nil {
if err := syscall.Access(fn, 0); err != nil {
return "", err
}
return fn, nil
}
type pt_own struct {
pto_ruid int32
pto_rgid int32
func unlockpt(f *os.File) error {
istr := strioctl{
icCmd: UNLKPT,
icTimeout: 0,
icLen: 0,
icDP: nil,
}
return ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr)))
}
func minor(x uint64) uint64 { return x & 0377 }
func ptsdev(fd uintptr) (uint64, error) {
istr := strioctl{
icCmd: ISPTM,
icTimeout: 0,
icLen: 0,
icDP: nil,
}
if err := ioctl(fd, I_STR, uintptr(unsafe.Pointer(&istr))); err != nil {
return 0, err
}
var status syscall.Stat_t
if err := syscall.Fstat(int(fd), &status); err != nil {
return 0, err
}
return uint64(minor(status.Rdev)), nil
}
type ptOwn struct {
rUID int32
rGID int32
}
func grantpt(f *os.File) error {
if ptsdev(f.Fd()) == NODEV {
return errors.New("not a master pty")
if _, err := ptsdev(f.Fd()); err != nil {
return err
}
var pto pt_own
pto.pto_ruid = int32(os.Getuid())
// XXX should first attempt to get gid of DEFAULT_TTY_GROUP="tty"
pto.pto_rgid = int32(os.Getgid())
var istr strioctl
istr.ic_cmd = OWNERPT
istr.ic_timout = 0
istr.ic_len = int32(unsafe.Sizeof(istr))
istr.ic_dp = unsafe.Pointer(&pto)
err := ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr)))
if err != nil {
pto := ptOwn{
rUID: int32(os.Getuid()),
// XXX should first attempt to get gid of DEFAULT_TTY_GROUP="tty"
rGID: int32(os.Getgid()),
}
istr := strioctl{
icCmd: OWNERPT,
icTimeout: 0,
icLen: int32(unsafe.Sizeof(strioctl{})),
icDP: unsafe.Pointer(&pto),
}
if err := ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr))); err != nil {
return errors.New("access denied")
}
return nil
}
func unlockpt(f *os.File) error {
istr := strioctl{UNLKPT, 0, 0, nil}
return ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr)))
}
// push STREAMS modules if not already done so
func streams_push(f *os.File, mod string) error {
var err error
// streamsPush pushes STREAMS modules if not already done so.
func streamsPush(f *os.File, mod string) error {
buf := []byte(mod)
// XXX I_FIND is not returning an error when the module
// is already pushed even though truss reports a return
// value of 1. A bug in the Go Solaris syscall interface?
@@ -129,11 +144,9 @@ func streams_push(f *os.File, mod string) error {
// https://www.illumos.org/issues/9042
// but since we are not using libc or XPG4.2, we should not be
// double-pushing modules
err = ioctl(f.Fd(), I_FIND, uintptr(unsafe.Pointer(&buf[0])))
if err != nil {
if err := ioctl(f.Fd(), I_FIND, uintptr(unsafe.Pointer(&buf[0]))); err != nil {
return nil
}
err = ioctl(f.Fd(), I_PUSH, uintptr(unsafe.Pointer(&buf[0])))
return err
return ioctl(f.Fd(), I_PUSH, uintptr(unsafe.Pointer(&buf[0])))
}

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