Lazygit is a terminal user interface (TUI) for git written in Go. It wraps the git CLI with an interactive, keyboard-driven interface that handles staging, committing, branching, rebasing, merge conflict resolution, cherry-picking, bisecting, worktree management, and more. This guide covers architecture, configuration internals, deployment, security considerations, developer workflows, and the full reference surface for administrators and contributors.
1. Architecture Overview
What the System Does
Lazygit is a single-binary Go application that renders a multi-panel terminal UI using the gocui TUI library (backed by tcell). It does not implement its own git logic; instead, it constructs and executes git CLI commands as subprocesses, parses their output, and presents the results in interactive panels. The application manages state through a combination of in-memory models, filesystem-based git state (.git/ directory), and a persisted YAML state file.
The architecture follows a controller-context-command layered pattern: controllers handle user input and business logic, contexts manage panel state and focus, and command structs encapsulate all git subprocess interactions.
Architecture Diagram
+-----------------------------------------------------------------+
| main.go (LDFLAGS) |
| | |
| app.Start() |
| | |
| +------------------+------------------+ |
| | | |
| pkg/config pkg/app |
| (YAML config, (CLI parsing via |
| migrations, flaggy, bootstrap, |
| editor presets) git version check) |
| | | |
| +------------------+------------------+ |
| | |
| pkg/gui |
| (gocui TUI engine) |
| +------------------+------------------+ |
| | | | |
| gui/controllers gui/context gui/services |
| (64 controllers) (panel state, (custom commands, |
| focus mgmt) background tasks) |
| | |
| pkg/commands |
| (GitCommand facade) |
| | |
| pkg/commands/git_commands |
| (22 command structs + 9 loaders) |
| | |
| oscommands.ICmdObjBuilder |
| (subprocess execution) |
| | |
| git CLI |
+-----------------------------------------------------------------+
Component Inventory
| Component | Path | Role |
|---|---|---|
| Entry point | main.go |
Sets LDFLAGS build info, calls app.Start() |
| CLI parser | pkg/app/entry_point.go |
14 CLI flags via flaggy library |
| App bootstrap | pkg/app/app.go |
Config load, git version check, repo setup, GUI init |
| Configuration | pkg/config/ |
YAML config loading, validation, automatic migrations, editor presets |
| GUI engine | pkg/gui/ |
gocui-based TUI: layout, views, event loop, background routines |
| Controllers | pkg/gui/controllers/ |
64 controllers, one per feature area (files, branches, commits, etc.) |
| Contexts | pkg/gui/context/ |
Panel state management, focus tracking, view-to-context mapping |
| Custom commands | pkg/gui/services/custom_commands/ |
User-defined commands with Go template prompts |
| Git facade | pkg/commands/git.go |
GitCommand struct aggregating all command structs |
| Git commands | pkg/commands/git_commands/ |
22 command structs (Branch, Commit, Rebase, Sync, etc.) |
| Git loaders | pkg/commands/git_commands/ |
9 data loaders (BranchLoader, CommitLoader, etc.) |
| Models | pkg/commands/models/ |
Data models: Commit, Branch, File, Remote, Tag, etc. |
| Patch system | pkg/commands/patch/ |
Interactive patch building for partial staging/unstaging |
| i18n | pkg/i18n/ |
9 languages via Crowdin (en, zh-CN, zh-TW, pl, nl, ja, ko, ru, pt) |
| Theming | pkg/theme/ |
Color attribute parsing, theme management |
| Utilities | pkg/utils/ |
Rebase-todo manipulation, string helpers, fuzzy matching |
| Update checker | pkg/updates/ |
GitHub release polling, self-update (goreleaser builds) |
| Constants | pkg/constants/ |
URL links for docs, issues, discussions, releases |
| Schema | schema/ |
JSON Schema for config validation |
| Integration tests | pkg/integration/ |
Full TUI-driven test framework with robot driver |
| Demo scripts | demo/ |
Automated demo recordings |
| Snake game | pkg/snake/ |
Easter egg built into the TUI |
Technology Stack
| Layer | Technology | Version |
|---|---|---|
| Language | Go | 1.25.0 |
| TUI framework | gocui (jesseduffield fork) | v0.3.1+ |
| Terminal backend | tcell | v2.13.8 |
| CLI parsing | flaggy | v1.8.0 |
| Config format | YAML | gopkg.in/yaml.v3 |
| XDG paths | adrg/xdg | v0.5.3 |
| Clipboard | atotto/clipboard | v0.1.4 |
| Filesystem | spf13/afero | v1.15.0 |
| Logging | logrus | v1.9.3 |
| GitHub integration | cli/go-gh | v2.13.0 |
| Fuzzy search | sahilm/fuzzy + fuzzy-patricia | v0.1.1 |
| Diff display | gookit/color | v1.4.2 |
| Deadlock detection | sasha-s/go-deadlock | v0.3.9 |
Data Flow
- Startup:
main.gopasses LDFLAGS build info toapp.Start(). Entry point parses CLI flags via flaggy. Config loads from YAML (with automatic migrations). Git version is validated (>= 2.32.0). Repo paths are resolved. - GUI initialization:
gui.NewGui()creates the gocui instance, registers all controllers and contexts, starts background routines (auto-fetch, auto-refresh). - User interaction: Keypresses are dispatched through gocui to the active context's controller. Controllers invoke git command structs to execute operations.
- Git operations: Command structs construct
gitCLI arguments viaNewGitCmd()builder, execute them as subprocesses, and parse output. Results update in-memory models. - Refresh cycle: After each operation, affected views are refreshed by re-running the appropriate loaders (BranchLoader, CommitLoader, etc.).
- Background: Auto-fetch runs on a configurable interval (
refresher.fetchInterval, default 60s). Auto-refresh watches for file changes (refresher.refreshInterval, default 10s).
Note
Lazygit does not embed or link against libgit2. All git operations are performed by shelling out to the
gitCLI binary. This means the system's installed git version directly determines available features and behavior.
2. System Requirements
| Requirement | Detail |
|---|---|
| Git | >= 2.32.0 (validated at startup; exits with error if older) |
| OS | Linux, macOS, Windows, FreeBSD |
| Terminal | Any terminal emulator supporting ANSI escape codes; 256-color or true-color recommended |
| Go (build only) | 1.25.0 |
| Disk | Minimal; single binary ~20MB |
| Memory | Depends on repository size; typical usage 50-200MB |
Important
The minimum git version of 2.32.0 is enforced at startup via
app.validateGitVersion(). If you see "Please upgrade to git 2.32.0 or later", this is a hard requirement, not a suggestion.
Optional Dependencies
| Feature | Requires |
|---|---|
| git-flow commands | git-flow installed and initialized in repo |
| GitHub PR integration | GitHub CLI auth token (via go-gh library), or network access to GitHub GraphQL API |
| Custom pagers | delta, diff-so-fancy, or any pager binary in $PATH |
| External diff tools | difftastic, meld, or tool of choice |
3. Configuration Reference
Config File Locations
Lazygit resolves its configuration file in this order:
LG_CONFIG_FILEenvironment variable (comma-separated list of paths)CONFIG_DIRenvironment variable followed by$CONFIG_DIR/config.yml- XDG legacy path:
$XDG_CONFIG_HOME/jesseduffield/lazygit/config.yml - XDG standard path:
$XDG_CONFIG_HOME/lazygit/config.yml(default:~/.config/lazygit/config.yml)
If no config file exists at the resolved path, lazygit creates an empty one automatically.
Tip
Use
LG_CONFIG_FILEto load multiple config files in order. Values in later files override earlier ones, exceptcustomCommandswhich are appended.
Config Files Table
| File | Format | Location | Purpose |
|---|---|---|---|
config.yml |
YAML | ~/.config/lazygit/ (XDG) |
Primary user configuration |
state.yml |
YAML | ~/.local/state/lazygit/ (XDG) |
Persisted app state (recent repos, PR cache) |
.lazygit.yml |
YAML | Repository root | Per-repo config overlay (loaded after global) |
Environment Variables
| Variable | Type | Default | Description | Required? |
|---|---|---|---|---|
LG_CONFIG_FILE |
string | (none) | Comma-separated list of config file paths | No |
CONFIG_DIR |
string | (none) | Override config directory | No |
LAZYGIT_LOG_PATH |
string | (none) | Override log file path | No |
GIT_DIR |
string | (none) | Override git directory | No |
GIT_WORK_TREE |
string | (none) | Override working tree path | No |
DEBUG |
bool | false |
Enable debug logging | No |
LOG_LEVEL |
string | (none) | Set log verbosity | No |
GIT_EDITOR |
string | (none) | Git editor override (checked before VISUAL/EDITOR) |
No |
VISUAL |
string | (none) | Visual editor fallback | No |
EDITOR |
string | (none) | Editor fallback | No |
CONTAINER |
string | (none) | Container detection for Linux config paths | No |
SHOW_RECENT_REPOS |
bool | false |
Force recent repos prompt (testing) | No |
FISH_VERSION |
string | (none) | Detected by editor preset logic for fish shell | No |
NU_VERSION |
string | (none) | Detected by editor preset logic for nushell | No |
Test/CI-Only Environment Variables
| Variable | Purpose |
|---|---|
LAZYGIT_HEADLESS |
Enable headless mode for integration tests |
INPUT_DELAY |
Milliseconds between simulated keypresses |
PARALLEL_TOTAL |
Total parallel test shards |
PARALLEL_INDEX |
Current shard index |
LAZYGIT_RACE_DETECTOR |
Enable Go race detector for tests |
LAZYGIT_GOCOVERDIR |
Code coverage output directory |
CLI Flags
Lazygit uses the flaggy library (not Cobra) for CLI argument parsing.
| Flag | Type | Default | Description |
|---|---|---|---|
-d, --debug |
bool | false |
Run in debug mode |
-v, --version |
bool | false |
Print version info |
-p, --path |
string | . |
Path to git repo |
-f, --filter |
string | (none) | Filter files by path |
-g, --git-dir |
string | (none) | Override $GIT_DIR |
-w, --work-tree |
string | (none) | Override $GIT_WORK_TREE |
--use-config-dir |
string | (none) | Override config directory |
--use-config-file |
string | (none) | Comma-separated config files |
-l, --log |
bool | false |
Tail the lazygit log (requires debug mode) |
--print-config-dir |
bool | false |
Print config directory and exit |
--print-default-config |
bool | false |
Print default config and exit |
Positional argument: lazygit <status|branch|log|stash> opens the corresponding panel on startup.
Automatic Config Migrations
Lazygit automatically migrates config files on startup when deprecated keys are detected. The following migrations are applied:
| Old Key | New Key | Migration Type |
|---|---|---|
gui.skipUnstageLineWarning |
gui.skipDiscardChangeWarning |
Rename |
keybinding.universal.executeCustomCommand |
keybinding.universal.executeShellCommand |
Rename |
gui.windowSize |
gui.screenMode |
Rename |
keybinding.files.openMergeTool |
keybinding.files.openMergeOptions |
Rename |
null keybindings |
<disabled> |
Value change |
git.commitPrefix (object) |
git.commitPrefix (array) |
Structure change |
git.commitPrefixes (objects) |
git.commitPrefixes (arrays) |
Structure change |
subprocess/stream/showOutput |
output enum |
Custom command field merge |
git.allBranchesLogCmd |
git.allBranchesLogCmds (array) |
Singular to plural |
git.paging (object) |
git.pagers (array) |
Structure change |
Warning
Config migrations write back to disk automatically. If you use version-controlled config files, expect automatic modifications on first run after upgrading lazygit. The migration output is printed to stdout before the GUI starts.
Editor Presets
Lazygit ships with 16+ editor presets configured in pkg/config/editor_presets.go:
| Preset | Editor | Edit Command | Edit at Line |
|---|---|---|---|
vi |
vi | vi -- {{filename}} |
vi +{{line}} -- {{filename}} |
vim |
vim | vim -- {{filename}} |
vim +{{line}} -- {{filename}} |
nvim |
Neovim | nvim -- {{filename}} |
nvim +{{line}} -- {{filename}} |
nvim-remote |
Neovim (remote) | nvim --server "$NVIM" --remote-tab {{filename}} |
(with +{{line}}) |
lvim |
LunarVim | lvim -- {{filename}} |
lvim +{{line}} -- {{filename}} |
emacs |
Emacs | emacs -- {{filename}} |
emacs +{{line}} -- {{filename}} |
micro |
micro | micro -- {{filename}} |
micro +{{line}} -- {{filename}} |
nano |
nano | nano -- {{filename}} |
nano +{{line}} -- {{filename}} |
kakoune |
Kakoune | kak -- {{filename}} |
kak +{{line}} -- {{filename}} |
helix |
Helix | hx -- {{filename}} |
hx {{filename}}:{{line}} |
vscode |
VS Code | code --reuse-window -- {{filename}} |
code ... --goto {{filename}}:{{line}} |
sublime |
Sublime Text | subl -- {{filename}} |
subl -- {{filename}}:{{line}} |
bbedit |
BBEdit | bbedit -- {{filename}} |
bbedit +{{line}} -- {{filename}} |
xcode |
Xcode | open -a Xcode {{filename}} |
(same) |
zed |
Zed | zed -- {{filename}} |
zed {{filename}}:{{line}} |
acme |
Acme | acme -- {{filename}} |
acme -- {{filename}}:{{line}} |
Custom Commands Schema
Custom commands are defined in config.yml under customCommands: and support the following fields:
| Field | Type | Description |
|---|---|---|
key |
string | Keybinding to trigger the command |
command |
string | Shell command (Go templates supported) |
context |
string | Panel context where this command is available |
description |
string | Human-readable description |
output |
enum | none (default), terminal, log, logWithPty, popup |
prompts |
array | Interactive prompts before execution |
loadingText |
string | Status bar text while running |
after |
object | Post-execution hooks |
commandMenu |
array | Sub-menu of commands (mutually exclusive with other fields except key and description) |
Output enum values:
| Value | Behavior |
|---|---|
none |
Run silently, refresh UI afterward |
terminal |
Suspend TUI and run in terminal (replaces old subprocess: true) |
log |
Stream output to command log panel |
logWithPty |
Stream with pseudo-terminal (preserves colors) |
popup |
Show output in popup dialog |
4. Deployment & Scaling
Build from Source
go install github.com/jesseduffield/lazygit@latest
Or build with version info:
go build -ldflags "\
-X main.version=0.44.1 \
-X main.commit=$(git rev-parse HEAD) \
-X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
-X main.buildSource=source" \
-o lazygit
Distribution Methods
Lazygit is distributed as a single static binary. Primary distribution channels:
| Method | Command |
|---|---|
| Homebrew (macOS/Linux) | brew install lazygit |
| Scoop (Windows) | scoop install lazygit |
| Chocolatey (Windows) | choco install lazygit |
| Arch Linux | pacman -S lazygit |
| Nix | nix-env -iA nixpkgs.lazygit |
| Go install | go install github.com/jesseduffield/lazygit@latest |
| Binary release | GitHub Releases (goreleaser) |
Note
The
buildSourceLDFLAGS value indicates how the binary was built. Goreleaser builds set it tobinaryBuild. This affects the update checker behavior.
Self-Update Mechanism
The pkg/updates/ module checks GitHub releases periodically. When a newer version is found, lazygit prompts the user to update. The update mechanism downloads the new binary and replaces the current one. This only works for goreleaser-built binaries (where buildSource == "binaryBuild"). Package-manager installs should be updated through their respective package managers.
5. Operations
State Management
Lazygit persists runtime state in state.yml (XDG state directory):
| Field | Type | Purpose |
|---|---|---|
LastUpdateCheck |
int64 | Unix timestamp of last update check |
RecentRepos |
string[] | Recently opened repository paths |
StartupPopupVersion |
int | Tracks which startup popups have been shown |
DidShowHunkStagingHint |
bool | Whether hunk staging hint was shown |
LastVersion |
string | Last lazygit version used (for release notes) |
ShellCommandsHistory |
string[] | Shell command history (in YAML: customcommandshistory) |
HideCommandLog |
bool | Whether command log panel is hidden |
GithubPullRequests |
map | Cached PR data per repo path |
Tip
The
GithubPullRequestscache allows PR info to display instantly on startup before the async GitHub API refresh completes. This cache is keyed by repo path.
Background Routines
The BackgroundRoutineMgr in pkg/gui/background.go manages two primary background routines:
- Auto-fetch (
git.autoFetch: true): Runsgit fetchatrefresher.fetchIntervalseconds (default: 60). An immediate fetch is triggered on startup and when switching repos. - Auto-refresh (
git.autoRefresh: true): Polls the working tree for changes atrefresher.refreshIntervalseconds (default: 10).
Both routines pause when the GUI is suspended (e.g., during subprocess execution). In debug mode, memory usage is logged every 10 seconds.
Note
Setting
refresher.fetchIntervalorrefresher.refreshIntervalto 0 or negative disables the respective background routine and logs a warning.
Logging
| Mode | Log Path | Content |
|---|---|---|
| Production | (none) | No file logging; only fatal errors to stderr |
Debug (-d) |
$LAZYGIT_LOG_PATH or ~/.local/state/lazygit/development.log |
Verbose logrus output |
Tail logs in a separate terminal:
lazygit --debug &
lazygit --log
Command Log
All git commands executed by lazygit are logged to the built-in command log panel (toggle visibility with the backtick key). Each entry shows the action name and the exact git command with arguments. This is invaluable for understanding what lazygit did and for debugging unexpected behavior.
6. Performance
Refresh Strategy
Lazygit uses a targeted refresh system. Each operation declares which views need refreshing via RefreshOptions.Scope:
| Scope | What's Refreshed |
|---|---|
FILES |
Working tree status, staged/unstaged files |
BRANCHES |
Branch list, current branch |
COMMITS |
Commit log, commit graph |
REMOTES |
Remote tracking branches |
TAGS |
Tag list |
PULL_REQUESTS |
GitHub PR data |
STASH |
Stash entries |
This avoids full-UI refreshes after every operation. The refresh mode can be SYNC (blocking) or ASYNC (non-blocking).
Large Repository Considerations
- Commit loading: Uses
git logwith--onelineand pagination. Large histories load incrementally. - File tree: Can be displayed flat or as a tree. Flat mode is faster for repos with many files.
- Auto-fetch: On very large remotes, background fetch can cause noticeable pauses. Increase
refresher.fetchIntervalor setgit.autoFetch: false. - Pagers: External pagers like
deltaadd overhead per diff. For very large diffs, consider disabling custom pagers.
Tip
If lazygit feels sluggish on a large repo, start by increasing
refresher.fetchIntervalto 300 (5 minutes) and check if auto-refresh is the bottleneck by temporarily settinggit.autoRefresh: false.
7. Security
Subprocess Execution
Lazygit executes git commands as subprocesses. All command arguments are constructed programmatically via the NewGitCmd() builder pattern, which prevents shell injection. Commands are not passed through a shell interpreter unless explicitly requested by the user (custom commands, shell commands).
Caution
Custom commands (
customCommandsin config) and shell commands (:prompt) execute arbitrary shell commands with the user's permissions. Treat config files from untrusted sources as potentially dangerous, especially per-repo.lazygit.ymlfiles in cloned repositories.
Credential Handling
Lazygit delegates all credential handling to git's built-in credential system. It does not store, cache, or manage credentials directly. Authentication for push/pull/fetch operations uses whatever credential helper git is configured with (credential.helper).
GitHub Integration
The GitHub PR feature uses the go-gh library to query the GitHub GraphQL API. Authentication uses tokens from the GitHub CLI (gh auth). PR data is cached in state.yml per repo path.
Warning
The GitHub PR fetch runs automatically on startup and refresh. If you work with repositories where you lack GitHub access (e.g., submodules in different organizations), this can cause 401 errors. See issue #5516. There is currently no config option to disable PR fetching per-repo.
Hosting Service Detection
Lazygit detects the hosting service from remote URLs to generate correct web links (for opening PRs, issues, etc.). Supported services:
| Service | URL Pattern |
|---|---|
| GitHub | github.com |
| GitLab | gitlab.com |
| Bitbucket | bitbucket.org |
| Azure DevOps | dev.azure.com, ssh.dev.azure.com |
| Gitea | Configurable via services config |
| Codeberg | codeberg.org |
| Gogs | Configurable via services config |
8. Developer Internals
Repository Structure
lazygit/
main.go Entry point (LDFLAGS)
cmd/
i18n/main.go i18n JSON export tool
integration_test/main.go Integration test runner
pkg/
app/ App bootstrap, CLI parsing, error handling
commands/
git.go GitCommand facade (aggregates all command structs)
git_commands/ 22 command structs + 9 loaders + models
hosting_service/ URL parsing for GitHub/GitLab/Bitbucket/etc.
models/ Data models (Commit, Branch, File, Remote, etc.)
oscommands/ OS-level command execution, platform detection
patch/ Interactive patch building
config/ YAML config, migrations, validation, editor presets
gui/
gui.go Main GUI struct and initialization
layout.go Panel layout calculation
background.go Background fetch/refresh routines
controllers/ 64 feature controllers
context/ Panel contexts and focus management
services/custom_commands/ Custom command execution engine
presentation/ Rendering: graph, icons, authors, formatting
keybindings/ Keybinding registration
popup/ Dialog/prompt management
modes/ UI modes (cherrypicking, diffing, filtering)
mergeconflicts/ Merge conflict resolution UI
patch_exploring/ Patch navigation and selection
filetree/ File tree data structure
style/ Text styling utilities
status/ Status bar management
types/ GUI type definitions and interfaces
common/ Shared Common struct (Log, Tr, AppState, Fs)
constants/ URLs and links
i18n/ Translation sets, language detection
integration/ Integration test framework
logs/ Logger initialization
snake/ Easter egg snake game
tasks/ Async task management
theme/ Theme loading and color parsing
updates/ Self-update mechanism
utils/ String helpers, rebase-todo manipulation, fuzzy
schema/ JSON Schema for config validation
docs/ User-facing documentation
demo/ Demo recording scripts
test/ Test fixtures and results
scripts/ Development scripts
Architecture Deep Dive
Controller Pattern
Each feature area has a dedicated controller in pkg/gui/controllers/. Controllers implement the IController interface and register keybindings for their context. The 64 controllers include:
| Controller | File | Handles |
|---|---|---|
FilesController |
files_controller.go |
File staging, unstaging, discarding |
BranchesController |
branches_controller.go |
Branch operations |
LocalCommitsController |
local_commits_controller.go |
Commit rewriting, rebase |
RemotesController |
remotes_controller.go |
Remote management |
TagsController |
tags_controller.go |
Tag creation, deletion |
StashController |
stash_controller.go |
Stash operations |
SubmodulesController |
submodules_controller.go |
Submodule management |
WorktreesController |
worktrees_controller.go |
Worktree operations |
BisectController |
bisect_controller.go |
Git bisect workflow |
MergeConflictsController |
merge_conflicts_controller.go |
Conflict resolution |
StagingController |
staging_controller.go |
Hunk/line staging |
PatchBuildingController |
patch_building_controller.go |
Partial commit extraction |
GitFlowController |
git_flow_controller.go |
git-flow commands |
UndoController |
undo_controller.go |
Undo/redo via reflog |
SyncController |
sync_controller.go |
Push/pull/fetch |
SnakeController |
snake_controller.go |
Easter egg game |
Command Struct Pattern
The GitCommand struct in pkg/commands/git.go is a facade that aggregates 22 specialized command structs:
GitCommand
.Blame -> BlameCommands
.Branch -> BranchCommands
.Commit -> CommitCommands
.Config -> ConfigCommands
.Custom -> CustomCommands
.Diff -> DiffCommands
.File -> FileCommands
.Flow -> FlowCommands
.Patch -> PatchCommands
.Rebase -> RebaseCommands
.Remote -> RemoteCommands
.Stash -> StashCommands
.Status -> StatusCommands
.Submodule -> SubmoduleCommands
.Sync -> SyncCommands
.Tag -> TagCommands
.WorkingTree -> WorkingTreeCommands
.Bisect -> BisectCommands
.Worktree -> WorktreeCommands
.Version -> GitVersion
.GitHub -> GitHubCommands
.HostingService -> HostingService
Each command struct shares a GitCommon base that provides access to the command builder, OS commands, repo paths, and config. Commands are constructed using the fluent NewGitCmd() builder:
cmdArgs := NewGitCmd("branch").
Arg("--sort=-committerdate").
ArgIf(containsField, "--format=%(HEAD)...").
ToArgv()
Data Loaders
Nine loader structs handle parsing git output into model objects:
| Loader | Parses | Output Model |
|---|---|---|
BranchLoader |
git branch, git for-each-ref |
Branch |
CommitLoader |
git log with custom format |
Commit |
CommitFileLoader |
git diff-tree |
CommitFile |
FileLoader |
git status --porcelain |
File |
ReflogCommitLoader |
git reflog |
Commit |
RemoteLoader |
git remote, git branch -r |
Remote, RemoteBranch |
StashLoader |
git stash list |
StashEntry |
TagLoader |
git tag --sort |
Tag |
WorktreeLoader |
git worktree list |
Worktree |
Code Patterns and Conventions
- Dependency injection: All command structs receive dependencies through constructors, enabling isolated testing.
- Fluent command builder:
NewGitCmd("...")with.Arg(),.ArgIf(),.ToArgv()chain. - Error handling: Uses
go-errors/errorsfor stack traces. Known errors mapped inpkg/app/errors.go. - Concurrency: Background routines use goroutines with channels. GUI updates are dispatched to the UI thread via
onUIThread(). - Deadlock detection: Uses
sasha-s/go-deadlockin debug builds to detect mutex deadlocks. - File operations: Uses
spf13/aferofilesystem abstraction, enabling in-memory filesystem for tests.
Testing Strategy
Lazygit employs a comprehensive multi-level testing approach:
Unit tests (*_test.go files throughout):
- Config migration tests in
pkg/config/app_config_test.go - Command builder tests in
pkg/commands/git_commands/*_test.go - Utility function tests in
pkg/utils/*_test.go
Integration tests (pkg/integration/):
- Full TUI-driven tests with a robot driver that simulates user input
- Tests live in
pkg/integration/tests/organized by feature area - Three execution modes:
- CLI:
go run cmd/integration_test/main.go cli [--slow] [--sandbox] <tests> - TUI:
go run cmd/integration_test/main.go tui(interactive test browser) - Go test:
go test pkg/integration/clients/*.go(CI/headless)
- CLI:
# Run all integration tests
go run cmd/integration_test/main.go cli
# Run specific test
go run cmd/integration_test/main.go cli commit/new_branch
# Run in sandbox mode (setup runs, then you control lazygit)
go run cmd/integration_test/main.go cli --sandbox commit/new_branch
# Run with slow input for debugging
go run cmd/integration_test/main.go cli --slow commit/new_branch
# Debug with dlv
go run cmd/integration_test/main.go cli -debug tag/reset.go
Tip
The TUI test runner (
go run cmd/integration_test/main.go tui) is the easiest way to browse and run tests. Presstfor slow mode,sfor sandbox mode,dfor debugger mode.
Test results are stored in test/_results/ where you can inspect the repository state after a failed test.
Testing against old git versions: Use rtx (or mise) to install older git versions:
rtx plugin add git
rtx install git 2.32.0
rtx local git 2.32.0
Potential Improvements
Based on open issues and code analysis:
- PR fetch opt-out (#5516): GitHub PR fetching cannot be disabled per-repo, causing 401 errors with submodules in different organizations
- MainBranches globbing (#5246):
mainBranchesconfig only accepts exact names, not glob patterns - Quit during search (#5520): The
quitkeybinding does not work while the search panel is active - Conventional commits (#4414): No built-in support for conventional commit format; achievable via
git.commitPrefixconfig - Alt/Opt modifier (#2166): Limited ability to map Alt/Option key combinations in keybindings
9. Integration Points
External Editor Integration
Lazygit launches external editors for commit message editing, file editing, and interactive rebase. The editor is resolved in this order:
os.editPresetconfig maps to preset command templatesos.edit/os.editAtLine/os.editAtLineAndWaitconfig for custom templates$GIT_EDITORthen$VISUALthen$EDITORthen system default
Editor templates support Go template variables: {{filename}}, {{line}}.
Pager Integration
Custom pagers for diff display are configured via git.pagers:
git:
pagers:
- colorArg: always
pager: delta --dark --paging=never
Hosting Service Integration
Lazygit generates web URLs for the configured hosting service. Custom services (Gitea, Gogs) can be configured:
services:
"git.example.com": "gitea:https://git.example.com"
Daemon Mode
Lazygit uses a daemon subprocess pattern for operations that need to run as $GIT_EDITOR or $GIT_SEQUENCE_EDITOR. The main lazygit process spawns a child lazygit with special daemon environment variables. The child process performs a specific task (e.g., modifying a rebase-todo file) and exits. This is how interactive rebase operations are handled without requiring manual editor interaction.
10. Troubleshooting
Error: Please upgrade to git X.XX.X or later
Cause: Lazygit requires git >= 2.32.0. Your installed version is older.
Solution:
- Check version:
git --version - Upgrade git through your package manager
- Verify:
git --versionshows >= 2.32.0
Error: The config at <path> couldn't be parsed
Cause: Invalid YAML syntax in configuration file.
Solution:
- Validate YAML:
python3 -c "import yaml; yaml.safe_load(open('~/.config/lazygit/config.yml'))" - Check for tab characters (YAML requires spaces)
- Use
lazygit --print-default-configas a reference
Prevention: Use an editor with YAML syntax checking.
Warning
If config parsing fails, lazygit will exit before the GUI starts. There is no fallback to defaults on parse error.
Error: The config at <path> has a validation error
Cause: Config has valid YAML but contains invalid values (e.g., unknown enum value, wrong type).
Solution:
- Read the error message carefully; it names the invalid key and allowed values
- Cross-reference with the JSON schema in
schema/config.json - Fix the value and restart
Error: You need to install git-flow and enable it in this repo
Cause: Attempted to use git-flow features without git-flow installed or initialized.
Solution:
- Install git-flow:
brew install git-flow(macOS) orapt install git-flow(Debian/Ubuntu) - Initialize in repo:
git flow init
Error: GraphQL query failed with status: <status>
Cause: GitHub API request failed. Common with 401 (unauthorized) or 403 (rate limited).
Solution:
- Authenticate with GitHub CLI:
gh auth login - Check rate limits:
gh api rate_limit - If the error occurs on submodule switching, this is a known issue (#5516)
Note
GitHub PR integration errors are non-fatal. Lazygit will continue to function without PR data displayed.
Crash on startup in specific terminals
Cause: Terminal emulator incompatibility with tcell terminal detection. Known to have affected Ghostty (#5511, fixed).
Solution:
- Update your terminal emulator to the latest version
- Try setting
$TERMexplicitly:TERM=xterm-256color lazygit - Try running in a different terminal to isolate the issue
Symlink working directory crash
Cause: Starting lazygit from a symlinked directory that points into a git repo can cause path resolution issues (#3015).
Solution: Navigate to the actual directory rather than the symlink, or use lazygit --path /actual/path.