Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96f0df9538 | ||
|
|
47358ed9be | ||
|
|
1f156c0da2 | ||
|
|
ee559035ad | ||
|
|
daffd293d1 | ||
|
|
9aa36d889b | ||
|
|
f2e1afea16 | ||
|
|
c855d2521a | ||
|
|
299b53c708 | ||
|
|
3882d55f69 | ||
|
|
f92379c8fa | ||
|
|
846c0edd7d | ||
|
|
cdd2883fe9 | ||
|
|
9a26e675ee | ||
|
|
f20b09ff16 | ||
|
|
7308b10312 | ||
|
|
f199e412ab | ||
|
|
9beec6e30f | ||
|
|
bfc3ed1c16 | ||
|
|
613c9b23b4 | ||
|
|
c8d56beaa7 | ||
|
|
3857644bb8 | ||
|
|
bb0601ae1f | ||
|
|
5099288efd | ||
|
|
4c299c5563 | ||
|
|
e5965af368 | ||
|
|
c1d48b1df5 | ||
|
|
e127b8c555 | ||
|
|
f642546ccb |
@@ -1,11 +0,0 @@
|
||||
# adapted from https://github.com/devcontainers/images/blob/main/src/go/.devcontainer/Dockerfile
|
||||
|
||||
# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.19, 1.18, 1-bullseye, 1.19-bullseye, 1.18-bullseye, 1-buster, 1.19-buster, 1.18-buster
|
||||
ARG VARIANT=1-trixie
|
||||
FROM golang:${VARIANT}
|
||||
|
||||
RUN go install mvdan.cc/gofumpt@latest
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
@@ -1,69 +0,0 @@
|
||||
// adapted from https://github.com/devcontainers/images/blob/main/src/go/.devcontainer/devcontainer.json
|
||||
{
|
||||
"build": {
|
||||
"dockerfile": "./Dockerfile",
|
||||
"context": "."
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/common-utils:1": {
|
||||
"installZsh": "true",
|
||||
"username": "vscode",
|
||||
"uid": "1000",
|
||||
"gid": "1000",
|
||||
"upgradePackages": "true"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/go:1": {
|
||||
"version": "none"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git:1": {
|
||||
"version": "latest",
|
||||
"ppa": "false"
|
||||
}
|
||||
},
|
||||
"overrideFeatureInstallOrder": [
|
||||
"ghcr.io/devcontainers/features/common-utils"
|
||||
],
|
||||
// not sure if we actually need these
|
||||
"runArgs": [
|
||||
"--cap-add=SYS_PTRACE",
|
||||
"--security-opt",
|
||||
"seccomp=unconfined"
|
||||
],
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go",
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true,
|
||||
"usePlaceholders": false // add parameter placeholders when completing a function
|
||||
},
|
||||
"files.eol": "\n"
|
||||
},
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"golang.Go"
|
||||
]
|
||||
}
|
||||
},
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "go version",
|
||||
|
||||
// See https://www.kenmuse.com/blog/avoiding-dubious-ownership-in-dev-containers/ for the safe.directory part
|
||||
// The defaultBranch part is required for our deprecated integration tests.
|
||||
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && git config --global init.defaultBranch master",
|
||||
|
||||
// Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
@@ -1,3 +0,0 @@
|
||||
*.go text eol=lf
|
||||
*.md text eol=lf
|
||||
*.json text eol=lf
|
||||
@@ -1,4 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [jesseduffield]
|
||||
custom: ['https://donorbox.org/lazygit']
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
### To Reproduce
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
### Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
### Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
### Version info:
|
||||
|
||||
* _Run `lazygit --version` and paste the result here_
|
||||
|
||||
### Terminal info:
|
||||
What terminal are you using, and which version? For some types of bugs this information can be relevant.
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here.
|
||||
|
||||
> [!NOTE]
|
||||
> Please try updating to the latest version or [manually building](https://github.com/jesseduffield/lazygit/#manual) the latest `master` to see if the issue still occurs.
|
||||
|
||||
<!--
|
||||
If you want to try and debug this issue yourself, you can run `lazygit --debug` in one terminal panel and `lazygit --logs` in another to view the logs.
|
||||
-->
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Discussion
|
||||
about: Begin a discussion
|
||||
title: ''
|
||||
labels: discussion
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Topic
|
||||
A clear and concise description of what you want to discuss.
|
||||
|
||||
### Your thoughts
|
||||
What you have to say about the topic.
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Is your feature request related to a problem? Please describe.
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
### Describe the solution you'd like
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
### Describe alternatives you've considered
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
### Additional context
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
<!--
|
||||
You may be able to add your desired feature with a custom command. Check out the examples here: https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium
|
||||
|
||||
If a custom command does what you want but you still want to see the feature built-in to lazygit, feel free to paste the custom command into the issue to help us better understand the functionality you want.
|
||||
-->
|
||||
@@ -1,18 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "maintenance"
|
||||
- "dependencies"
|
||||
- "go"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "maintenance"
|
||||
- "dependencies"
|
||||
- "github_actions"
|
||||
@@ -1,18 +0,0 @@
|
||||
### PR Description
|
||||
|
||||
### Please check if the PR fulfills these requirements
|
||||
|
||||
* [ ] Cheatsheets are up-to-date (run `go generate ./...`)
|
||||
* [ ] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
|
||||
* [ ] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide)
|
||||
* [ ] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
|
||||
* [ ] If a new UserConfig entry was added, make sure it can be hot-reloaded (see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
|
||||
* [ ] Docs have been updated if necessary
|
||||
* [ ] You've read through your own file changes for silly mistakes etc
|
||||
|
||||
<!--
|
||||
Be sure to name your PR with an imperative e.g. 'Add worktrees view', and make sure the title
|
||||
is suitable to be included as a bullet point in release notes (i.e. phrased from a user's point
|
||||
of view).
|
||||
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for examples
|
||||
-->
|
||||
@@ -1,29 +0,0 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
categories:
|
||||
- title: Features ✨
|
||||
labels:
|
||||
- feature
|
||||
- title: Enhancements 🔥
|
||||
labels:
|
||||
- enhancement
|
||||
- title: Fixes 🔧
|
||||
labels:
|
||||
- bug
|
||||
- title: Maintenance ⚙️
|
||||
labels:
|
||||
- maintenance
|
||||
- title: Docs 📖
|
||||
labels:
|
||||
- docs
|
||||
- title: I18n 🌎
|
||||
labels:
|
||||
- i18n
|
||||
- title: Performance Improvements 📊
|
||||
labels:
|
||||
- performance
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- "*"
|
||||
@@ -1,15 +0,0 @@
|
||||
name: Check Required Labels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, labeled, unlabeled, synchronize]
|
||||
|
||||
jobs:
|
||||
check-required-label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@23e10fde7e062233401931a0eece796cd9bf3177 # v5
|
||||
with:
|
||||
mode: exactly
|
||||
count: 1
|
||||
labels: "ignore-for-release, feature, enhancement, bug, maintenance, docs, i18n, performance"
|
||||
@@ -1,253 +0,0 @@
|
||||
name: Continuous Integration
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.25
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
unit-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
|
||||
name: ci - ${{matrix.os}}
|
||||
runs-on: ${{matrix.os}}
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Test code
|
||||
# we're passing -short so that we skip the integration tests, which will be run in parallel below
|
||||
run: |
|
||||
mkdir -p /tmp/code_coverage
|
||||
go test ./... -short -cover -args "-test.gocoverdir=/tmp/code_coverage"
|
||||
- name: Upload code coverage artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-unit-${{ matrix.os }}-${{ github.run_id }}
|
||||
path: /tmp/code_coverage
|
||||
|
||||
integration-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
git-version:
|
||||
- 2.32.0 # oldest supported version
|
||||
- 2.38.2 # first version that supports the rebase.updateRefs config
|
||||
- 2.44.0
|
||||
- latest # We rely on github to have the latest version installed on their VMs
|
||||
race:
|
||||
- false
|
||||
# Additionally run the whole suite once under the race detector. Data
|
||||
# races live in lazygit's own Go code rather than in git, so a single
|
||||
# git version is enough; use the latest to skip the git-build steps.
|
||||
include:
|
||||
- git-version: latest
|
||||
race: true
|
||||
runs-on: ubuntu-latest
|
||||
name: "Integration Tests - git ${{matrix.git-version}}${{ matrix.race && ' (race)' || '' }}"
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
- name: Restore Git cache
|
||||
if: matrix.git-version != 'latest'
|
||||
id: cache-git-restore
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ~/git-${{matrix.git-version}}
|
||||
key: ${{runner.os}}-git-${{matrix.git-version}}
|
||||
- name: Build Git ${{matrix.git-version}}
|
||||
if: steps.cache-git-restore.outputs.cache-hit != 'true' && matrix.git-version != 'latest'
|
||||
run: >
|
||||
sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential ca-certificates curl gettext libexpat1-dev libssl-dev libz-dev openssl
|
||||
&& curl -sL "https://mirrors.edge.kernel.org/pub/software/scm/git/git-${{matrix.git-version}}.tar.xz" -o - | tar xJ -C "$HOME"
|
||||
&& cd "$HOME/git-${{matrix.git-version}}"
|
||||
&& ./configure
|
||||
&& make -j
|
||||
- name: Install Git ${{matrix.git-version}}
|
||||
if: matrix.git-version != 'latest'
|
||||
run: sudo make -C "$HOME/git-${{matrix.git-version}}" -j install
|
||||
- name: Save Git cache
|
||||
if: steps.cache-git-restore.outputs.cache-hit != 'true' && matrix.git-version != 'latest'
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: ~/git-${{matrix.git-version}}
|
||||
key: ${{runner.os}}-git-${{matrix.git-version}}
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Print git version
|
||||
run: git --version
|
||||
- name: Test code
|
||||
env:
|
||||
# See https://go.dev/blog/integration-test-coverage. The race variant
|
||||
# skips coverage: it's redundant with the non-race latest job and
|
||||
# would only slow the -race build down further. Leaving the dir unset
|
||||
# makes run_integration_tests.sh take its non-coverage path.
|
||||
LAZYGIT_GOCOVERDIR: ${{ !matrix.race && '/tmp/code_coverage' || '' }}
|
||||
# Only set for the race variant. The race detector needs cgo; it's on
|
||||
# by default on the Linux runner, but we set it explicitly to be safe.
|
||||
LAZYGIT_RACE_DETECTOR: ${{ matrix.race && '1' || '' }}
|
||||
CGO_ENABLED: ${{ matrix.race && '1' || '' }}
|
||||
# Append each test's duration to this file; run_integration_tests.sh
|
||||
# prints the slowest at the end, to spot slow/anomalous tests.
|
||||
LAZYGIT_TEST_TIMING: /tmp/test_timings.txt
|
||||
run: |
|
||||
mkdir -p /tmp/code_coverage
|
||||
./scripts/run_integration_tests.sh
|
||||
- name: Upload code coverage artifacts
|
||||
if: ${{ !matrix.race }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-integration-${{ matrix.git-version }}-${{ github.run_id }}
|
||||
path: /tmp/code_coverage
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
GOARCH: amd64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Build linux binary
|
||||
run: |
|
||||
GOOS=linux go build
|
||||
- name: Build windows binary
|
||||
run: |
|
||||
GOOS=windows go build
|
||||
- name: Build darwin binary
|
||||
run: |
|
||||
GOOS=darwin go build
|
||||
- name: Build integration test binary
|
||||
run: |
|
||||
GOOS=linux go build cmd/integration_test/main.go
|
||||
- name: Build integration test injector
|
||||
run: |
|
||||
GOOS=linux go build pkg/integration/clients/injector/main.go
|
||||
check-codebase:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
GOARCH: amd64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Check Vendor Directory
|
||||
# ensure our vendor directory matches up with our go modules
|
||||
run: |
|
||||
go mod vendor && git diff --exit-code || (echo "Unexpected change to vendor directory. Run 'go mod vendor' locally and commit the changes" && exit 1)
|
||||
- name: Check go.mod file
|
||||
# ensure our go.mod file is clean
|
||||
run: |
|
||||
go mod tidy && git diff --exit-code || (echo "go.mod file is not clean. Run 'go mod tidy' locally and commit the changes" && exit 1)
|
||||
- name: Check All Auto-Generated Files
|
||||
# ensure all our auto-generated files are up to date
|
||||
run: |
|
||||
go generate ./... && git diff --quiet || (git status -s; echo "Auto-generated files not up to date. Run 'go generate ./...' locally and commit the changes" && exit 1)
|
||||
shell: bash # needed so that we get "-o pipefail"
|
||||
- name: Check Filenames
|
||||
run: scripts/check_filenames.sh
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Check formatting
|
||||
run: ./scripts/gofumpt-check.sh
|
||||
- name: Lint
|
||||
# Run even if the formatting check failed, so that both sets of
|
||||
# problems are reported in a single CI run.
|
||||
if: ${{ !cancelled() }}
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9
|
||||
with:
|
||||
# If you change this, make sure to also update scripts/golangci-lint-shim.sh
|
||||
version: v2.12.2
|
||||
upload-coverage:
|
||||
# List all jobs that produce coverage files
|
||||
needs: [unit-tests, integration-tests]
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: /tmp/code_coverage
|
||||
|
||||
- name: Combine coverage files
|
||||
run: |
|
||||
# Find all directories in /tmp/code_coverage and create a comma-separated list
|
||||
COVERAGE_DIRS=$(find /tmp/code_coverage -mindepth 1 -maxdepth 1 -type d -printf '/tmp/code_coverage/%f,' | sed 's/,$//')
|
||||
echo "Coverage directories: $COVERAGE_DIRS"
|
||||
# Run the combine command with the generated list
|
||||
go tool covdata textfmt -i=$COVERAGE_DIRS -o coverage.out
|
||||
echo "Combined coverage:"
|
||||
go tool cover -func coverage.out | tail -1 | awk '{print $3}'
|
||||
|
||||
- name: Upload to Codacy
|
||||
run: |
|
||||
CODACY_PROJECT_TOKEN="${CODACY_PROJECT_TOKEN}" \
|
||||
bash <(curl -Ls https://coverage.codacy.com/get.sh) report \
|
||||
--force-coverage-parser go -r coverage.out
|
||||
|
||||
env:
|
||||
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||
check-for-fixups:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref != 'refs/heads/master'
|
||||
steps:
|
||||
# See https://github.com/actions/checkout/issues/552#issuecomment-1167086216
|
||||
- name: "PR commits"
|
||||
run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} ))" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: "Checkout PR branch and all PR commits"
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: ${{ env.PR_FETCH_DEPTH }}
|
||||
|
||||
- name: Check for fixups
|
||||
run: |
|
||||
./scripts/check_for_fixups.sh ${{ github.event.pull_request.base.ref }}
|
||||
@@ -1,39 +0,0 @@
|
||||
name: Close Issues
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
close_issue:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.issue.pull_request == null && startsWith(github.event.comment.body, '/close') }}
|
||||
steps:
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const trustedUsers = ['ChrisMcD1', 'jesseduffield', 'stefanhaller']
|
||||
const commenter = context.payload.comment.user.login
|
||||
|
||||
console.log(`Commenter: ${commenter}`)
|
||||
|
||||
if (!trustedUsers.includes(commenter)) {
|
||||
console.log(`User ${commenter} is not trusted. Ignoring.`)
|
||||
return
|
||||
}
|
||||
|
||||
const issueNumber = context.payload.issue.number
|
||||
const owner = context.repo.owner
|
||||
const repo = context.repo.repo
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
state: 'closed'
|
||||
})
|
||||
|
||||
console.log(`Closed issue #${issueNumber} by request from ${commenter}.`)
|
||||
@@ -1,174 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
# schedule:
|
||||
# # Runs at 8:00 AM UTC on every Saturday
|
||||
# # We'll check below if it's the first Saturday of the month, and fail if not
|
||||
# - cron: '0 8 * * 6'
|
||||
|
||||
# Allow manual triggering of the workflow
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_bump:
|
||||
description: 'Version bump type'
|
||||
type: choice
|
||||
required: true
|
||||
default: 'minor (normal)'
|
||||
options:
|
||||
- minor (normal)
|
||||
- patch (hotfix)
|
||||
branch:
|
||||
description: 'Branch to release from'
|
||||
type: string
|
||||
required: true
|
||||
default: 'master'
|
||||
ignore_blocks:
|
||||
description: 'Ignore blocking PRs/issues'
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
check-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for correct repository
|
||||
if: ${{ github.event_name != 'workflow_dispatch' && github.repository != 'stefanhaller/lazygit' }}
|
||||
run: |
|
||||
echo "Should only run in the stefanhaller/lazygit repository"
|
||||
exit 1
|
||||
|
||||
- name: Check for first Saturday of the month
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
run: |
|
||||
if (( $(date +%e) > 7 )); then
|
||||
echo "This is not the first Saturday of the month"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: jesseduffield/lazygit
|
||||
ref: ${{ inputs.branch }}
|
||||
token: ${{ secrets.LAZYGIT_RELEASE_PAT }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Latest Tag
|
||||
run: |
|
||||
latest_tag=$(git describe --tags --abbrev=0 || echo "v0.0.0")
|
||||
|
||||
if ! [[ $latest_tag =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Tag format is invalid. Expected format: vX.X.X"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Latest tag: $latest_tag"
|
||||
echo "latest_tag=$latest_tag" >> $GITHUB_ENV
|
||||
|
||||
- name: Check for changes since last release
|
||||
env:
|
||||
LATEST_TAG: ${{ env.latest_tag }}
|
||||
run: |
|
||||
if [ -z "$(git diff --name-only "$LATEST_TAG")" ]; then
|
||||
echo "No changes detected since last release"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check that docs and schema are up to date
|
||||
run: |
|
||||
if diff -r -q docs docs-master > /dev/null && diff -r -q schema schema-master > /dev/null; then
|
||||
echo "Docs and schema are up to date."
|
||||
else
|
||||
echo "Docs or schema are out of date. Please run 'scripts/update_docs_for_release.sh' and make a PR."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check for Blocking Issues/PRs
|
||||
if: ${{ !inputs.ignore_blocks }}
|
||||
id: check_blocks
|
||||
run: |
|
||||
gh auth setup-git
|
||||
gh auth status
|
||||
|
||||
echo "Checking for blocking issues and PRs..."
|
||||
|
||||
# Check for blocking issues
|
||||
blocking_issues=$(gh issue list -l blocks-release --json number,title --jq '.[] | "- \(.title) (#\(.number))"')
|
||||
|
||||
# Check for blocking PRs
|
||||
blocking_prs=$(gh pr list -l blocks-release --json number,title --jq '.[] | "- \(.title) (#\(.number)) (PR)"')
|
||||
|
||||
# Combine the results
|
||||
blocking_items="$blocking_issues"$'\n'"$blocking_prs"
|
||||
|
||||
# Remove empty lines
|
||||
blocking_items=$(echo "$blocking_items" | grep . || true)
|
||||
|
||||
if [ -n "$blocking_items" ]; then
|
||||
echo "Blocking issues/PRs detected:"
|
||||
echo "$blocking_items"
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.LAZYGIT_RELEASE_PAT }}
|
||||
|
||||
- name: Calculate next version
|
||||
env:
|
||||
LATEST_TAG: ${{ env.latest_tag }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
VERSION_BUMP: ${{ inputs.version_bump }}
|
||||
run: |
|
||||
echo "Latest tag: $LATEST_TAG"
|
||||
IFS='.' read -r major minor patch <<< "$LATEST_TAG"
|
||||
|
||||
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
|
||||
if [[ "$VERSION_BUMP" == "patch (hotfix)" ]]; then
|
||||
patch=$((patch + 1))
|
||||
else
|
||||
minor=$((minor + 1))
|
||||
patch=0
|
||||
fi
|
||||
else
|
||||
# Default behavior for scheduled runs
|
||||
minor=$((minor + 1))
|
||||
patch=0
|
||||
fi
|
||||
|
||||
new_tag="$major.$minor.$patch"
|
||||
|
||||
if ! [[ $new_tag =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: New tag's format is invalid. Expected format: vX.X.X"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "New tag: $new_tag"
|
||||
echo "new_tag=$new_tag" >> $GITHUB_ENV
|
||||
|
||||
- name: Create and Push Tag
|
||||
env:
|
||||
NEW_TAG: ${{ env.new_tag }}
|
||||
GITHUB_TOKEN: ${{ secrets.LAZYGIT_RELEASE_PAT }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag "$NEW_TAG" -a -m "Release $NEW_TAG"
|
||||
git push origin "refs/tags/$NEW_TAG"
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
|
||||
- name: Run goreleaser
|
||||
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: v2
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.LAZYGIT_RELEASE_PAT}}
|
||||
@@ -1,29 +0,0 @@
|
||||
# see https://github.com/JamesIves/github-sponsors-readme-action
|
||||
name: Generate Sponsors README
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jesseduffield/lazygit' }}
|
||||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Generate Sponsors 💖
|
||||
uses: JamesIves/github-sponsors-readme-action@02650b8cd445fc16dfef73195f9c406dce041623 # v1.6.1
|
||||
with:
|
||||
token: ${{ secrets.SPONSORS_TOKEN }}
|
||||
file: "README.md"
|
||||
|
||||
- name: Create Pull Request 🚀
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
|
||||
with:
|
||||
commit-message: "README.md: Update Sponsors"
|
||||
title: "README.md: Update Sponsors"
|
||||
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
labels: "ignore-for-release"
|
||||
delete-branch: true
|
||||
token: ${{ secrets.SPONSORS_PR_TOKEN }}
|
||||
@@ -1,36 +0,0 @@
|
||||
# Please do not add personal files
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Notes
|
||||
*.notes
|
||||
|
||||
# Tests
|
||||
test/repos/repo
|
||||
coverage.txt
|
||||
|
||||
# JetBrains stuff
|
||||
.idea/
|
||||
|
||||
# Binaries
|
||||
lazygit
|
||||
lazygit.exe
|
||||
|
||||
test/git_server/data
|
||||
|
||||
test/_results/**
|
||||
|
||||
oryxBuildBinary
|
||||
__debug_bin*
|
||||
|
||||
.worktrees
|
||||
demo/output/*
|
||||
|
||||
coverage.out
|
||||
|
||||
# Nix
|
||||
result
|
||||
result-*
|
||||
.direnv
|
||||
.envrc
|
||||
@@ -1,114 +0,0 @@
|
||||
version: "2"
|
||||
run:
|
||||
go: "1.25"
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
uniq-by-line: false
|
||||
linters:
|
||||
enable:
|
||||
- copyloopvar
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- intrange
|
||||
- makezero
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- revive
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- wastedassign
|
||||
settings:
|
||||
copyloopvar:
|
||||
check-alias: true
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
nakedret:
|
||||
# the gods will judge me but I just don't like naked returns at all
|
||||
max-func-lines: 0
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
|
||||
# SA1019 is for checking that we're not using fields marked as
|
||||
# deprecated in a comment. It decides this in a loose way so I'm
|
||||
# silencing it. Also because it's tripping on our own structs.
|
||||
- -SA1019
|
||||
|
||||
# ST1003 complains about names like remoteUrl or itemId (should be
|
||||
# remoteURL and itemID). While I like these suggestions, it also
|
||||
# complains about enum constants that are all caps, and we use these and
|
||||
# I like them, and also about camelCase identifiers that contain an
|
||||
# underscore, which we also use in a few places. Since it can't be
|
||||
# configured to ignore specific cases, and I don't want to use nolint
|
||||
# comments in the code, we have to disable it altogether.
|
||||
- -ST1003 # Poorly chosen identifier
|
||||
|
||||
# Probably a good idea, but we first have to review our error reporting
|
||||
# strategy to be able to use it everywhere.
|
||||
- -ST1005 # Error strings should not be capitalized
|
||||
|
||||
# Many of our classes use self as a receiver name, and we think that's fine.
|
||||
- -ST1006 # Use of self or this as receiver name
|
||||
|
||||
# De Morgan's law suggests to replace `!(a && b)` with `!a || !b`; but
|
||||
# sometimes I find one more readable than the other, so I want to decide
|
||||
# that myself.
|
||||
- -QF1001 # De Morgan's law
|
||||
|
||||
# QF1003 is about using a tagged switch instead of an if-else chain. In
|
||||
# many cases this is a useful suggestion; however, sometimes the change
|
||||
# is only possible by adding a default case to the switch (when there
|
||||
# was no `else` block in the original code), in which case I don't find
|
||||
# it to be an improvement.
|
||||
- -QF1003 # Could replace with tagged switch
|
||||
|
||||
# We need to review our use of embedded fields. I suspect that in some
|
||||
# cases the fix is not to remove the selector for the embedded field,
|
||||
# but to turn the embedded field into a named field.
|
||||
- -QF1008 # Could remove embedded field from selector
|
||||
|
||||
# The following checks are all disabled by default in golangci-lint, but
|
||||
# we disable them again explicitly here to make it easier to keep this
|
||||
# list in sync with the gopls config in .vscode/settings.json.
|
||||
- -ST1000, # At least one file in a package should have a package comment
|
||||
- -ST1020, # The documentation of an exported function should start with the function's name
|
||||
- -ST1021, # The documentation of an exported type should start with type's name
|
||||
- -ST1022, # The documentation of an exported variable or constant should start with variable's name
|
||||
|
||||
dot-import-whitelist:
|
||||
- github.com/jesseduffield/lazygit/pkg/integration/components
|
||||
revive:
|
||||
severity: warning
|
||||
rules:
|
||||
- name: atomic
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: error-naming
|
||||
- name: var-declaration
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: time-naming
|
||||
- name: indent-error-flow
|
||||
- name: errorf
|
||||
- name: superfluous-else
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- std-error-handling
|
||||
paths:
|
||||
- vendor/
|
||||
formatters:
|
||||
enable:
|
||||
# gofumpt is intentionally not listed here: golangci-lint bundles its own
|
||||
# gofumpt version, which drifts from the one we pin in go.mod. We run that
|
||||
# pinned version separately via scripts/gofumpt-check.sh instead.
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- vendor/
|
||||
@@ -1,38 +0,0 @@
|
||||
version: 2
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- freebsd
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- '386'
|
||||
# Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`.
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
|
||||
|
||||
archives:
|
||||
- name_template: >-
|
||||
{{- .ProjectName }}_
|
||||
{{- .Version }}_
|
||||
{{- .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}32-bit
|
||||
{{- else if eq .Arch "arm" }}armv6
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [ zip ]
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
version_template: '{{ .Tag }}-next'
|
||||
changelog:
|
||||
use: github-native
|
||||
sort: asc
|
||||
@@ -1 +0,0 @@
|
||||
disableStartupPopups: true
|
||||
@@ -1,70 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Lazygit",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": [
|
||||
"--debug",
|
||||
"--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"
|
||||
],
|
||||
"hideSystemGoroutines": true,
|
||||
"console": "integratedTerminal",
|
||||
},
|
||||
{
|
||||
"name": "Tail Lazygit logs",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": [
|
||||
"--logs",
|
||||
"--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
},
|
||||
{
|
||||
"name": "JSON Schema generator",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/pkg/jsonschema/generator.go",
|
||||
"cwd": "${workspaceFolder}/pkg/jsonschema",
|
||||
"console": "integratedTerminal",
|
||||
},
|
||||
{
|
||||
"name": "Attach to a running Lazygit",
|
||||
"type": "go",
|
||||
"request": "attach",
|
||||
"mode": "local",
|
||||
"processId": "lazygit",
|
||||
"hideSystemGoroutines": true,
|
||||
"console": "integratedTerminal",
|
||||
},
|
||||
{
|
||||
// To use this, first start an integration test with the "cli" runner and
|
||||
// use the -debug option; e.g.
|
||||
// $ make integration-test-cli -- -debug tag/reset.go
|
||||
"name": "Attach to integration test runner",
|
||||
"type": "go",
|
||||
"request": "attach",
|
||||
"mode": "local",
|
||||
"processId": "test_lazygit",
|
||||
"hideSystemGoroutines": true,
|
||||
"console": "integratedTerminal",
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Run with logs",
|
||||
"configurations": [
|
||||
"Tail Lazygit logs",
|
||||
"Debug Lazygit"
|
||||
],
|
||||
"stopAll": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"gopls": {
|
||||
"formatting.gofumpt": false,
|
||||
"ui.diagnostic.staticcheck": true,
|
||||
"ui.diagnostic.analyses": {
|
||||
// This list must match the one in .golangci.yml
|
||||
"SA1019": false,
|
||||
"ST1003": false,
|
||||
"ST1005": false,
|
||||
"ST1006": false,
|
||||
"QF1001": false,
|
||||
"QF1003": false,
|
||||
"QF1008": false,
|
||||
"ST1000": false,
|
||||
"ST1020": false,
|
||||
"ST1021": false,
|
||||
"ST1022": false,
|
||||
// Dot imports; this warning is enabled in .golangci.yml, but with an
|
||||
// extra dot-import-whitelist config. Because I couldn't figure out how to
|
||||
// specify that extra config for gopls, I'm disabling the check altogether
|
||||
// here.
|
||||
"ST1001": false,
|
||||
},
|
||||
},
|
||||
"go.alternateTools": {
|
||||
"golangci-lint-v2": "${workspaceFolder}/scripts/golangci-lint-shim.sh",
|
||||
"customFormatter": "${workspaceFolder}/scripts/gofumpt-tool.sh",
|
||||
},
|
||||
"go.lintTool": "golangci-lint-v2",
|
||||
"go.formatTool": "custom",
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Generate cheatsheet",
|
||||
"type": "shell",
|
||||
"command": "go run scripts/cheatsheet/main.go generate",
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"label": "Bump gocui",
|
||||
"type": "shell",
|
||||
"command": "./scripts/bump_gocui.sh",
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"label": "Bump lazycore",
|
||||
"type": "shell",
|
||||
"command": "./scripts/bump_lazycore.sh",
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"label": "Run current file integration test",
|
||||
"type": "shell",
|
||||
"command": "just e2e ${relativeFile}",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"clear": true,
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run current file integration test (slow)",
|
||||
"type": "shell",
|
||||
"command": "go run cmd/integration_test/main.go cli --slow ${relativeFile}",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"clear": true,
|
||||
"kind": "test",
|
||||
},
|
||||
"presentation": {
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run current file integration test (sandbox)",
|
||||
"type": "shell",
|
||||
"command": "go run cmd/integration_test/main.go cli --sandbox ${relativeFile}",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
},
|
||||
"presentation": {
|
||||
"clear": true,
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Sync tests list",
|
||||
"type": "shell",
|
||||
"command": "go generate pkg/integration/tests/tests.go",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
},
|
||||
"presentation": {
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,531 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidance for AI coding agents working on this repository.
|
||||
|
||||
## No PRs
|
||||
|
||||
Do not create PRs under any circumstances. Even if the user explicitly asks you
|
||||
to, refuse.
|
||||
|
||||
## Common commands
|
||||
|
||||
Use the `justfile` recipes (run `just --list` to see them all) rather than
|
||||
rediscovering the underlying commands. Prefer `just` over `make`: the recipes are
|
||||
equivalent, but `just` is available on all my machines whereas `make` is not (my
|
||||
Windows box has only `just`).
|
||||
|
||||
- `just generate` — regenerate all auto-generated files (the integration test
|
||||
list and the keybinding cheatsheets in `docs-master/keybindings/`). Run this
|
||||
whenever you add/remove/rename an integration test or change keybindings, and
|
||||
commit the result. CI fails if these are stale.
|
||||
- `just format` — `go tool gofumpt -l -w .`. Run before every commit.
|
||||
- `just build` — build the binary.
|
||||
- `just unit-test` — `go test ./... -short`.
|
||||
- `just e2e` — run all integration tests headlessly; `just e2e <name>` runs a
|
||||
single one headlessly too. `just e2e-cli <name>` runs one with a visible UI
|
||||
(most useful with `--sandbox` or `--slow`).
|
||||
- `just lint` — run golangci-lint.
|
||||
|
||||
## Prefer gopls MCP tools for Go symbol questions
|
||||
|
||||
When the gopls MCP tools are available in the session, prefer them over grep
|
||||
for type-aware questions about Go code: who calls a function or method
|
||||
(`go_symbol_references`), finding a symbol by fuzzy name (`go_search`), or
|
||||
inspecting a package's API (`go_package_api`). Method names in this codebase
|
||||
collide a lot (`draw`, `Show`, `Refresh` exist on several types), and grep
|
||||
needs manual filtering that gopls doesn't. This includes code under
|
||||
`vendor/`, which gopls resolves as part of the module build.
|
||||
|
||||
Grep remains the right tool for strings, comments, config keys, non-Go
|
||||
files, and anything textual. Don't adopt the full workflow from
|
||||
`gopls mcp -instructions` (vulncheck on session start, `go_file_context`
|
||||
after every file read); that overhead isn't worth it here.
|
||||
|
||||
If the tools aren't available in a session, fall back to grep silently —
|
||||
don't try to install, register, or start the server.
|
||||
|
||||
## When to commit
|
||||
|
||||
Do not leave completed work uncommitted. Once a logical unit of work is done
|
||||
and the tree is green, commit it — don't wait to be asked. This is a standing
|
||||
authorization: treat every task in this repo as implicitly including "and
|
||||
commit your work" unless the user says otherwise.
|
||||
|
||||
Commit as you go, not all at once at the end. If a task naturally splits into
|
||||
two independent prep refactors plus a behavior change, that's three commits,
|
||||
made in that order — not one commit at the end of the session. (Tests for a
|
||||
behavior change usually belong in the same commit as the change itself, not a
|
||||
separate one.)
|
||||
|
||||
## How to structure commits
|
||||
|
||||
Prefer a fine-grained commit history. Commits should be as small as possible
|
||||
while still being meaningful and self-contained.
|
||||
|
||||
- **Every commit must compile and pass all tests.** No "WIP" commits, no
|
||||
commits that leave the tree broken and rely on a follow-up to fix it. A
|
||||
`fixup!` is not such a follow-up; see "Iterate with `fixup!` commits" for
|
||||
what one may leave broken until it is folded in.
|
||||
- **Every commit must be `gofumpt`-formatted.** Run `just format` before
|
||||
committing.
|
||||
- **Every commit must be lint-clean.** Run `just lint` before committing —
|
||||
don't introduce a lint warning in one commit and rely on a later commit
|
||||
(or the user) to clean it up.
|
||||
- **Commit messages explain _why_, not _what_.** The diff already shows what
|
||||
changed; the message should capture the motivation, the constraint, or the
|
||||
bug being fixed. If the reason is obvious from a one-line subject, no body
|
||||
is needed — but never paraphrase the diff.
|
||||
- **Separate preparatory refactorings from behavior changes.** If a fix or
|
||||
feature is easier to review after a refactor, land the refactor in its own
|
||||
commit first. Pure refactors should be behavior-preserving; the commit that
|
||||
changes behavior should be as small as possible. This applies even when the
|
||||
refactor only becomes apparent _while_ writing the behavior change — e.g. you
|
||||
extract a helper to avoid duplication. Don't let "I discovered it mid-change"
|
||||
excuse bundling it in. Before committing, review your diff and split out any
|
||||
hunk that is behavior-preserving (an extraction, a rename, a move) into a
|
||||
preceding commit, by staging hunks or resetting and recommitting in order.
|
||||
- **A preparatory refactor is a new commit only when it prepares something
|
||||
new.** Before adding one, find the commit that introduced the code you are
|
||||
about to restructure. If that commit is on this branch, the refactor is a
|
||||
`fixup!` for it rather than a commit of its own: a branch must never contain
|
||||
a commit whose code a later commit on the same branch tidies up. A prep
|
||||
refactor earns a commit of its own only when the shape it corrects came from
|
||||
before the branch. This holds across a branch stack too — if the commit that
|
||||
introduced the code is in an earlier branch of the stack, the fixup belongs
|
||||
there, and the branches above it get replayed. The one exception is when
|
||||
fixing it there turns out to be unreasonably difficult; ask me what to do
|
||||
rather than deciding to leave the repair at the tip.
|
||||
- **Do not use conventional commits** (no `feat:`/`fix:`/`chore:` prefixes).
|
||||
Match the plain English imperative style of the existing history.
|
||||
- **Wrap message body to 72 characters**. The subject is allowed to go up to 80
|
||||
characters, or even a little more if needed to convey a good single-line
|
||||
summary; the body should be wrapped at 72 exactly, no more, no less.
|
||||
- **End every commit message with the `Co-authored-by:` trailer** naming the
|
||||
model that wrote it, exactly as your harness instructions spell it. Nothing
|
||||
in `just check` catches a missing one, so it has to be part of writing the
|
||||
message rather than something to notice afterwards.
|
||||
|
||||
## Iterate with `fixup!` commits
|
||||
|
||||
When refining work that's already committed — adjusting an approach,
|
||||
incorporating an idea from elsewhere, fixing something that belongs to the
|
||||
same logical unit — create a fixup against the target commit
|
||||
(`git commit --fixup=<sha>`) so it sits alongside its target, ready for the
|
||||
user to fold in later with `git rebase --autosquash`. Don't pile follow-up
|
||||
commits on top with the intent of squashing them later.
|
||||
|
||||
This holds **even when the target is the most recent commit (HEAD)**: use
|
||||
`git commit --fixup`, not `git commit --amend`. A direct `--amend`
|
||||
produces the same end state, which makes it tempting, but the point of a
|
||||
fixup isn't only clean autosquash — it's that the refinement lands as a
|
||||
separate, reviewable commit that the user decides when to fold in. A bare
|
||||
`--amend` rewrites the commit on the spot and skips that checkpoint. Don't
|
||||
treat "I'm only touching the tip commit" as an exception.
|
||||
|
||||
Always use `fixup!` or `amend!` commits, never amend changes directly, even if
|
||||
you naturally would because "the branch isn't pushed yet". The user always wants
|
||||
to review what you changed, so make this transparent; no exceptions.
|
||||
|
||||
**When the tip is the wrong place for a fixup, insert it mid-branch.**
|
||||
Committing a fixup at the tip of the branch only works while the code it
|
||||
touches still looks the same there; once later commits have rewritten that
|
||||
code — or the target has since been split — the fixup won't apply, and
|
||||
rewriting the later commits to accommodate it defeats the point. Check out the
|
||||
target, make the change, `git commit --fixup=<target>`, then
|
||||
`git rebase --onto <the fixup> <target> <branch>` to replay the rest of the
|
||||
branch. The fixup stays a separate, reviewable commit; only its position
|
||||
changes.
|
||||
|
||||
**A fixup may leave commits before it broken until it is folded in.** If a
|
||||
`fixup!` on an early commit deletes something that a later commit still uses,
|
||||
the later commit doesn't build until its own `fixup!`, right behind it, catches
|
||||
up; the same goes for lint. That is expected. The rules above about every
|
||||
commit compiling, testing and linting clean describe the history _after_
|
||||
autosquash, and I fold fixups in soon after reviewing them. Never amend a
|
||||
commit directly, or edit the commits between two fixups, to keep every commit
|
||||
of the un-squashed history green. The reviewable fixup is worth more than a
|
||||
green intermediate state. Verify at each fixup instead, since the tree there
|
||||
is what the folded-in history will have at that point, and say in the handoff
|
||||
which commits stay broken until which fixup.
|
||||
|
||||
**After a mid-stack fixup, check every branch tip above it, not just the stack
|
||||
tip.** A fixup that deletes or renames something rewrites every commit replayed
|
||||
above it, and a commit further up can hide the damage at the tip. A helper
|
||||
whose last caller the fixup deleted is flagged as unused by `just lint` at the
|
||||
tip of its own PR, but a later PR that calls it again makes the stack tip lint
|
||||
clean. Each PR is reviewed and merged on its own, so each PR branch tip has to
|
||||
be green on its own. After the replay, run `just build`, `just unit-test` and
|
||||
`just lint` at each branch tip from the insertion point up. If the fixup deleted
|
||||
or renamed a symbol, also build every replayed commit, for example with
|
||||
`git -c rebase.autosquash=false rebase -x 'go build ./...' <insertion point>`;
|
||||
unchanged commits are fast-forwarded, so their hashes stay, and the commits a
|
||||
fixup is expected to leave broken stop it, so `git rebase --continue` past
|
||||
those.
|
||||
|
||||
If the changes don't map cleanly onto existing commits — say they cut
|
||||
across several of them, or restructure something at a different layer
|
||||
than any existing commit naturally owns — stop and ask the user how to
|
||||
proceed. Resetting the branch and redoing the work is sometimes the right
|
||||
call, but it's the user's call to make.
|
||||
|
||||
After writing a fixup, re-read the target commit's message. If anything in
|
||||
that message has become inaccurate or misleading because of the fixup, use
|
||||
an `amend!` commit instead. The safest way to create one is
|
||||
`git commit --fixup=amend:<sha>`, which opens the editor prefilled with the
|
||||
target's existing message for you to revise.
|
||||
|
||||
An `amend!` commit's message has this exact shape:
|
||||
|
||||
```
|
||||
amend! <original subject>
|
||||
|
||||
<new subject>
|
||||
|
||||
<new body>
|
||||
```
|
||||
|
||||
The first line (`amend! <original subject>`) is **only the matcher** that
|
||||
ties the commit to its target — it must equal the target's current subject.
|
||||
Everything after the blank line is the **complete replacement message**, so
|
||||
it must begin with a subject line of its own. Even when you only mean to
|
||||
change the body, you still repeat the (unchanged) subject as that first line.
|
||||
|
||||
This is the trap when writing the message by hand with `-m` instead of using
|
||||
the prefilled editor: if you pass only the body, there is no replacement
|
||||
subject line, so after autosquash the target loses its subject and the first
|
||||
body paragraph silently gets promoted to the subject. By hand it must be
|
||||
`-m "amend! <subject>" -m "<subject>" -m "<body>"` — note the subject appears
|
||||
twice, once in the matcher and once as the start of the replacement message.
|
||||
|
||||
A plain `fixup!` keeps the original message verbatim, so message drift stays
|
||||
in unless you explicitly correct it.
|
||||
|
||||
**Never squash the fixups yourself.** Leave them in the history as separate
|
||||
commits. Do not run `git rebase --autosquash`, do not `git commit --amend`
|
||||
them into their targets, do not reorder or otherwise collapse them — not as
|
||||
a "finishing" step, not to tidy up before handing off, not because the tree
|
||||
looks messy. The whole point of a fixup is that the iteration stays
|
||||
**visible and reviewable**; squashing it away yourself destroys exactly the
|
||||
artifact it exists to create. Collapsing fixups into their targets is the
|
||||
user's action, taken once they've reviewed the iterations. Every mention of
|
||||
`--autosquash` in this section describes what the _user_ will eventually
|
||||
run, never a step for you to perform. If you think the history is ready to
|
||||
collapse, say so and leave it to them.
|
||||
|
||||
The same commit-structure rules apply to `fixup!` and `amend!` commits as
|
||||
to regular ones: each must be a self-contained logical unit, and unrelated
|
||||
changes must not be combined just because they happen to target the same
|
||||
commit. If you have two independent refinements for the same target, make
|
||||
two separate fixups. Reviewability of the intermediate state matters even
|
||||
when the end state after autosquash would be identical.
|
||||
|
||||
## Surface mid-implementation decisions; decide them together
|
||||
|
||||
Planning can't anticipate everything. When a decision surfaces while you're
|
||||
implementing — a design choice, a tradeoff, a scope cut, a "this turned out
|
||||
harder than expected, so maybe X" — don't quietly make the call and keep
|
||||
going, even if you have a clear recommendation and even if the call seems
|
||||
small. Stop, lay out the options and your recommendation, and let me weigh in.
|
||||
I want to make these calls _with_ you, not discover them after the fact in the
|
||||
diff.
|
||||
|
||||
This isn't a request to stop and ask about every trivial detail; obvious
|
||||
mechanical choices with one sensible answer don't need a checkpoint. It's about
|
||||
genuine forks — the ones where a reasonable person might pick differently, or
|
||||
where you'd be trading away something the plan assumed (scope, UX, performance,
|
||||
reload behavior, …). When in doubt, surface it.
|
||||
|
||||
This applies with equal force to unforeseen _discoveries_, not just to
|
||||
decisions you set out to make. If you find something the plan didn't account
|
||||
for — a latent bug, a race, a wrong assumption, a case that turns out
|
||||
unhandled — stop and raise it before designing or writing a fix, even when the
|
||||
fix seems obvious and even when it's "just correctness." Finding the problem is
|
||||
itself the fork: whether to fix it here or in a separate change, how generally
|
||||
to solve it, and whether it reshapes the current work are all calls for me to
|
||||
make with you. Don't quietly fold a self-directed fix for a newly-found problem
|
||||
into the branch and let me discover it in the diff.
|
||||
|
||||
## Prefer the cleaner design over the smaller diff
|
||||
|
||||
When a task could be implemented either by tacking onto existing code or by
|
||||
first restructuring it slightly, choose the restructuring. "Minimal change" is
|
||||
not a goal in itself; a readable final state is. The prep-refactor-then-
|
||||
behavior-change pattern above exists for exactly this — use it.
|
||||
|
||||
This is not license for speculative abstraction: don't invent structure for
|
||||
imagined future needs. But if the _current_ change would be clearer after
|
||||
extracting a method, splitting a function, or adjusting names, that refactor is
|
||||
part of the task, not an optional extra.
|
||||
|
||||
If you catch yourself thinking any of these, stop and refactor first:
|
||||
|
||||
- "This does a bit of wasted work, but it's harmless."
|
||||
- "I'll just add the new behavior alongside the old."
|
||||
- "The existing method does more than I need, but calling it is fine."
|
||||
|
||||
## Demonstrating bugs before fixing them
|
||||
|
||||
When fixing a defect, whenever it is reasonably possible, first land a commit
|
||||
that changes the relevant test(s) or adds new ones to demonstrate the bug, then
|
||||
fix the bug in a follow-up commit. This gives reviewers (and `git bisect`) a
|
||||
clear before/after and proves the test actually exercises the broken code path.
|
||||
|
||||
This applies only to defects that existed before the entire branch or branch
|
||||
stack. Never use the bug-demonstration pattern for a regression introduced by
|
||||
an earlier commit in the current stack. Fix or rewrite the commit that
|
||||
introduced the regression so that no commit in the final history contains it.
|
||||
Put the regression test in a preparatory commit before the introducing commit,
|
||||
so it guards that commit in the final history. If the test cannot pass before
|
||||
the feature exists, restructure the implementation or test seam until it can;
|
||||
if that would require a design tradeoff, stop and discuss it rather than adding
|
||||
a later demonstration/fix pair.
|
||||
|
||||
Use the `EXPECTED` / `ACTUAL` pattern in the bug-demonstrating commit. The test
|
||||
asserts the current (wrong) behavior so it passes on the broken code, with the
|
||||
correct expectation preserved inline as a comment. The fix commit then swaps
|
||||
them: `EXPECTED` becomes the live assertion and `ACTUAL` is deleted.
|
||||
|
||||
This pattern works in both integration tests and unit tests. Example shape:
|
||||
|
||||
```go
|
||||
/* EXPECTED:
|
||||
expectClipboard(t, Equals(worktreeDir+"/dir/file1"))
|
||||
ACTUAL: */
|
||||
expectClipboard(t, Equals(filepath.Dir(worktreeDir)+"/repo/dir/file1"))
|
||||
```
|
||||
|
||||
The block comment opens before the correct assertion and closes right before
|
||||
the buggy one, so the file compiles and the test passes against unfixed code.
|
||||
In the fix commit, remove the comment markers and delete the `ACTUAL` line.
|
||||
Don't explain the pattern in commit messages.
|
||||
|
||||
The fix commit must be _exactly_ "delete the markers and delete the `ACTUAL`
|
||||
line" — no other edits. That means `EXPECTED` and `ACTUAL` have to be drop-in
|
||||
replacements for each other at the same syntactic position. If you can't write
|
||||
them that way (e.g. one is `.IsEmpty()` and the other is `.Lines(...)`),
|
||||
restructure the surrounding code until you can — usually by putting the
|
||||
comment block between two adjacent chained calls, so both forms are just the
|
||||
next method in the chain:
|
||||
|
||||
```go
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
/* EXPECTED:
|
||||
IsEmpty()
|
||||
ACTUAL: */
|
||||
Lines(
|
||||
Equals("D file03.txt"),
|
||||
)
|
||||
```
|
||||
|
||||
If you find yourself reaching for a local variable so that both forms can be
|
||||
expressed against the same receiver, the structure isn't right yet — go back
|
||||
and fix it instead of papering over it with a binding.
|
||||
|
||||
Use this pattern only where it makes sense; don't apply it by default. Only
|
||||
ever use it for bugs, never for added features or behavior changes that aren't
|
||||
bugfixes; it is useful to demonstrate how a bug existed before fixing it, but
|
||||
it is never useful to demonstrate how a feature didn't exist before implementing
|
||||
it.
|
||||
|
||||
## Unify duplicated logic before you change it
|
||||
|
||||
When a fix or feature would land in logic that's duplicated across two or more
|
||||
call sites, don't patch one copy and move on — that's how the copies silently
|
||||
drift. (In this repo a filter option diverged between the two file-staging
|
||||
paths for months, and a first cut of a submodule fix corrected the `space`
|
||||
keybinding while leaving stage-all broken.) Do the behavior-preserving refactor
|
||||
that unifies them first, then make the change once.
|
||||
|
||||
Keep that refactor at the foundation of the branch, before the change. Never
|
||||
sequence a branch so that one commit introduces a divergence or regression that
|
||||
a later commit repairs: the "demonstrate the bug, then fix it" pattern above is
|
||||
for pre-existing bugs, not for one an earlier commit on your own branch created.
|
||||
Follow this even when the need for the refactor is only discovered in the middle
|
||||
of working on the branch; suggest to the user to rewrite the history to move the
|
||||
refactor to an earlier commit (but don't do it without asking first).
|
||||
|
||||
## Don't read model state right after a `Refresh`
|
||||
|
||||
A `Refresh` (or `RefreshFromWorker`) does its git work on a worker and then
|
||||
_enqueues_ the model update onto the UI thread. So when `Refresh` returns, the
|
||||
model is **not** updated yet — the write is still queued. Reading a field
|
||||
synchronously right after refreshing its scope reads the stale, pre-refresh
|
||||
value (and this is true even for SYNC refreshes):
|
||||
|
||||
```go
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
|
||||
files := self.c.Model().Files // BUG: still the pre-refresh value
|
||||
```
|
||||
|
||||
Put the read in `RefreshOptions.Then` instead — it's queued after the scope's
|
||||
model writes, so it sees the fresh value:
|
||||
|
||||
```go
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Scope: []types.RefreshableView{types.FILES},
|
||||
Then: func() error {
|
||||
files := self.c.Model().Files // fresh
|
||||
return nil
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
`Then` is a `func() error` and works with any non-`ASYNC` mode.
|
||||
|
||||
## Integration test conventions
|
||||
|
||||
Don't bind views to local variables. Always chain method calls directly from
|
||||
`t.Views().<View>()`. Patterns like `filesView := t.Views().Files().Focus()`
|
||||
followed by `filesView.Lines(...)` are not how tests in this repo are written;
|
||||
keep the call site fluent.
|
||||
|
||||
## Use stretchr/testify for assertions
|
||||
|
||||
Prefer `assert.Equal` (and friends) over hand-rolled `if` checks. The failure
|
||||
messages are more useful and the intent is clearer at a glance.
|
||||
|
||||
## Translatable strings use Go templates, not `%s`
|
||||
|
||||
Never put `fmt.Sprintf`-style placeholders (`%s`, `%d`, …) in translatable
|
||||
strings — the fields of `TranslationSet` and `Actions` in
|
||||
`pkg/i18n/english.go`. Use named Go-template placeholders and fill them in with
|
||||
`utils.ResolvePlaceholderString`:
|
||||
|
||||
```go
|
||||
// in english.go
|
||||
DeleteBranchTitle: "Delete branch '{{.selectedBranchName}}'?",
|
||||
|
||||
// at the call site
|
||||
utils.ResolvePlaceholderString(
|
||||
self.c.Tr.DeleteBranchTitle,
|
||||
map[string]string{"selectedBranchName": branchName},
|
||||
)
|
||||
```
|
||||
|
||||
Named placeholders tell localizers what each value is (a bare `%s` says
|
||||
nothing, and translators can't safely reorder positional verbs across
|
||||
languages), and the map form extends cleanly when a string later needs more
|
||||
than one placeholder. This holds for every user-facing string, including short
|
||||
ones like disabled-action reasons and toasts.
|
||||
|
||||
## Only edit the English translations
|
||||
|
||||
`pkg/i18n/english.go` is the one translation file you edit; add, change, and
|
||||
remove strings there. The other languages under `pkg/i18n/translations/` are
|
||||
maintained by Crowdin and synced automatically — never edit them by hand, not
|
||||
even to add a key you just introduced or to delete one you just removed. A
|
||||
removed English string simply leaves an orphan key in those files, which
|
||||
Crowdin cleans up on its own; an unknown key in a translation file is ignored
|
||||
at load time, so it does no harm in the meantime.
|
||||
|
||||
## Try to keep new english.go strings within the existing column alignment
|
||||
|
||||
`gofumpt` aligns the `TranslationSet` struct fields and the `EnglishTranslationSet`
|
||||
literal into columns, so a new field whose name is longer than the widest one in
|
||||
its alignment block re-indents every line in that block. When there are several
|
||||
feature branches in flight that all add strings, that reformatting churn turns
|
||||
english.go into a rebase-conflict magnet. So when it's cheap to do so, make an
|
||||
effort to keep a new field name within the current widest name in the block
|
||||
(measure it; it's around 40 characters today), shortening the Go field name to
|
||||
fit. This is a soft preference, not a rule: the usual "best name wins" still
|
||||
applies, so don't mangle a name past the point of readability just to save a
|
||||
column. Applies only to `pkg/i18n/english.go`.
|
||||
|
||||
## Code comments are for future readers, not development history
|
||||
|
||||
Comments in source code explain _why this code is shaped the way it is_. They
|
||||
are not the place to narrate the path we took during development — what was
|
||||
tried first, what didn't work, what's "more reliable" or "cleaner" than some
|
||||
alternative. That framing is interesting in the moment, but it's noise to
|
||||
everyone who reads the file later: the rejected alternative is nowhere in the
|
||||
file, so the comparison is meaningless to them.
|
||||
|
||||
Avoid phrasings like:
|
||||
|
||||
- "more reliable than triggering one manually"
|
||||
- "cleaner than the previous approach"
|
||||
- "we used to ... but ..."
|
||||
- "after trying X, we found Y"
|
||||
- "X rather than Y", where Y is what the code did before the change
|
||||
|
||||
The iteration story is sometimes worth preserving — but it belongs in the
|
||||
commit message, which is the durable record of _why this change was made_. The
|
||||
code comment should make sense to someone who has never seen any prior version
|
||||
and is just trying to understand the file as it currently exists.
|
||||
|
||||
The tell is subtler than an explicit "we used to". A comment that justifies the
|
||||
code against an alternative — "run it on a worker rather than blocking the UI",
|
||||
"switch panels in `Then` rather than a moment earlier" — is history in disguise
|
||||
whenever that alternative is what the code did before the change. It reads as
|
||||
ordinary rationale, but the reader has no way to know the contrast is with a
|
||||
version that no longer exists.
|
||||
|
||||
So the check to apply is: would you have written this comment if you were
|
||||
writing the file from scratch, with no diff in mind? If not, the sentence
|
||||
belongs in the commit message.
|
||||
|
||||
## Don't justify routine call sites
|
||||
|
||||
If the codebase calls a helper in twenty places without explanation, your
|
||||
twenty-first call site doesn't need one either. A comment there says "something
|
||||
here is unusual"; when nothing is, it's noise — and it invites exactly the kind
|
||||
of before/after justification the section above warns about. Look at the
|
||||
neighboring call sites before writing one: if they're bare, match them.
|
||||
|
||||
## Don't present "live with the bug" as an option
|
||||
|
||||
When you're investigating a defect and laying out fix options for the user,
|
||||
"accept the race / leave it as-is / document it and move on" is not one of
|
||||
them. A known race condition, data corruption, or correctness violation is a
|
||||
bug that needs a real fix, not a tradeoff. Even if the failure rate is low,
|
||||
even if the window is tiny, even if no current code path appears to hit it —
|
||||
present actual fixes. If a real fix is genuinely out of reach (e.g. it
|
||||
requires API changes you can't make), say so plainly; don't dress "no fix"
|
||||
up as a viable option in a numbered list alongside real ones.
|
||||
|
||||
## Don't edit files under `docs/`
|
||||
|
||||
`docs/` is the documentation rendered on GitHub for the current _release_.
|
||||
Users read it as the reference for the version they're running. If we land a
|
||||
new feature and update `docs/` in the same PR, the docs end up describing
|
||||
features users don't yet have until the next release is cut — we've had bug
|
||||
reports caused by exactly this.
|
||||
|
||||
So:
|
||||
|
||||
- Document new features in `docs-master/` only. The release process
|
||||
(`scripts/update_docs_for_release.sh`) copies `docs-master/` to `docs/` at
|
||||
release time.
|
||||
- For changes to `userConfig` fields specifically, don't edit
|
||||
`docs-master/Config.md` by hand either — the relevant section is
|
||||
auto-generated from the struct field doc comments. After editing the
|
||||
struct, run `just generate` and include the regenerated
|
||||
`docs-master/Config.md` (and `schema-master/config.json`) in your commit.
|
||||
- Don't hard-wrap the doc comments on `userConfig` fields. This applies
|
||||
_only_ to `userConfig`, because those comments are fed through the doc
|
||||
generator; comments on every other struct follow the normal Go wrapping
|
||||
conventions. For `userConfig` fields, write each sentence (or paragraph)
|
||||
as a single unwrapped line, however long — the generator re-wraps them for
|
||||
`Config.md` (see `wrapLine` in `pkg/jsonschema/generate_config_docs.go`).
|
||||
Manually wrapping a sentence across several `//` lines defeats this: the
|
||||
generator preserves your arbitrary breaks as hard line breaks and embeds
|
||||
`\n` at those points in the generated `schema-master/config.json`
|
||||
description. (Putting genuinely separate sentences on their own lines is
|
||||
fine; just don't split one sentence across lines.)
|
||||
|
||||
## Don't search outside the working tree
|
||||
|
||||
Never run `find` (or similar) from `/` or other paths outside the project. All
|
||||
third-party code we use is vendored under `vendor/`, so dependency sources are
|
||||
reachable from inside the working tree — search there instead of the host
|
||||
filesystem.
|
||||
|
||||
## gocui is in-tree, not a dependency
|
||||
|
||||
The `gocui` TUI library is a fork maintained directly in this repo under
|
||||
`pkg/gocui` — it's an ordinary package, not a Go module dependency. Don't look
|
||||
for it in `go.mod`/`go.sum` or the module cache (`$GOMODCACHE`); it isn't
|
||||
there. When you need to read or change gocui internals (the task manager, the
|
||||
event loop, worker/UI-thread dispatch, view rendering), edit `pkg/gocui`
|
||||
directly.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Lazygit Code of Conduct
|
||||
|
||||
Be nice, or face the wrath of the maintainer.
|
||||
@@ -1,35 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
## The short version
|
||||
|
||||
This project does not accept pull requests. Don't bother making one, it won't be merged.
|
||||
|
||||
However, there are other forms of contributions that are very welcome and encouraged; see below for what those are.
|
||||
|
||||
## Why no PRs?
|
||||
|
||||
There are two main reasons for this, and I want to be very honest about them:
|
||||
|
||||
- I am maintaining lazygit for fun, as a hobby in my free time (which is quite limited). I'd like to spend my free time on things that I enjoy doing. I enjoy working on lazygit's code and improving it myself; I don't enjoy reviewing PRs. It's that simple, really. Reviewing PRs takes a lot of time; time that I would rather spend on developing lazygit myself.
|
||||
- Even if I had the time and inclination to review PRs, this has become quite difficult today: most PRs nowadays are AI-generated to some extent (often completely), which in itself is not necessarily a bad thing; I heavily use AI myself these days, and I get great results from it. However, agentic coding needs to be guided by humans so that the results are good, and for contributed PRs I can't tell to what extent the human contributor did this, or is even capable of it; and I don't want to do the work of guiding a contributor's coding agent. If I post PR review feedback and have to suspect that the contributor simply passes it on to their coding agent, then that is a work mode that doesn't make sense to me, and I would rather just drive my own agent to do the work.
|
||||
|
||||
### Why it might still make sense to post a PR
|
||||
|
||||
I can think of two such reasons:
|
||||
|
||||
- You implemented a lazygit improvement that you want to use yourself; in this case it could make sense to let others merge this change into their forks if they find it useful too. And if enough people say they want the feature, this can persuade me to add it, so putting it out there to give it visibility can be helpful.
|
||||
- You posted an issue for a feature request, and have a prototype that implements it; it could be useful to publish the branch as a draft PR to better illustrate how the feature works.
|
||||
|
||||
For this reason I usually don't close pull requests to give them more visibility. Just don't expect your PR to be merged.
|
||||
|
||||
## So how can I contribute then?
|
||||
|
||||
There are other forms of contributions to a project besides source code that are very welcome and encouraged; for instance:
|
||||
|
||||
- File issues for bugs that you find, and I'll do my best to take care of fixing them (if they are important enough).
|
||||
- File feature requests for new functionality that you want to see in lazygit. I have a lot of ideas for future improvement myself, but I have also implemented a lot of feature ideas that weren't mine, and I'm grateful for those ideas. (Of course, there are also lots of feature requests that I don't implement, so don't be disappointed if I don't jump on yours.)
|
||||
- Help make other people's bug reports reproducible. Sometimes people report bugs that they have only seen once, and in such a case it can be helpful to come up with reproducible scenarios.
|
||||
- Help complete or improve the translation into other languages; join https://crowdin.com/project/lazygit for that.
|
||||
- Run a master build! This is probably the most valuable way to help me. Test the latest master not just by occasionally trying it, but by actually using it for your daily work; report any issues that you find. This will help prevent having to release hotfix updates for regressions that are only noticed by users updating to a new release.
|
||||
|
||||
Importantly, if you file issues (whether bug reports or feature requests), stay around to answer questions and discuss your issue. There are few things that I find more annoying than spending time on responding to someone's issue (sometimes even making a PR that addresses it), and to then never hear from the OP again. So please set up your Github notifications so that you see when there's activity on your issue, and continue to participate.
|
||||
@@ -1,19 +0,0 @@
|
||||
# run with:
|
||||
# docker build -t lazygit .
|
||||
# docker run -it lazygit:latest /bin/sh
|
||||
|
||||
FROM golang:1.25 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
|
||||
|
||||
FROM alpine:3.19
|
||||
RUN apk add --no-cache -U git xdg-utils
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
COPY --from=build /go/src/github.com/jesseduffield/lazygit ./
|
||||
COPY --from=build /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
|
||||
RUN echo "alias gg=lazygit" >> ~/.profile
|
||||
|
||||
ENTRYPOINT [ "lazygit" ]
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Jesse Duffield
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,77 +0,0 @@
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go build -gcflags='all=-N -l'
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go install
|
||||
|
||||
.PHONY: run
|
||||
run: build
|
||||
./lazygit
|
||||
|
||||
# Run `make run-debug` in one terminal tab and `make print-log` in another to view the program and its log output side by side
|
||||
.PHONY: run-debug
|
||||
run-debug:
|
||||
go run main.go -debug
|
||||
|
||||
.PHONY: print-log
|
||||
print-log:
|
||||
go run main.go --logs
|
||||
|
||||
.PHONY: unit-test
|
||||
unit-test:
|
||||
go test ./... -short
|
||||
|
||||
.PHONY: test
|
||||
test: unit-test integration-test-all
|
||||
|
||||
# Generate all our auto-generated files (test list, cheatsheets, maybe other things in the future)
|
||||
.PHONY: generate
|
||||
generate:
|
||||
go generate ./...
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
go tool gofumpt -l -w .
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
./scripts/gofumpt-check.sh
|
||||
./scripts/golangci-lint-shim.sh run
|
||||
|
||||
# For more details about integration test, see https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md.
|
||||
.PHONY: integration-test-tui
|
||||
integration-test-tui:
|
||||
go run cmd/integration_test/main.go tui $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
.PHONY: integration-test-cli
|
||||
integration-test-cli:
|
||||
go run cmd/integration_test/main.go cli $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
.PHONY: integration-test-all
|
||||
integration-test-all:
|
||||
go test pkg/integration/clients/*.go
|
||||
|
||||
.PHONY: bump-gocui
|
||||
bump-gocui:
|
||||
scripts/bump_gocui.sh
|
||||
|
||||
.PHONY: bump-lazycore
|
||||
bump-lazycore:
|
||||
scripts/bump_lazycore.sh
|
||||
|
||||
.PHONY: record-demo
|
||||
record-demo:
|
||||
demo/record_demo.sh $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
.PHONY: rerecord-demos
|
||||
rerecord-demos:
|
||||
demo/rerecord_demos.sh $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
.PHONY: vendor
|
||||
vendor:
|
||||
go mod tidy && go mod vendor
|
||||
@@ -1,105 +0,0 @@
|
||||
# Vision and Design Principles
|
||||
|
||||
## Vision
|
||||
|
||||
Lazygit's vision is to be the most enjoyable UI for git.
|
||||
|
||||
## Design Principles
|
||||
|
||||
There are seven (sometimes contradictory) design principles we follow:
|
||||
|
||||
- [Discoverability](#discoverability)
|
||||
- [Simplicity](#simplicity)
|
||||
- [Safety](#safety)
|
||||
- [Power](#power)
|
||||
- [Speed](#speed)
|
||||
- [Conformity with git](#conformity-with-git)
|
||||
- [Think of the codebase](#think-of-the-codebase)
|
||||
|
||||
### Discoverability
|
||||
|
||||
TUI's are notoriously hard to learn, thanks to limited screen real-estate to provide contextual help and a general lack of effort on the part of developers to make things obvious. We want Lazygit to buck the trend and be easy for a new user to grok.
|
||||
|
||||
Examples:
|
||||
|
||||
- Clearly document all the features/configuration options
|
||||
- e.g. gifs in the README
|
||||
- Document how to solve various git problems with Lazygit
|
||||
- This is something we don't have yet but should: a section in the docs explaining how Lazygit can help you in various scenarios
|
||||
- Use tooltips to explain what actions will do
|
||||
- Make it easy for users to ask questions and get answers from the community
|
||||
- Make it easy to find entities and actions from within Lazygit
|
||||
- Use visual elements to make things obvious
|
||||
- e.g. '<-- YOU ARE HERE' label when rebasing
|
||||
- Don't require the user to memorise keybindings
|
||||
- e.g. when the user is mid-rebase, we prominently show that the keybinding for viewing rebase options is 'm'
|
||||
- When the user performs an action in Lazygit, make the impact obvious
|
||||
- If the affected entity isn't visible, show a toast notification
|
||||
- If a keybinding is disabled, give a reason why
|
||||
|
||||
### Simplicity
|
||||
|
||||
The git CLI is very complex but most git use cases are simple. Lazygit needs to ensure that simple use cases are easy to satisfy.
|
||||
|
||||
- Make the most common use cases dead-simple (staging files, committing, pulling/pushing)
|
||||
- Don't overwhelm the user with options
|
||||
- Use sensible defaults
|
||||
- We already have too many configuration options: think hard before adding any new ones
|
||||
- A bit of elaboration on this one: in the past we made the mistake of adding new config options all the time for unimportant things. The thinking was: one user wants to have this new feature or behavior, we are not sure if everybody will like it, so we hide it behind a config. This seems good because we satisfy everybody's needs, but it's bad because if Config.md is pages and pages of text, most users will not bother reading all of it, so they won't be aware of the actually useful options among all the obscure ones. We should be much more conservative about adding new config options that only few users are likely to use.
|
||||
|
||||
### Safety
|
||||
|
||||
It's easy to screw things up in git so Lazygit should try to protect the user from screwing things up.
|
||||
|
||||
- Prompt for a confirmation before doing anything that's hard to reverse
|
||||
- Make it easy to correct mistakes
|
||||
- e.g. undo action
|
||||
- the escape key should get you out of most transient situations (rebasing, diffing, etc)
|
||||
|
||||
### Power
|
||||
|
||||
Users shouldn't have to drop down the CLI _too_ often. Lazygit should be able to handle some complex use cases.
|
||||
|
||||
- Make complex (but common) CLI flows simple
|
||||
- e.g. interactive rebasing
|
||||
- Use the custom commands system to handle the really rare complex edge-cases
|
||||
|
||||
### Speed
|
||||
|
||||
Pro users should be able to move at lightning speed with Lazygit.
|
||||
|
||||
- Always think about the number of keypresses involved in a given UX flow
|
||||
- Make lazygit performant and responsive
|
||||
- Think about the individual commands being run and how fast they are
|
||||
- Startup should be FAST. If you want to run something at startup that is slow, make it non-blocking.
|
||||
- Support muscle-memory
|
||||
- Prefer disabling menu items instead of hiding them so that muscle memory can be used to select the desired menu item
|
||||
- Try to make keybinding intuitions to transfer across contexts (e.g. 'd' for destroy)
|
||||
- When changing keybindings in a new release, always consider what will happen if a user does not read the release notes and relies on muscle memory.
|
||||
|
||||
### Conformity with git
|
||||
|
||||
Satisfying the use-cases of git users is more important than perfectly conforming to git's API, but even obscure parts of git's API were motivated by real use-cases.
|
||||
|
||||
- Users should only have to drop down to the git CLI in rare circumstances
|
||||
- Honour the git config
|
||||
- Don't override anything set in the git config without the user's permission
|
||||
- Work with git, not against it.
|
||||
- Too much magic will get us into trouble
|
||||
- Avoid storing Lazygit-specific session state that could instead be stored in git
|
||||
- Ensure that Lazygit can represent the state of any repo
|
||||
- Sometimes git's default behaviour is just silly and we'll make the call to override but it should be a well-considered decision.
|
||||
|
||||
### Think of the codebase
|
||||
|
||||
Will somebody PLEASE think of the codebase!
|
||||
|
||||
Some features are not worth the added complexity in the codebase. The more this codebase grows, the harder it will be to make the changes that everybody wants.
|
||||
|
||||
## Resolving conflicts
|
||||
|
||||
Many of the above objectives are directly antithetical to one another. If you add an extra confirmation prompt for the sake of _safety_, you're sacrificing _speed_. If you support toggling various git flags in the name of _power_, you're sacrificing _simplicity_. There are a few things to say here.
|
||||
|
||||
When there are conflicts, we need to make a judgement call. In general we should err on the side of safety and simplicity as the default, with the ability for users to make things faster / more powerful either through configuration or separate keybindings.
|
||||
|
||||
This does not mean for example that force pushes should be impossible without being manually enabled: force pushes are table stakes for anybody who rebases. But it does mean that a confirmation popup should appear when force pushing.
|
||||
@@ -1,142 +0,0 @@
|
||||
// author_colors_repo creates a git repository for checking that the colors
|
||||
// lazygit gives to authors are readable.
|
||||
//
|
||||
// If gui.authorColors names no color for an author, lazygit derives one from a
|
||||
// hash of their name. The authors of an ordinary repository rarely land near the
|
||||
// edges of the range that this color is picked from. Every commit in the
|
||||
// repository created here is by an author at one of those edges: the lowest or
|
||||
// highest lightness, combined with the lowest or highest saturation, at twelve
|
||||
// hues around the color wheel. The commit subject says which edge it is.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/author_colors_repo <path>
|
||||
//
|
||||
// Then open the repository with lazygit in each terminal theme you want to check.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
|
||||
)
|
||||
|
||||
type extreme int
|
||||
|
||||
const (
|
||||
lowest extreme = iota
|
||||
highest
|
||||
)
|
||||
|
||||
func (self extreme) String() string {
|
||||
if self == lowest {
|
||||
return "min"
|
||||
}
|
||||
return "max"
|
||||
}
|
||||
|
||||
func (self extreme) matches(fraction float64) bool {
|
||||
if self == lowest {
|
||||
return fraction < 0.01
|
||||
}
|
||||
return fraction >= 0.99
|
||||
}
|
||||
|
||||
const (
|
||||
numHues = 12
|
||||
|
||||
// Small enough that the windows of neighbouring hues don't overlap
|
||||
hueTolerance = 0.02
|
||||
)
|
||||
|
||||
type commit struct {
|
||||
author string
|
||||
subject string
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
log.Fatalf("usage: %s <path>", os.Args[0])
|
||||
}
|
||||
path := os.Args[1]
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
log.Fatalf("%s already exists", path)
|
||||
}
|
||||
|
||||
extremes := []extreme{lowest, highest}
|
||||
commits := make([]commit, 0, len(extremes)*len(extremes)*numHues)
|
||||
for _, lightness := range extremes {
|
||||
for _, saturation := range extremes {
|
||||
for i := range numHues {
|
||||
hue := float64(i) / numHues
|
||||
author, actualHue := findAuthor(lightness, saturation, hue)
|
||||
subject := fmt.Sprintf("lightness %s, saturation %s, hue %.0f°", lightness, saturation, actualHue*360)
|
||||
commits = append(commits, commit{author: author, subject: subject})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := runGit("", nil, "init", "-q", path); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Commit in reverse, so that the commits panel lists them in the order above
|
||||
startTime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
for i := range commits {
|
||||
c := commits[len(commits)-1-i]
|
||||
date := fmt.Sprintf("%d +0000", startTime.Add(time.Duration(i)*time.Hour).Unix())
|
||||
env := []string{
|
||||
"GIT_AUTHOR_NAME=" + c.author,
|
||||
"GIT_AUTHOR_EMAIL=author@example.com",
|
||||
"GIT_AUTHOR_DATE=" + date,
|
||||
"GIT_COMMITTER_NAME=" + c.author,
|
||||
"GIT_COMMITTER_EMAIL=author@example.com",
|
||||
"GIT_COMMITTER_DATE=" + date,
|
||||
}
|
||||
if err := runGit(path, env, "commit", "-q", "--allow-empty", "-m", c.subject); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Created %s with %d commits\n", path, len(commits))
|
||||
}
|
||||
|
||||
// findAuthor returns the first name of the form "L<lightness> S<saturation> <n>"
|
||||
// whose color lies at the given extremes of lightness and saturation, and close
|
||||
// to the given hue. It also returns the hue that the name lands on.
|
||||
//
|
||||
// Every name of this form has the same initials, so the color is the only
|
||||
// thing that differs between authors in the commits panel.
|
||||
func findAuthor(lightness extreme, saturation extreme, hue float64) (string, float64) {
|
||||
for n := 1; ; n++ {
|
||||
name := fmt.Sprintf("L%s S%s %d", lightness, saturation, n)
|
||||
h, s, l := authors.ColorPosition(name)
|
||||
if lightness.matches(l) && saturation.matches(s) && hueDistance(h, hue) <= hueTolerance {
|
||||
return name, h
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hueDistance is the distance between two hues on the color wheel, where each
|
||||
// hue is a fraction of a full turn.
|
||||
func hueDistance(a float64, b float64) float64 {
|
||||
d := math.Abs(a - b)
|
||||
return math.Min(d, 1-d)
|
||||
}
|
||||
|
||||
func runGit(dir string, env []string, args ...string) error {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
// Keep the user's git config out of it, so that hooks, commit signing and
|
||||
// the like don't apply, and every run creates the same commits
|
||||
cmd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL="+os.DevNull, "GIT_CONFIG_NOSYSTEM=1")
|
||||
cmd.Env = append(cmd.Env, env...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
)
|
||||
|
||||
func saveLanguageFileToJson(tr *i18n.TranslationSet, filepath string) error {
|
||||
jsonData, err := json.MarshalIndent(tr, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jsonData = append(jsonData, '\n')
|
||||
return os.WriteFile(filepath, jsonData, 0o644)
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := saveLanguageFileToJson(i18n.EnglishTranslationSet(), "en.json")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/clients"
|
||||
)
|
||||
|
||||
var usage = `
|
||||
Usage:
|
||||
See https://github.com/jesseduffield/lazygit/tree/master/pkg/integration/README.md
|
||||
|
||||
CLI mode:
|
||||
> go run cmd/integration_test/main.go cli [--slow] [--sandbox] <test1> <test2> ...
|
||||
If you pass no test names, it runs all tests
|
||||
Accepted environment variables:
|
||||
INPUT_DELAY (e.g. 200): the number of milliseconds to wait between keypresses or mouse clicks
|
||||
|
||||
TUI mode:
|
||||
> go run cmd/integration_test/main.go tui
|
||||
This will open up a terminal UI where you can run tests
|
||||
|
||||
Help:
|
||||
> go run cmd/integration_test/main.go help
|
||||
`
|
||||
|
||||
type flagInfo struct {
|
||||
name string // name of the flag; can be used with "-" or "--"
|
||||
flag *bool // a pointer to the variable that should be set to true when this flag is passed
|
||||
}
|
||||
|
||||
// Takes the args that you want to parse (excluding the program name and any
|
||||
// subcommands), and returns the remaining args with the flags removed
|
||||
func parseFlags(args []string, flags []flagInfo) []string {
|
||||
outer:
|
||||
for len(args) > 0 {
|
||||
for _, f := range flags {
|
||||
if args[0] == "-"+f.name || args[0] == "--"+f.name {
|
||||
*f.flag = true
|
||||
args = args[1:]
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
log.Fatal(usage)
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "help":
|
||||
fmt.Println(usage)
|
||||
case "cli":
|
||||
slow := false
|
||||
sandbox := false
|
||||
waitForDebugger := false
|
||||
raceDetector := false
|
||||
testNames := parseFlags(os.Args[2:], []flagInfo{
|
||||
{"slow", &slow},
|
||||
{"sandbox", &sandbox},
|
||||
{"debug", &waitForDebugger},
|
||||
{"race", &raceDetector},
|
||||
})
|
||||
clients.RunCLI(testNames, slow, sandbox, waitForDebugger, raceDetector)
|
||||
case "tui":
|
||||
raceDetector := false
|
||||
remainingArgs := parseFlags(os.Args[2:], []flagInfo{
|
||||
{"race", &raceDetector},
|
||||
})
|
||||
if len(remainingArgs) > 0 {
|
||||
log.Fatal("tui only supports the -race argument.")
|
||||
}
|
||||
clients.RunTUI(raceDetector)
|
||||
default:
|
||||
log.Fatal(usage)
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -1,12 +0,0 @@
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
nodeName = lock.nodes.root.inputs.flake-compat;
|
||||
in
|
||||
fetchTarball {
|
||||
url =
|
||||
lock.nodes.${nodeName}.locked.url
|
||||
or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
||||
}
|
||||
) { src = ./.; }).defaultNix
|
||||
@@ -1,2 +0,0 @@
|
||||
This directory contains stuff for recording lazygit demos.
|
||||
|
||||
|
After Width: | Height: | Size: 473 KiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 820 KiB |
|
After Width: | Height: | Size: 6.9 MiB |
|
After Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 462 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 650 KiB |
|
After Width: | Height: | Size: 11 MiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 431 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 rbong
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,116 +0,0 @@
|
||||
"""Make a copy of the Flog Symbols font whose lines fill a terminal cell.
|
||||
|
||||
The demo recordings draw the commit graph with the branch drawing symbols, and
|
||||
the terminal that vhs records takes them from the Flog Symbols Demo font in
|
||||
demo/fonts. This script made its regular and bold faces from FlogSymbols.ttf of
|
||||
https://github.com/rbong/flog-symbols (see LICENSE-FlogSymbols):
|
||||
|
||||
pip install fonttools
|
||||
python3 demo/fonts/fit_flog_symbols.py FlogSymbols.ttf "Flog Symbols Demo" \\
|
||||
demo/fonts
|
||||
|
||||
Flog Symbols draws its lines for a cell that is 620 units wide and reaches from
|
||||
-206 to 1006 units. A terminal that takes the symbols from a fallback font
|
||||
draws them in the cells of its main font, and if those are larger, the lines
|
||||
stop short of the cell edges and leave gaps between neighbouring cells.
|
||||
|
||||
So move the ends of the strokes that run to an edge of the cell out to the
|
||||
terminal's cell edges, and centre everything else in the cell. The circles and
|
||||
bends in the middle of the cell keep their shape.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
# The cell of the symbols, in font units
|
||||
LEFT, RIGHT, BOTTOM, TOP = -6, 626, -206, 1006
|
||||
|
||||
# The width of the terminal cells in the recordings, in font units: at the font
|
||||
# size in demo/settings.tape they are 16 pixels wide (SauceCodePro's 14.4
|
||||
# pixels, plus the pixel of letter spacing that vhs adds, rounded up), which is
|
||||
# 16/24 of an em. Regenerate the font if the font size changes.
|
||||
CELL_WIDTH = 667
|
||||
|
||||
# How far the horizontal strokes reach into the neighbouring cells, in font
|
||||
# units. xterm.js doesn't clip a character to its cell horizontally, so this
|
||||
# has to stay short of where the bends in a neighbouring cell begin. In the
|
||||
# recordings, less than 30 leaves a dim line where two cells meet, and 35 or
|
||||
# more opens a dark gap there.
|
||||
HORIZONTAL_OVERLAP = 30
|
||||
|
||||
# How far past the top and bottom of the symbols' cell the vertical strokes
|
||||
# reach, in font units. xterm.js clips a character to its row, so this only
|
||||
# has to be more than the terminal's row sticks out beyond the symbols' cell.
|
||||
VERTICAL_REACH = 400
|
||||
|
||||
# Points this close to an edge of the symbols' cell belong to the end of a
|
||||
# stroke that runs to that edge
|
||||
EDGE_TOLERANCE = 30
|
||||
|
||||
|
||||
def main():
|
||||
flog_path, family, output_dir = sys.argv[1:4]
|
||||
font = TTFont(flog_path)
|
||||
fit_to_cell(font)
|
||||
|
||||
# The bold face has the same outlines. Without one, the browser makes the
|
||||
# symbols of bold text bold itself by thickening them, and that leaves gaps
|
||||
# where they meet.
|
||||
for style in ("Regular", "Bold"):
|
||||
set_style(font, family, style)
|
||||
font.save(os.path.join(output_dir, f"{family.replace(' ', '')}-{style}.ttf"))
|
||||
|
||||
|
||||
def fit_to_cell(font):
|
||||
glyf = font["glyf"]
|
||||
|
||||
# Centre the symbols in the terminal's cell
|
||||
dx = round((CELL_WIDTH - (RIGHT + LEFT)) / 2)
|
||||
|
||||
for name in font.getGlyphOrder():
|
||||
glyph = glyf[name]
|
||||
if glyph.numberOfContours <= 0:
|
||||
continue
|
||||
coordinates = glyph.coordinates
|
||||
for i, (x, y) in enumerate(coordinates):
|
||||
if x <= LEFT + EDGE_TOLERANCE:
|
||||
x = -HORIZONTAL_OVERLAP
|
||||
elif x >= RIGHT - EDGE_TOLERANCE:
|
||||
x = CELL_WIDTH + HORIZONTAL_OVERLAP
|
||||
else:
|
||||
x += dx
|
||||
if y <= BOTTOM + EDGE_TOLERANCE:
|
||||
y -= VERTICAL_REACH
|
||||
elif y >= TOP - EDGE_TOLERANCE:
|
||||
y += VERTICAL_REACH
|
||||
coordinates[i] = (x, y)
|
||||
glyph.recalcBounds(glyf)
|
||||
font["hmtx"][name] = (CELL_WIDTH, glyph.xMin)
|
||||
|
||||
|
||||
def set_style(font, family, style):
|
||||
bold = style == "Bold"
|
||||
postscript_name = f"{family.replace(' ', '')}-{style}"
|
||||
full_name = family if not bold else f"{family} {style}"
|
||||
for record in font["name"].names:
|
||||
if record.nameID == 1:
|
||||
record.string = family
|
||||
elif record.nameID == 2:
|
||||
record.string = style
|
||||
elif record.nameID == 4:
|
||||
record.string = full_name
|
||||
elif record.nameID in (3, 6):
|
||||
record.string = postscript_name
|
||||
|
||||
os2 = font["OS/2"]
|
||||
os2.usWeightClass = 700 if bold else 400
|
||||
fs_bold, fs_regular = 1 << 5, 1 << 6
|
||||
os2.fsSelection &= ~(fs_bold | fs_regular)
|
||||
os2.fsSelection |= fs_bold if bold else fs_regular
|
||||
font["head"].macStyle = 1 if bold else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 962 KiB |
|
After Width: | Height: | Size: 7.0 MiB |
|
After Width: | Height: | Size: 626 KiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 611 KiB |
|
After Width: | Height: | Size: 4.2 MiB |
@@ -1,248 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# The repository the demo is uploaded to. GitHub only plays videos that live in
|
||||
# its own attachment store, and an attachment is tied to one repository.
|
||||
REPO=jesseduffield/lazygit
|
||||
|
||||
# The issue that collects the demo recordings. Posting a comment there is what
|
||||
# makes an uploaded video readable by people who are not signed in to GitHub.
|
||||
# It can stay closed; commenting on a closed issue publishes the video just as
|
||||
# well, and does not reopen it.
|
||||
PUBLISH_ISSUE=6051
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [--no-upload] <test path>"
|
||||
echo "e.g. $0 pkg/integration/tests/demo/nuke_working_tree.go"
|
||||
echo
|
||||
echo "--no-upload leaves the video in demo/output and stops there, for"
|
||||
echo "checking how a change to demo/settings.tape turns out."
|
||||
exit 1
|
||||
}
|
||||
|
||||
UPLOAD=true
|
||||
|
||||
if [ "$1" = "--no-upload" ]
|
||||
then
|
||||
UPLOAD=false
|
||||
shift
|
||||
fi
|
||||
|
||||
TEST=$1
|
||||
|
||||
if [ "$#" -ne 1 ]
|
||||
then
|
||||
usage
|
||||
fi
|
||||
|
||||
TOOLS="vhs ttyd ffmpeg"
|
||||
|
||||
if [ "$UPLOAD" = true ]
|
||||
then
|
||||
TOOLS="$TOOLS gh"
|
||||
fi
|
||||
|
||||
for TOOL in $TOOLS
|
||||
do
|
||||
if ! command -v "$TOOL" > /dev/null 2>&1
|
||||
then
|
||||
echo "$TOOL could not be found"
|
||||
echo "Install it with: brew install $TOOL"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$UPLOAD" = true ]
|
||||
then
|
||||
WORKTREE_PATH=$(git worktree list | grep assets | awk '{print $1}')
|
||||
|
||||
if [ -z "$WORKTREE_PATH" ]
|
||||
then
|
||||
echo "Could not find assets worktree. You'll need to create a worktree for the assets branch using the following command:"
|
||||
echo "git worktree add .worktrees/assets assets"
|
||||
echo "The assets branch has no shared history with the main branch: it exists to store assets which are too large to store in the main branch."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUTPUT_DIR="$WORKTREE_PATH/demo"
|
||||
else
|
||||
OUTPUT_DIR=demo/output
|
||||
fi
|
||||
|
||||
# Get last part of the test path and set that as the output name
|
||||
# example test path: pkg/integration/tests/01_basic_test.go
|
||||
# For that we want: NAME=01_basic_test
|
||||
NAME=$(echo "$TEST" | sed -e 's/.*\///' | sed -e 's/\..*//')
|
||||
|
||||
# Add the demo to the tests list (if missing) so that it can be run
|
||||
go generate pkg/integration/tests/tests.go
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
SCRATCH=$(mktemp -d)
|
||||
trap 'rm -rf "$SCRATCH"' EXIT
|
||||
|
||||
TAPE="$SCRATCH/$NAME.tape"
|
||||
RECORDING="$SCRATCH/$NAME.mp4"
|
||||
OUTPUT="$OUTPUT_DIR/$NAME.mp4"
|
||||
|
||||
# Start recording once lazygit has drawn the top left corner of a view frame.
|
||||
# Demos run in whichever screen mode they ask for, so no particular panel is
|
||||
# on screen for all of them, but every view is drawn with a frame. This is the
|
||||
# corner that `border: rounded` draws; a demo config that turns borders off
|
||||
# would need a different signal.
|
||||
#
|
||||
# The two quotes in the end marker keep the literal VHSDONE out of the command
|
||||
# line that stays on screen while we wait for the marker to be printed.
|
||||
cat > "$TAPE" <<EOF
|
||||
Output "$RECORDING"
|
||||
|
||||
Source demo/settings.tape
|
||||
|
||||
# The command is typed while the recording is hidden, so there is nothing to
|
||||
# gain from animating it.
|
||||
Set TypingSpeed 0ms
|
||||
|
||||
Hide
|
||||
Type "go run cmd/integration_test/main.go cli --slow $TEST; echo VHS''DONE"
|
||||
Enter
|
||||
Wait+Screen@180s /╭/
|
||||
Show
|
||||
Wait+Screen@600s /VHSDONE/
|
||||
Hide
|
||||
EOF
|
||||
|
||||
vhs "$TAPE"
|
||||
|
||||
if [ ! -f "$RECORDING" ]
|
||||
then
|
||||
echo "vhs recorded the demo but wrote no video."
|
||||
echo "vhs 0.12.0 does this; see https://github.com/charmbracelet/vhs/issues/787."
|
||||
echo "Install a working version with: go install github.com/charmbracelet/vhs@v0.11.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The browser draws its playback controls over the bottom of the video, and
|
||||
# they are tall enough to hide lazygit's caption line. Pad the frame so that
|
||||
# the caption sits above them. Chrome draws the tallest bar of the three, and
|
||||
# at the width a README gives the video its buttons start to overlap the
|
||||
# caption below about 90px, so there is not much room to trim here. Measure it
|
||||
# again if demo/settings.tape changes the size of the recording.
|
||||
CAPTION_CLEARANCE=90
|
||||
|
||||
BACKGROUND=$(sed -n 's/.*"background": *"\(#[0-9a-fA-F]*\)".*/\1/p' demo/settings.tape)
|
||||
|
||||
if [ -z "$BACKGROUND" ]
|
||||
then
|
||||
echo "Could not read the background colour from demo/settings.tape"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# vhs keeps recording until the marker reaches the screen, and by then lazygit
|
||||
# has exited and the shell has painted its prompt back over the demo. Find the
|
||||
# moment that happened so we can cut it off. Measure the share of pixels that
|
||||
# are not background: it collapses when lazygit's panels give way to a prompt,
|
||||
# and the reading holds steady from there to the end of the recording.
|
||||
CUT=$(ffmpeg -v error -i "$RECORDING" \
|
||||
-vf "format=gray,lutyuv=y='if(gt(val\,60)\,255\,0)',signalstats,metadata=print:key=lavfi.signalstats.YAVG:file=-" \
|
||||
-an -f null - 2>/dev/null |
|
||||
# ffmpeg writes these numbers with a decimal point, so keep awk in a
|
||||
# locale that reads them back that way. The + 0 turns them from strings
|
||||
# into numbers, without which awk compares them as text.
|
||||
LC_ALL=C awk '/pts_time:/ { split($3, a, ":"); time = a[2] }
|
||||
/YAVG=/ { split($0, b, "="); n++; at[n] = time + 0; ink[n] = b[2] + 0 }
|
||||
END {
|
||||
# Walk back over the frames that read the same as the last one.
|
||||
k = n
|
||||
while (k > 1 && ink[k - 1] < ink[n] * 1.05 && ink[k - 1] > ink[n] * 0.95) {
|
||||
k--
|
||||
}
|
||||
# Only call it a prompt if the screen really did empty out. A
|
||||
# demo that simply ends on a still frame leaves nothing to cut.
|
||||
if (k > 1 && ink[k - 1] > ink[n] * 2) {
|
||||
printf "%.3f\n", at[k]
|
||||
}
|
||||
}')
|
||||
|
||||
if [ -n "$CUT" ]
|
||||
then
|
||||
TRIM="-t $CUT"
|
||||
else
|
||||
TRIM=
|
||||
fi
|
||||
|
||||
# The video ends on the last frame of the demo. A browser goes on showing that
|
||||
# frame once it has played to the end, so it needs no padding out. Move the
|
||||
# moov atom to the front so that playback can start before the whole file has
|
||||
# downloaded.
|
||||
# shellcheck disable=SC2086
|
||||
ffmpeg -y -loglevel error -i "$RECORDING" $TRIM \
|
||||
-vf "pad=iw:ih+$CAPTION_CLEARANCE:0:0:color=$BACKGROUND" \
|
||||
-c:v libx264 -crf 23 -preset slow -pix_fmt yuv420p \
|
||||
-movflags +faststart -an "$OUTPUT"
|
||||
|
||||
if [ "$UPLOAD" = false ]
|
||||
then
|
||||
echo "Demo recorded to $OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# GitHub's web editor posts to this endpoint when you drag a file into a
|
||||
# comment box. It is undocumented, but it accepts an ordinary token, so we can
|
||||
# upload from here. You need push access to $REPO for it to work. If the
|
||||
# endpoint ever goes away, drag the video into a comment box on github.com
|
||||
# instead and copy the URL that GitHub inserts.
|
||||
REPOSITORY_ID=$(gh api "repos/$REPO" --jq .id)
|
||||
|
||||
RESPONSE=$(curl --silent --show-error --fail \
|
||||
--request POST \
|
||||
--header "Authorization: Bearer $(gh auth token)" \
|
||||
--header "Accept: application/json" \
|
||||
--header "Content-Type: video/mp4" \
|
||||
--data-binary "@$OUTPUT" \
|
||||
"https://uploads.github.com/user-attachments/assets?name=$NAME.mp4&content_type=video%2Fmp4&repository_id=$REPOSITORY_ID")
|
||||
|
||||
URL=$(echo "$RESPONSE" | sed -e 's/.*"url":"//' -e 's/".*//')
|
||||
|
||||
if [ -z "$URL" ]
|
||||
then
|
||||
echo "Could not read an attachment URL out of GitHub's response:"
|
||||
echo "$RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# An attachment stays private until a posted comment somewhere in the
|
||||
# repository refers to it. Until that happens the video is a 404 for anyone who
|
||||
# is not signed in, and the README shows a broken player. Referring to it once
|
||||
# makes it public for good, even if the comment is deleted afterwards, so we
|
||||
# collect the recordings in one issue and leave the comments in place.
|
||||
gh api "repos/$REPO/issues/$PUBLISH_ISSUE/comments" \
|
||||
--raw-field "body=$NAME
|
||||
|
||||
$URL" > /dev/null
|
||||
|
||||
# Make sure that worked before handing over a URL, because the person recording
|
||||
# the demo is signed in and will not see the failure.
|
||||
ATTEMPT=0
|
||||
while [ "$ATTEMPT" -lt 30 ]
|
||||
do
|
||||
if curl --silent --fail --output /dev/null --max-time 20 --range 0-1 "$URL"
|
||||
then
|
||||
break
|
||||
fi
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ "$ATTEMPT" -eq 30 ]
|
||||
then
|
||||
echo "$URL is still not readable without signing in to GitHub."
|
||||
echo "Embedding it now would give logged-out readers a broken player."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Demo recorded to $OUTPUT"
|
||||
echo
|
||||
echo "Embed it with:"
|
||||
echo "<video src=\"$URL\" controls></video>"
|
||||
@@ -1,113 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# Re-records the demos that a page embeds and points the page at the new
|
||||
# videos. Use it when a change to demo/settings.tape, or to lazygit's
|
||||
# appearance, leaves the existing recordings looking out of date.
|
||||
#
|
||||
# A page names the demo behind each video in a comment above it:
|
||||
#
|
||||
# <!-- demo: commit_and_push -->
|
||||
# <video src="https://github.com/user-attachments/assets/..." controls></video>
|
||||
#
|
||||
# GitHub drops that comment when it renders the page, so it costs the reader
|
||||
# nothing. This script re-records the demo the comment names and rewrites the
|
||||
# URL on the line below it.
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [--no-upload] [page ...]"
|
||||
echo "e.g. $0 README.md"
|
||||
echo
|
||||
echo "Re-records every demo the given pages embed. With no page given,"
|
||||
echo "that means README.md."
|
||||
echo
|
||||
echo "--no-upload leaves the videos in demo/output and the pages untouched,"
|
||||
echo "which is what you want for reviewing a change to demo/settings.tape."
|
||||
exit 1
|
||||
}
|
||||
|
||||
NO_UPLOAD=
|
||||
|
||||
if [ "$1" = "--no-upload" ]
|
||||
then
|
||||
NO_UPLOAD=--no-upload
|
||||
shift
|
||||
fi
|
||||
|
||||
if [ "$1" = "-h" ] || [ "$1" = "--help" ]
|
||||
then
|
||||
usage
|
||||
fi
|
||||
|
||||
if [ ! -x demo/record_demo.sh ]
|
||||
then
|
||||
echo "Run this from the root of the repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$#" -eq 0 ]
|
||||
then
|
||||
set -- README.md
|
||||
fi
|
||||
|
||||
for PAGE in "$@"
|
||||
do
|
||||
if [ ! -f "$PAGE" ]
|
||||
then
|
||||
echo "$PAGE does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NAMES=$(sed -n 's/^<!-- demo: \([A-Za-z0-9_]*\) -->$/\1/p' "$PAGE")
|
||||
|
||||
if [ -z "$NAMES" ]
|
||||
then
|
||||
echo "$PAGE embeds no demos. Each video needs a <!-- demo: <name> -->"
|
||||
echo "comment on the line above it to say which demo it came from."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for NAME in $NAMES
|
||||
do
|
||||
TEST="pkg/integration/tests/demo/$NAME.go"
|
||||
|
||||
if [ ! -f "$TEST" ]
|
||||
then
|
||||
echo "$PAGE asks for a demo called $NAME, but $TEST does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== $NAME ==="
|
||||
|
||||
if [ -n "$NO_UPLOAD" ]
|
||||
then
|
||||
demo/record_demo.sh --no-upload "$TEST"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Keep the recording chatter on screen, since a full run takes a while,
|
||||
# and read the new URL back out of it afterwards.
|
||||
LOG=$(mktemp)
|
||||
demo/record_demo.sh "$TEST" | tee "$LOG"
|
||||
URL=$(sed -n 's/.*<video src="\([^"]*\)".*/\1/p' "$LOG")
|
||||
rm -f "$LOG"
|
||||
|
||||
if [ -z "$URL" ]
|
||||
then
|
||||
echo "Recording $NAME produced no URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
awk -v marker="<!-- demo: $NAME -->" -v url="$URL" '
|
||||
hit { sub(/src="[^"]*"/, "src=\"" url "\""); hit = 0 }
|
||||
$0 == marker { hit = 1 }
|
||||
{ print }
|
||||
' "$PAGE" > "$PAGE.new"
|
||||
|
||||
mv "$PAGE.new" "$PAGE"
|
||||
|
||||
echo "$PAGE now points at $URL"
|
||||
done
|
||||
done
|
||||
@@ -1,48 +0,0 @@
|
||||
# Terminal settings shared by all demo recordings. The tape that
|
||||
# demo/record_demo.sh generates for a demo sources this file.
|
||||
|
||||
# Use the non-Mono Nerd Font variant; the Mono variant renders the icons too
|
||||
# small. The terminal that vhs records doesn't draw the branch drawing symbols
|
||||
# of the commit graph itself, so they come from the second font, which is in
|
||||
# demo/fonts.
|
||||
Set FontFamily "SauceCodePro NF,Flog Symbols Demo"
|
||||
Set FontSize 24
|
||||
Set LineHeight 1.0
|
||||
Set Padding 20
|
||||
|
||||
# vhs sizes the terminal in pixels rather than in cells, so these two numbers
|
||||
# are what produce a 120x35 grid at the font settings above. If you change the
|
||||
# font, the font size or the padding, record a tape that runs `stty size` and
|
||||
# adjust them until the grid is 120x35 again.
|
||||
Set Width 1866
|
||||
Set Height 1140
|
||||
|
||||
# There is no frame rate setting here because vhs ignores `Set Framerate` when
|
||||
# it writes a video; it captures at 25 fps either way.
|
||||
|
||||
# The frame around the focused view is bold green, and xterm.js draws bold text
|
||||
# in the bright variant of a colour, so brightGreen is the one those frames end
|
||||
# up using.
|
||||
|
||||
Set Theme {
|
||||
"background": "#1d1d1d",
|
||||
"foreground": "#dddad6",
|
||||
"cursor": "#c7c7c7",
|
||||
"selection": "#44475a",
|
||||
"black": "#7a7a7a",
|
||||
"red": "#fc4384",
|
||||
"green": "#3fb950",
|
||||
"yellow": "#ffa727",
|
||||
"blue": "#102895",
|
||||
"magenta": "#c930c7",
|
||||
"cyan": "#00c5c7",
|
||||
"white": "#c7c7c7",
|
||||
"brightBlack": "#676767",
|
||||
"brightRed": "#ff7fac",
|
||||
"brightGreen": "#56d364",
|
||||
"brightYellow": "#ebdf86",
|
||||
"brightBlue": "#6871ff",
|
||||
"brightMagenta": "#ff76ff",
|
||||
"brightCyan": "#5ffdff",
|
||||
"brightWhite": "#fffefe"
|
||||
}
|
||||
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 343 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 443 KiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -1,437 +0,0 @@
|
||||
# Custom Command Keybindings
|
||||
|
||||
You can add custom command keybindings in your config.yml (accessible by pressing 'e' on the status panel from within lazygit) like so:
|
||||
|
||||
```yml
|
||||
customCommands:
|
||||
- key: '<c-r>'
|
||||
context: 'commits'
|
||||
command: 'hub browse -- "commit/{{.SelectedLocalCommit.Hash}}"'
|
||||
- key: 'a'
|
||||
context: 'files'
|
||||
command: "git {{if .SelectedFile.HasUnstagedChanges}} add {{else}} reset {{end}} {{.SelectedFile.Name | quote}}"
|
||||
description: 'Toggle file staged'
|
||||
- key: 'C'
|
||||
context: 'global'
|
||||
command: "git commit"
|
||||
output: terminal
|
||||
- key: 'n'
|
||||
context: 'localBranches'
|
||||
prompts:
|
||||
- type: 'menu'
|
||||
title: 'What kind of branch is it?'
|
||||
key: 'BranchType'
|
||||
options:
|
||||
- name: 'feature'
|
||||
description: 'a feature branch'
|
||||
value: 'feature'
|
||||
- name: 'hotfix'
|
||||
description: 'a hotfix branch'
|
||||
value: 'hotfix'
|
||||
- name: 'release'
|
||||
description: 'a release branch'
|
||||
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'
|
||||
```
|
||||
|
||||
Looking at the command assigned to the 'n' key, here's what the result looks like:
|
||||
|
||||

|
||||
|
||||
Custom command keybindings will appear alongside inbuilt keybindings when you view the keybindings menu by pressing '?':
|
||||
|
||||

|
||||
|
||||
For a given custom command, here are the allowed fields:
|
||||
| _field_ | _description_ | required |
|
||||
|-----------------|----------------------|-|
|
||||
| key | The key to trigger the command. Use a single key or list of keys, as described in [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md). Custom commands without a key specified can be triggered by selecting them from the keybindings (`?`) menu | no |
|
||||
| command | The command to run (using Go template syntax for placeholder values) | yes |
|
||||
| context | The context in which to listen for the key (see [below](#contexts)) | yes |
|
||||
| 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 |
|
||||
| output | Where the output of the command should go. 'none' discards it, 'terminal' suspends lazygit and runs the command in the terminal (useful for commands that require user input), 'log' streams it to the command log, 'logWithPty' is like 'log' but runs the command in a pseudo terminal (can be useful for commands that produce colored output when the output is a terminal), and 'popup' shows it in a popup. | no |
|
||||
| outputTitle | The title to display in the popup panel if output is set to 'popup'. If left unset, the command will be used as the title. | no |
|
||||
| after | Actions to take after the command has completed | no |
|
||||
|
||||
Here are the options for the `after` key:
|
||||
| _field_ | _description_ | required |
|
||||
|-----------------|----------------------|-|
|
||||
| checkForConflicts | true/false. If true, check for merge conflicts | no |
|
||||
|
||||
## Contexts
|
||||
|
||||
The permitted contexts are:
|
||||
|
||||
| _context_ | _description_ |
|
||||
| -------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| status | The 'Status' tab |
|
||||
| files | The 'Files' tab |
|
||||
| worktrees | The 'Worktrees' tab |
|
||||
| submodules | The 'Submodules' 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 |
|
||||
|
||||
> **Bonus**
|
||||
>
|
||||
> You can use a comma-separated string, such as `context: 'commits, subCommits'`, to make it effective in multiple contexts.
|
||||
|
||||
|
||||
## Prompts
|
||||
|
||||
### Common fields
|
||||
|
||||
These fields are applicable to all prompts.
|
||||
|
||||
| _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 |
|
||||
| condition | A Go template expression; if it resolves to empty string or `false`, the prompt is skipped. See [Conditional prompts](#conditional-prompts) | no |
|
||||
|
||||
### 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 'authors', 'branches', 'files', 'refs', 'remotes', 'remoteBranches', 'tags' | 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 |
|
||||
|
||||
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 |
|
||||
| key | Keybinding to invoke this menu option without needing to navigate to it. Use a single key or list of keys, as described in [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md) | no |
|
||||
|
||||
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'
|
||||
```
|
||||
|
||||
Here's an example of supplying keybindings for menu options:
|
||||
|
||||
```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'
|
||||
key: 'f'
|
||||
- value: 'hotfix'
|
||||
name: 'hotfix branch'
|
||||
description: 'branch based off main for fast bug fixes'
|
||||
key: 'h'
|
||||
- value: 'release'
|
||||
name: 'release branch'
|
||||
description: 'branch for a release'
|
||||
key: 'r'
|
||||
```
|
||||
|
||||
In this example, pressing 'f', 'h', or 'r' will directly select the corresponding option without needing to navigate to it first.
|
||||
|
||||
### 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'
|
||||
```
|
||||
|
||||
### Conditional prompts
|
||||
|
||||
Here's an example of a conditional prompt:
|
||||
|
||||
```yml
|
||||
customCommands:
|
||||
- key: 'a'
|
||||
context: 'localBranches'
|
||||
prompts:
|
||||
- type: 'menu'
|
||||
title: 'How do you want to create the branch?'
|
||||
key: 'Method'
|
||||
options:
|
||||
- value: 'simple'
|
||||
name: 'Simple'
|
||||
description: 'just a branch name'
|
||||
- value: 'prefix'
|
||||
name: 'With prefix'
|
||||
description: 'with a category prefix'
|
||||
- type: 'menu'
|
||||
title: 'Branch prefix'
|
||||
key: 'Prefix'
|
||||
condition: '{{ eq .Form.Method "prefix" }}'
|
||||
options:
|
||||
- value: 'feature/'
|
||||
- value: 'hotfix/'
|
||||
- value: 'release/'
|
||||
- type: 'input'
|
||||
title: 'Branch name'
|
||||
key: 'Name'
|
||||
command: "git checkout -b '{{.Form.Prefix}}{{.Form.Name}}'"
|
||||
```
|
||||
|
||||
In this example the 'Branch prefix' menu only appears if the user chose 'With prefix'. Otherwise it is skipped and `.Form.Prefix` defaults to empty string.
|
||||
|
||||
## 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:
|
||||
|
||||
```
|
||||
SelectedCommit
|
||||
SelectedCommitRange
|
||||
SelectedFile
|
||||
SelectedPath
|
||||
SelectedSubmodule
|
||||
SelectedLocalBranch
|
||||
SelectedRemoteBranch
|
||||
SelectedRemote
|
||||
SelectedTag
|
||||
SelectedStashEntry
|
||||
SelectedCommitFile
|
||||
SelectedWorktree
|
||||
CheckedOutBranch
|
||||
```
|
||||
|
||||
(For legacy reasons, `SelectedLocalCommit`, `SelectedReflogCommit`, and `SelectedSubCommit` are also available, but they are deprecated.)
|
||||
|
||||
|
||||
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/services/custom_commands/models.go) (all the modelling lives in the same file).
|
||||
|
||||
We don't support accessing all elements of a range selection yet. We might add this in the future, but as a special case you can access the range of selected commits by using `SelectedCommitRange`, which has two properties `.To` and `.From` which are the hashes of the bottom and top selected commits, respectively. This is useful for passing them to a git command that operates on a range of commits. For example, to create patches for all selected commits, you might use
|
||||
```yml
|
||||
command: "git format-patch {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}}"
|
||||
```
|
||||
|
||||
We support the following functions:
|
||||
|
||||
### Quoting
|
||||
|
||||
Quote wraps a string in quotes with necessary escaping for the current platform.
|
||||
|
||||
```
|
||||
git {{.SelectedFile.Name | quote}}
|
||||
```
|
||||
|
||||
### Running a command
|
||||
|
||||
Runs a command and returns the output. If the command outputs more than a single line, it will produce an error.
|
||||
|
||||
```
|
||||
initialValue: "username/{{ runCommand "date +\"%Y/%-m\"" }}/"
|
||||
```
|
||||
|
||||
## 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)
|
||||
|
||||
## Menus of custom commands
|
||||
|
||||
For custom commands that are not used very frequently it may be preferable to hide them in a menu; you can assign a key to open the menu, and the commands will appear inside. This has the advantage that you don't have to come up with individual unique keybindings for all those commands that you don't use often; the keybindings for the commands in the menu only need to be unique within the menu. Here is an example:
|
||||
|
||||
```yml
|
||||
customCommands:
|
||||
- key: X
|
||||
description: "Copy/paste commits across repos"
|
||||
commandMenu:
|
||||
- key: c
|
||||
command: 'git format-patch --stdout {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}} | pbcopy'
|
||||
context: commits, subCommits
|
||||
description: "Copy selected commits to clipboard"
|
||||
- key: v
|
||||
command: 'pbpaste | git am'
|
||||
context: "commits"
|
||||
description: "Paste selected commits from clipboard"
|
||||
```
|
||||
|
||||
If you use the commandMenu property, none of the other properties except key and description can be used.
|
||||
|
||||
## Debugging
|
||||
|
||||
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `output: popup` so that it doesn't actually execute the command but you can see how the placeholders were resolved.
|
||||
|
||||
## More Examples
|
||||
|
||||
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!
|
||||
@@ -1,93 +0,0 @@
|
||||
# Custom Diff Renderers
|
||||
|
||||
Custom diff renderers are useful for showing a better rendering of a diff than git's builtin raw diff, and using one is strongly recommended (I personally prefer delta myself, but that's a matter of personal preference). There are three types of diff renderers that lazygit supports:
|
||||
|
||||
- **stdin filters**, e.g. [delta](#delta) and [diff-so-fancy](#diff-so-fancy). They take git's raw output as stdin and produce something nicer on stdout, and they are hooked up using git's GIT_PAGER mechanism. (These used to be called "custom pagers" in earlier lazygit versions.)
|
||||
- **external diff programs**, e.g. difftastic; these are called using git's `--ext-diff` flag, and they take over diff generation from git completely rather than post-processing git's output.
|
||||
- **git's raw output using custom arguments**; mainly useful for `--color-words` (or `--word-diff` if you are color blind).
|
||||
|
||||
Diff renderers are configured with the `diffRenderers` array in the `git` section of lazygit's config file; it is an array because you can have multiple entries that you can cycle through with the `|` key. This can be useful if you usually prefer a particular diff renderer, but want to use a different one for certain kinds of diffs.
|
||||
|
||||
Fields that are shared by all renderer types:
|
||||
|
||||
- **type** The type of diff renderer; choices are `stdinFilter`, `extDiff`, or `rawGit`. `stdinFilter` is the default, because it's the most common one; so you can omit this if you use delta.
|
||||
- **name** A name that is shown in the status bar toast when cycling renderers; defaults to the first word of the renderer command, but can be useful e.g. to distinguish "delta" from "delta side-by-side" if you have entries for both.
|
||||
|
||||
Fields only for `stdinFilter`:
|
||||
|
||||
- **command** The command line to use for `GIT_PAGER`.
|
||||
|
||||
- **colorArg** whether you want the `--color=always` arg in your `git diff` command. Some diff renderers want it set to `always`, others want it set to `never`. The default is `always`, since that's what most renderers need.
|
||||
|
||||
Fields only for `extDiff`:
|
||||
|
||||
- **command** The command line to use for the `diff.external` git config. If left empty, it uses the global value of git's `diff.external` config; this can be useful if you also want to use it for diffs on the command line, and it also has the advantage that you can configure it per file type in `.gitattributes`; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
|
||||
|
||||
Fields only for `rawGit`:
|
||||
|
||||
- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`), as an array of strings.
|
||||
|
||||
The `command` of a `stdinFilter` or `extDiff` renderer is a [Go template](https://pkg.go.dev/text/template) with these variables:
|
||||
|
||||
- `{{width}}`: the width of the view that the diff is rendered into.
|
||||
- `{{colorScheme}}`: `dark` or `light`, depending on whether the terminal has a dark or a light background. Lazygit asks the terminal about this; if yours doesn't tell, set `gui.colorScheme`.
|
||||
- `{{columnWidth}}` (only for `stdinFilter`): the width of one side of a side-by-side rendering, e.g. for `ydiff -p cat -s -w {{columnWidth}}`.
|
||||
- `{{diffContext}}` (only for `extDiff`): lazygit's current diff context size, the value controlled by the `{`/`}` keybindings.
|
||||
|
||||
A variable can also be written with a leading dot, as in `{{.width}}`. The command can use template expressions too; for example, `delta --paging=never {{if gt .width 160}}--side-by-side{{end}}` shows the diff side by side only when there is room for it, or `delta --syntax-theme={{if eq .colorScheme "light"}}Github{{else}}Dracula{{end}}` picks a different syntax theme based on the background.
|
||||
|
||||
Here's an example for a multi-renderer setup:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
diffRenderers:
|
||||
- command: delta --{{colorScheme}} --paging=never
|
||||
- command: ydiff -p cat
|
||||
colorArg: never
|
||||
- type: extDiff
|
||||
command: difft --color=always --background={{colorScheme}} --context={{diffContext}}
|
||||
- type: rawGit
|
||||
args: [--color-words]
|
||||
name: color-words
|
||||
- type: rawGit # git's default diff
|
||||
name: default
|
||||
```
|
||||
|
||||
## Delta:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
diffRenderers:
|
||||
- command: delta --{{colorScheme}} --paging=never
|
||||
```
|
||||
|
||||

|
||||
|
||||
`--{{colorScheme}}` passes `--dark` or `--light` to delta, so that it matches the background of your terminal.
|
||||
|
||||
A cool feature of delta is --hyperlinks, which renders clickable links for the line numbers in the left margin, and lazygit supports these. To use them, set the `command:` field to `delta --{{colorScheme}} --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"`; this allows you to click on an underlined line number in the diff to jump right to that same line in your editor.
|
||||
|
||||
Note that delta's `--navigate` option doesn't work in lazygit, for technical reasons.
|
||||
|
||||
## Diff-so-fancy
|
||||
|
||||
```yaml
|
||||
git:
|
||||
diffRenderers:
|
||||
- command: diff-so-fancy
|
||||
```
|
||||
|
||||

|
||||
|
||||
## ydiff
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
sidePanelWidth: 0.2 # gives you more space to show things side-by-side
|
||||
git:
|
||||
diffRenderers:
|
||||
- colorArg: never
|
||||
command: ydiff -p cat
|
||||
```
|
||||
|
||||

|
||||
@@ -1,65 +0,0 @@
|
||||
# Fixup Commits
|
||||
|
||||
## Background
|
||||
|
||||
There's this common scenario that you have a PR in review, the reviewer is
|
||||
requesting some changes, and you make those changes and would normally simply
|
||||
squash them into the original commit that they came from. If you do that,
|
||||
however, there's no way for the reviewer to see what you changed. You could just
|
||||
make a separate commit with those changes at the end of the branch, but this is
|
||||
not ideal because it results in a git history that is not very clean.
|
||||
|
||||
To help with this, git has a concept of fixup commits: you do make a separate
|
||||
commit, but the subject of this commit is the string "fixup! " followed by the
|
||||
original commit subject. This both tells the reviewer what's going on (you are
|
||||
making a change that you later will squash into the designated commit), and it
|
||||
provides an easy way to actually perform this squash operation when you are
|
||||
ready to do that (before merging).
|
||||
|
||||
## Creating fixup commits
|
||||
|
||||
You could of course create fixup commits manually by typing in the commit
|
||||
message with the prefix yourself. But lazygit has an easier way to do that:
|
||||
in the Commits view, select the commit that you want to create a fixup for, and
|
||||
press shift-F (for "Create fixup commit for this commit"). This automatically
|
||||
creates a commit with the appropriate subject line.
|
||||
|
||||
Don't confuse this with the lowercase "f" command ("Fixup commit"); that one
|
||||
squashes the selected commit into its parent, this is not what we want here.
|
||||
|
||||
## Creating amend commits
|
||||
|
||||
There's a special type of fixup commit that uses "amend!" instead of "fixup!" in
|
||||
the commit message subject; in addition to fixing up the original commit with
|
||||
changes it allows you to also (or only) change the commit message of the
|
||||
original commit. The menu that appears when pressing shift-F has options for
|
||||
both of these; they bring up a commit message panel similar to when you reword a
|
||||
commit, but then create the "amend!" commit containing the new message. Note
|
||||
that in that panel you only type the new message as you want it to be
|
||||
eventually; lazygit then takes care of formatting the "amend!" commit
|
||||
appropriately for you (with the subject of your new message moving into the body
|
||||
of the "amend!" commit).
|
||||
|
||||
## Squashing fixup commits
|
||||
|
||||
When you're ready to merge the branch and want to squash all these fixup commits
|
||||
that you created, that's very easy to do: select the first commit of your branch
|
||||
and hit shift-S (for "Squash all 'fixup!' commits above selected commit
|
||||
(autosquash)"). Boom, done.
|
||||
|
||||
## Finding the commit to create a fixup for
|
||||
|
||||
When you are making changes to code that you changed earlier in a long branch,
|
||||
it can be tedious to find the commit to squash it into. Lazygit has a command to
|
||||
help you with this, too: in the Files view, press ctrl-f to select the right
|
||||
base commit in the Commits view automatically. From there, you can either press
|
||||
shift-F to create a fixup commit for it, or shift-A to amend your changes into
|
||||
the commit if you haven't published your branch yet.
|
||||
|
||||
If you have many modifications in your working copy, it is a good idea to stage
|
||||
related changes that are meant to go into the same fixup commit; if no changes
|
||||
are staged, ctrl-f works on all unstaged modifications, and then it might show
|
||||
an error if it finds multiple different base commits. If you are interested in
|
||||
what the command does to do its magic, and how you can help it work better, you
|
||||
may want to read the [design document](dev/Find_Base_Commit_For_Fixup_Design.md)
|
||||
that describes this.
|
||||
@@ -1,11 +0,0 @@
|
||||
# Documentation Overview
|
||||
|
||||
* [Configuration](./Config.md).
|
||||
* [Custom Commands](./Custom_Command_Keybindings.md)
|
||||
* [Custom Diff Renderers](./Custom_DiffRenderers.md)
|
||||
* [Dev docs](./dev)
|
||||
* [Keybindings](./keybindings)
|
||||
* [Undo/Redo](./Undoing.md)
|
||||
* [Range Select](./Range_Select.md)
|
||||
* [Searching/Filtering](./Searching.md)
|
||||
* [Stacked Branches](./Stacked_Branches.md)
|
||||
@@ -1,14 +0,0 @@
|
||||
# Range Select
|
||||
|
||||
Some actions can be performed on a range of contiguous items. For example:
|
||||
* staging multiple files at once
|
||||
* squashing multiple commits at once
|
||||
* copying (for cherry-pick) multiple commits at once
|
||||
|
||||
There are two ways to select a range of items:
|
||||
1. Sticky range select: Press 'v' to toggle range select, then expand the selection using the up/down arrow key. To reset the selection, press 'v' again.
|
||||
2. Non-sticky range select: Press shift+up or shift+down to expand the selection. To reset the selection, press up/down without shift.
|
||||
|
||||
The sticky option will be more familiar to vim users, and the second option will feel more natural to users who aren't used to doing things in a modal way.
|
||||
|
||||
In order to perform an action on a range of items, simply press the normal key for that action. If the action only works on individual items, it will raise an error. This is a new feature and the plan is to incrementally support range select for more and more actions. If there is an action you would like to support range select which currently does not, please raise an issue in the repo.
|
||||
@@ -1,25 +0,0 @@
|
||||
# Searching/Filtering
|
||||
|
||||
## View searching/filtering
|
||||
|
||||
Depending on the currently focused view, hitting '/' will bring up a filter or search prompt. When filtering, the contents of the view will be filtered down to only those lines which match the query string. When searching, the contents of the view are not filtered, but matching lines are highlighted and you can iterate through matches with `n`/`N`.
|
||||
|
||||
In the commits view we don't filter, but search; this is deliberate because you typically care about the commits that come before/after a matching commit.
|
||||
|
||||
If you would like both filtering and searching to be enabled on a given view, please raise an issue for this.
|
||||
|
||||
## Menu filtering
|
||||
|
||||
The keybindings (`?`) and recent repositories menus can be filtered simply by typing. The filter field appears at the bottom of the menu while you type; there is no need to press `/` or confirm the filter before navigating the results.
|
||||
|
||||
## Filtering files by status
|
||||
|
||||
You can filter the files view to only show staged/unstaged files by pressing `<c-b>` in the files view.
|
||||
|
||||
## Filtering commits by file path
|
||||
|
||||
You can filter the commits view to only show commits which contain changes to a given file path.
|
||||
|
||||
You can do this in a couple of ways:
|
||||
1) Start lazygit with the -f flag e.g. `lazygit -f my/path`
|
||||
2) From within lazygit, press `<c-s>` and then enter the path of the file you want to filter by
|
||||