1. Project Overview & Quickstart (jesseduffield/lazygit)
File: README.md
Warp, the intelligent terminal
Available for macOS and Linux
<sup>VisitΒ warp.devΒ to learn more.</sup> Tuple, the premier screen sharing app for developers on macOS and Windows.
I (Jesse) co-founded Subble to save your company time and money by finding unused and over-provisioned SaaS licences. Check it out!
Sponsors
Elevator Pitch
Rant time: You've heard it before, git is powerful, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? Are you kidding me? To stage part of a file you need to use a command line program to step through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, you have to edit an arcane patch file by hand? Are you KIDDING me?! Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? YOU HAVE GOT TO BE KIDDING ME!
If you're a mere mortal like me and you're tired of hearing how powerful git is when in your daily life it's a powerful pain in your ass, lazygit might be for you.
Table of contents
- Sponsors
- Elevator Pitch
- Table of contents
- Features
- Tutorials
- Installation
- Binary Releases
- Dev container
- Homebrew
- MacPorts
- Void Linux
- Scoop (Windows)
- gah (Linux and Mac OS)
- Arch Linux
- Fedora / Amazon Linux 2023 / CentOS Stream
- Solus Linux
- Debian and Ubuntu
- Funtoo Linux
- Gentoo Linux
- openSUSE
- NixOS
- Flox
- FreeBSD
- Termux
- Conda
- Go
- Chocolatey (Windows)
- Winget (Windows 10 1709 or later)
- Manual
- Usage
- Configuration
- Contributing
- Donate
- FAQ
- Shameless Plug
- Alternatives
Lazygit is not my fulltime job but it is a hefty part time job so if you want to support the project please consider sponsoring me
Features
Stage individual lines
Press space on the selected line to stage it, or press v to start selecting a range of lines. You can also press a to select the entirety of the current hunk.
Interactive Rebase
Press i to start an interactive rebase. Then squash (s), fixup (f), drop (d), edit (e), move up (ctrl+k) or move down (ctrl+j) any of the TODO commits, before continuing the rebase by bringing up the rebase options menu with m and then selecting continue.
You can also perform any of these actions as a once-off (e.g. pressing s on a commit to squash it) without explicitly starting a rebase.
This demo also uses shift+down to select a range of commits to move and fixup.
Cherry-pick
Press shift+c on a commit to copy it and press shift+v to paste (cherry-pick) it.
Bisect
Press b in the commits view to mark a commit as good/bad in order to begin a git bisect.
Nuke the working tree
For when you really want to just get rid of anything that shows up when you run git status (and yes that includes dirty submodules) kidpix style, press shift+d to bring up the reset options menu and then select the 'nuke' option.
Amend an old commit
Pressing shift+a on any commit will amend that commit with the currently staged changes (running an interactive rebase in the background).
Filter
You can filter a view with /. Here we filter down our branches view and then hit enter to view its commits.
Invoke a custom command
Lazygit has a very flexible custom command system. In this example a custom command is defined which emulates the built-in branch checkout action.
Worktrees
You can create worktrees to have multiple branches going at once without the need for stashing or creating WIP commits when switching between them. Press w in the branches view to create a worktree from the selected branch and switch to it.
Rebase magic (custom patches)
You can build a custom patch from an old commit and then remove the patch from the commit, split out a new commit, apply the patch in reverse to the index, and more.
In this example we have a redundant comment that we want to remove from an old commit. We hit <enter> on the commit to view its files, then <enter> on a file to focus the patch, then <space> to add the comment line to our custom patch, and then ctrl+p to view the custom patch options; selecting to remove the patch from the current commit.
Learn more in the Rebase magic Youtube tutorial.
Rebase from marked base commit
Say you're on a feature branch that was itself branched off of the develop branch, and you've decided you'd rather be branching off the master branch. You need a way to rebase only the commits from your feature branch. In this demo we check to see which was the last commit on the develop branch, then press shift+b to mark that commit as our base commit, then press r on the master branch to rebase onto it, only bringing across the commits from our feature branch. Then we push our changes with shift+p.
Undo
You can undo the last action by pressing z and redo with shift+z. Here we drop a couple of commits and then undo the actions.
Undo uses the reflog which is specific to commits and branches so we can't undo changes to the working tree or stash.
Commit graph
When viewing the commit graph in an enlarged window (use + and _ to cycle screen modes), the commit graph is shown. Colours correspond to the commit authors, and as you navigate down the graph, the parent commits of the selected commit are highlighted.
Compare two commits
If you press shift+w on a commit (or branch/ref) a menu will open that allows you to mark that commit so that any other commit you select will be diffed against it. Once you've selected the second commit, you'll see the diff in the main view and if you press <enter> you'll see the files of the diff. You can press shift+w to view the diff menu again to see options like reversing the diff direction or exiting diff mode. You can also exit diff mode by pressing <escape>.
Show GitHub pull requests
In the branches panel, lazygit can show which of your branches have an associated GitHub pull request by showing a GitHub icon next to the branch name; its color shows the state of the PR (open, merged, etc.). For those that have one, you can press shift-G to open the PR in the browser. There is no configuration needed to enable this for github.com, but it requires the gh tool to be installed, and you need to do gh auth login once to allow lazygit to access GitHub. For GitHub Enterprise, also run gh auth login --hostname <webDomain> and add a services entry for the host with the github provider.
Tutorials
Installation
Most of the above packages are maintained by third parties so be sure to vet them yourself and confirm that the maintainer is a trustworthy looking person who attends local sports games and gives back to their communities with barbeque fundraisers etc
Binary Releases
For Windows, Mac OS(10.12+) or Linux, you can download a binary release here.
Dev container feature
If you want to use lazygit in e.g. one of your GitHub Codespaces, there is a third-party dev container feature based on the binary releases mentioned above.
Homebrew
It works with Linux, too.
brew install lazygitMacPorts
Latest version built from github releases.
Tap:
sudo port install lazygitVoid Linux
Packages for Void Linux are available in the distro repo
They follow upstream latest releases
sudo xbps-install -S lazygitScoop (Windows)
You can install lazygit using scoop. It's in the extras bucket:
# Add the extras bucket
scoop bucket add extras
# Install lazygit
scoop install lazygitgah (Linux and Mac OS)
You can install lazygit using gah:
gah install lazygitArch Linux
Packages for Arch Linux are available via pacman and AUR (Arch User Repository).
There are two packages. The stable one which is built with the latest release
and the git version which builds from the most recent commit.
- Stable:
sudo pacman -S lazygit - Development: https://aur.archlinux.org/packages/lazygit-git/
Instruction of how to install AUR content can be found here:
https://wiki.archlinux.org/index.php/Arch_User_Repository
Fedora / Amazon Linux 2023 / CentOS Stream
Packages for Fedora, Amazon Linux 2023 and CentOS Stream are available via
Copr (Cool Other Package Repo).
sudo dnf copr enable dejan/lazygit
sudo dnf install lazygitThese packages are built using the RPM spec file located here: https://codeberg.org/dejan/rpm-lazygit
You should be able to build RPMs for Fedora 41 or older, and other Fedora derivatives using the
SRPM (Source RPM) file that you can grab from the latest COPR build.
Fedora / RHEL Derivatives (Terra)
Packages for Fedora and RHEL derivatives are also available from the Terra Repository.
sudo dnf install --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release
sudo dnf install lazygit(Install guide for Atomic/EL can be found on their README)
Terra also has lazygit-doc, which contains the contents of the docs folder.
Solus Linux
sudo eopkg install lazygitDebian and Ubuntu
For Debian 13 "Trixie", Sid, and later, or Ubuntu 25.10 "Questing Quokka" and later:
sudo apt install lazygitFor Debian 12 "Bookworm", Ubuntu 25.04 "Plucky Puffin" and earlier:
LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | \grep -Po '"tag_name": *"v\K[^"]*')
LAZYGIT_ARCH=$(uname -m | sed -e 's/aarch64/arm64/')
curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/download/v${LAZYGIT_VERSION}/lazygit_${LAZYGIT_VERSION}_Linux_${LAZYGIT_ARCH}.tar.gz"
tar xf lazygit.tar.gz lazygit
sudo install lazygit -D -t /usr/local/bin/Verify the correct installation of lazygit:
lazygit --versionFuntoo Linux
Funtoo Linux has an autogenerated lazygit package in dev-kit:
sudo emerge dev-vcs/lazygitGentoo Linux
Lazygit is not (yet) in main Gentoo portage, however an ebuild is available in GURU overlay
You can either add the overlay to your system and install lazygit as usual:
sudo eselect repository enable guru
sudo emaint sync -r guru
sudo emerge dev-vcs/lazygitopenSUSE
The lazygit package is currently built in devel:languages:go/lazygit.
To install lazygit on openSUSE Tumbleweed run:
sudo zypper ar https://download.opensuse.org/repositories/devel:/languages:/go/openSUSE_Factory/devel:languages:go.repo
sudo zypper ref && sudo zypper in lazygitTo install lazygit on openSUSE Leap run:
source /etc/os-release
sudo zypper ar https://download.opensuse.org/repositories/devel:/languages:/go/$VERSION_ID/devel:languages:go.repo
sudo zypper ref && sudo zypper in lazygitNixOS
Using lazygit from nixpkgs
On NixOS, lazygit is packaged with nix and distributed via nixpkgs.
You can try lazygit without installing it with:
nix-shell -p lazygit
# or with flakes enabled
nix run nixpkgs#lazygitOr you can add lazygit to your configuration.nix using the environment.systemPackages option.
More details can be found via NixOS search page.
Using the official lazygit flake
This repository includes a nix flake that provides the latest development version and additional development tools:
Run lazygit directly from the repository:
nix run github:jesseduffield/lazygit
# or from a local clone
nix run .Build lazygit from source:
nix build github:jesseduffield/lazygit
# or from a local clone
nix build .Development environment:
For contributors, the flake provides a development shell with Go toolchain, development tools, and dependencies:
nix develop github:jesseduffield/lazygit
# or from a local clone
nix developThe development shell includes:
- Go toolchain
- git and make
- Proper environment variables for development
Using in other flakes:
The flake also provides an overlay for easy integration into other flake-based projects:
{
inputs.lazygit.url = "github:jesseduffield/lazygit";
outputs = { self, nixpkgs, lazygit }: {
# Use the overlay
nixpkgs.overlays = [ lazygit.overlays.default ];
};
}Flox
Lazygit can be installed into a Flox environment as follows.
flox install lazygitMore details about Flox can be found on their website.
FreeBSD
pkg install lazygitTermux
apt install lazygitConda
Released versions are available for different platforms, see https://anaconda.org/conda-forge/lazygit
conda install -c conda-forge lazygitGo
go install github.com/jesseduffield/lazygit@latestPlease note:
If you get an error claiming that lazygit cannot be found or is not defined, you
may need to add ~/go/bin to your $PATH (macOS/Linux), or %HOME%\go\bin
(Windows). Not to be mistaken for C:\Go\bin (which is for Go's own binaries,
not apps like lazygit).
Chocolatey (Windows)
You can install lazygit using Chocolatey:
choco install lazygitWinget (Windows 10 1709 or later)
You can install lazygit using the winget command in the Windows Terminal with the following command:
winget install -e --id=JesseDuffield.lazygitManual
You'll need to install Go
git clone https://github.com/jesseduffield/lazygit.git
cd lazygit
go installYou can also use go run main.go to compile and run in one go (pun definitely intended)
Usage
Call lazygit in your terminal inside a git repository.
$ lazygitIf you want, you can
also add an alias for this with echo "alias lg='lazygit'" >> ~/.zshrc (or
whichever rc file you're using).
Keybindings
You can check out the list of keybindings here.
Changing Directory On Exit
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your ~/.zshrc (or other rc file):
lg()
{
export LAZYGIT_NEW_DIR_FILE=~/.lazygit/newdir
lazygit "$@"
if [ -f $LAZYGIT_NEW_DIR_FILE ]; then
cd "$(cat $LAZYGIT_NEW_DIR_FILE)"
rm -f $LAZYGIT_NEW_DIR_FILE > /dev/null
fi
}Then source ~/.zshrc and from now on when you call lg and exit you'll switch directories to whatever you were in inside lazygit. To override this behaviour you can exit using shift+Q rather than just q.
Undo/Redo
See the docs
Configuration
Check out the configuration docs.
Custom Diff Renderers
See the docs
Custom Commands
If lazygit is missing a feature, there's a good chance you can implement it yourself with a custom command!
See the docs
Git flow support
Lazygit supports Gitflow (or git-flow-next) if you have it installed. To understand how the Gitflow model works check out Vincent Driessen's original post explaining it. To view Gitflow options from within Lazygit, press i from within the branches view.
Contributing
We love your input! Please check out the contributing guide.
For contributor discussion about things not better discussed here in the repo, join the discord channel
Check out this video walking through the creation of a small feature in lazygit if you want an idea of where to get started.
Debugging Locally
Run lazygit --debug in one terminal tab and lazygit --logs in another to view the program and its log output side by side
Donate
If you would like to support the development of lazygit, consider sponsoring me (github is matching all donations dollar-for-dollar for 12 months)
FAQ
What do the commit colors represent?
- Green: the commit is included in the master branch
- Yellow: the commit is not included in the master branch
- Red: the commit has not been pushed to the upstream branch
Shameless Plug
If you want to see what I (Jesse) am up to in terms of development, follow me on
twitter or check out my blog
Alternatives
If you find that lazygit doesn't quite satisfy your requirements, these may be a better fit:
File: docs/dev/Busy.md
Knowing when Lazygit is busy/idle
The use-case
This topic deserves its own doc because there are a few touch points for it. We have a use-case for knowing when Lazygit is idle or busy because integration tests follow the following process:
- press a key
- wait until Lazygit is idle
- run assertion / press another key
- repeat
In the past the process was:
- press a key
- run assertion
- if assertion fails, wait a bit and retry
- repeat
The old process was problematic because an assertion may give a false positive due to the contents of some view not yet having changed since the last key was pressed.
The solution
First, it's important to distinguish three different types of goroutines:
- The UI goroutine, of which there is only one, which infinitely processes a queue of events
- Worker goroutines, which do some work and then typically enqueue an event in the UI goroutine to display the results
- Background goroutines, which periodically spawn worker goroutines (e.g. doing a git fetch every minute)
The point of distinguishing worker goroutines from background goroutines is that when any worker goroutine is running, we consider Lazygit to be 'busy', whereas this is not the case with background goroutines. It would be pointless to have background goroutines be considered 'busy' because then Lazygit would be considered busy for the entire duration of the program!
In gocui, the underlying package we use for managing the UI and events, we keep track of how many busy goroutines there are using the Task type. A task represents some work being done by lazygit. The gocui Gui struct holds a map of tasks and allows creating a new task (which adds it to the map), pausing/continuing a task, and marking a task as done (which removes it from the map). Lazygit is considered to be busy so long as there is at least one busy task in the map; otherwise it's considered idle. When Lazygit goes from busy to idle, it notifies the integration test.
It's important that we play by the rules below to ensure that after the user does anything, all the processing that follows happens in a contiguous block of busy-ness with no gaps.
Spawning a worker goroutine
Here's the basic implementation of OnWorker (using the same flow as WaitGroups):
func (g *Gui) OnWorker(f func(*Task)) {
task := g.NewTask()
go func() {
f(task)
task.Done()
}()
}The crucial thing here is that we create the task before spawning the goroutine, because it means that we'll have at least one busy task in the map until the completion of the goroutine. If we created the task within the goroutine, the current function could exit and Lazygit would be considered idle before the goroutine starts, leading to our integration test prematurely progressing.
You typically invoke this with self.c.OnWorker(f). Note that the callback function receives the task. This allows the callback to pause/continue the task (see below).
Spawning a background goroutine
Spawning a background goroutine is as simple as:
go utils.Safe(f)Where utils.Safe is a helper function that ensures we clean up the gui if the goroutine panics.
Programmatically enqueueing a UI event
This is invoked with self.c.OnUIThread(f). Internally, it creates a task before enqueuing the function as an event (including the task in the event struct) and once that event is processed by the event queue (and any other pending events are processed) the task is removed from the map by calling task.Done().
Pressing a key
If the user presses a key, an event will be enqueued automatically and a task will be created before (and Done'd after) the event is processed.
Special cases
There are a couple of special cases where we manually pause/continue the task directly in the client code. These are subject to change but for the sake of completeness:
Writing to the main view(s)
If the user focuses a file in the files panel, we run a git diff command for that file and write the output to the main view. But we only read enough of the command's output to fill the view's viewport: further loading only happens if the user scrolls. Given that we have a background goroutine for running the command and writing more output upon scrolling, we create our own task and call Done on it as soon as the viewport is filled.
Requesting credentials from a git command
Some git commands (e.g. git push) may request credentials. This is the same deal as above; we use a worker goroutine and manually pause continue its task as we go from waiting on the git command to waiting on user input. This requires passing the task through to the Push method so that it can be paused/continued.
File: docs/dev/Codebase_Guide.md
Lazygit Codebase Guide
Packages
pkg/app: Contains startup code, initialises a bunch of stuff like logging, the user config, etc, before starting the gui. Catches and handles some errors that the gui raises.pkg/app/daemon: Contains code relating to the lazygit daemon. This could be better named: it's is not a daemon in the sense that it's a long-running background process; rather it's a short-lived background process that we pass to git for certain tasks, like GIT_EDITOR for when we want to set the TODO file for an interactive rebase.pkg/cheatsheet: Generates the keybinding cheatsheets indocs/keybindings.pkg/commands/git_commands: All communication to the git binary happens here. So for example there's aCheckoutmethod which callsgit checkout.pkg/commands/oscommands: Contains code for talking to the OS, and for invoking commands in generalpkg/commands/git_config: Reading of the git config all happens here.pkg/commands/hosting_service: Contains code that is specific to git hosting services (aka forges).pkg/commands/models: Contains model structs that represent commits, branches, files, etc.pkg/commands/patch: Contains code for parsing and working with git patchespkg/common: Contains theCommonstruct which holds common dependencies like the logger, i18n, and the user config. Most structs in the code will have a field namedcwhich holds a common struct (or a derivative of the common struct).pkg/config: Contains code relating to the Lazygit user config. Specificallypkg/config/user_config/godefines the user config struct and its default values. See below for some important information about using it.pkg/constants: Contains some constant strings (e.g. links to docs)pkg/env: Contains code relating to setting/getting environment variablespkg/i18n: Contains internationalised stringspkg/integration: Contains end-to-end testspkg/jsonschema: Contains generator for user config JSON schema.pkg/logs: Contains code for instantiating the logger and for tailing the logs vialazygit --logspkg/tasks: Contains code for running asynchronous tasks: mostly related to efficiently rendering command output to the main window.pkg/theme: Contains code related to colour themes.pkg/updates: Contains code related to Lazygit updates (checking for update, download and installing the update)pkg/utils: Contains lots of low-level helper functionspkg/gui: Contains code related to the gui. We've still got a God Struct in the form of our Gui struct, but over time code has been moved out into contexts, controllers, and helpers, and we intend to continue moving more code out over time.pkg/gui/context: Contains code relating to contexts. There is a context for each view e.g. a branches context, a tags context, etc. Contexts manage state related to the view and receive keypresses.pkg/gui/controllers: Contains code relating to controllers. Controllers define a list of keybindings and their associated handlers. One controller can be assigned to multiple contexts, and one context can contain multiple controllers.pkg/gui/controllers/helpers: Contains code that is shared between multiple controllers.pkg/gui/filetree: Contains code relating to the representation of filetrees.pkg/gui/keybindings: Contains code for mapping between keybindings and their labelspkg/gui/mergeconflicts: Contains code relating to the handling of merge conflictspkg/gui/modes: Contains code relating to the state of different modes e.g. cherry picking mode, rebase mode.pkg/gui/patch_exploring: Contains code relating to the state of patch-oriented views like the staging view.pkg/gui/popup: Contains code that lets you easily raise popupspkg/gui/presentation: Contains presentation code i.e. code concerned with rendering content inside viewspkg/gui/services/custom_commands: Contains code related to user-defined custom commands.pkg/gui/status: Contains code for invoking loaders and toastspkg/gui/style: Contains code for specifying text styles (colour, bold, etc)pkg/gui/types: Contains various gui-specific types and interfaces. Lots of code lives here to avoid circular dependenciesvendor/github.com/jesseduffield/gocui: Gocui is the underlying library used for handling the gui event loop, handling keypresses, and rendering the UI. It defines the View struct which our own context structs build upon.
Important files
pkg/config/user_config.go: defines the user config and default valuespkg/gui/keybindings.go: defines keybindings which have not yet been moved into a controller (originally all keybindings were defined here)pkg/gui/controllers.go: links up controllers with contextspkg/gui/controllers/helpers/helpers.go: defines all the different helper structspkg/commands/git.go: defines all the different git command structspkg/gui/gui.go: defines the top-level gui state and gui initialisation/run codepkg/gui/layout.go: defines what happens on each renderpkg/gui/controllers/helpers/window_arrangement_helper.go: defines the layout of the UI and the size/position of each windowpkg/gui/context/context.go: defines the different contextspkg/gui/context/setup.go: defines initialisation code for all contextspkg/gui/context.go: manages the lifecycle of contexts, the context stack, and focus changes.pkg/gui/types/views.go: defines viewspkg/gui/views.go: defines the ordering of views (front to back) and their initialisation codepkg/gui/gui_common.go: defines gui-specific methods that all controllers and helpers have access topkg/i18n/english.go: defines the set of i18n strings and their English valuespkg/gui/controllers/helpers/refresh_helper.go: manages refreshing of models. The refresh helper is typically invoked at the end of an action to re-load affected models from git (e.g. re-load branches after doing a git pull)pkg/gui/controllers/quit_actions.go: contains code that runs when you hit 'escape' on a view (assuming the view doesn't define its own escape handler)vendor/github.com/jesseduffield/gocui/gui.go: defines the gocui gui structvendor/github.com/jesseduffield/gocui/view.go: defines the gocui view struct
Concepts
- View: Views are defined in the gocui package, and they maintain an internal buffer of content which is rendered each time the screen is drawn.
- Context: A context is tied to a view and contains some additional state and logic specific to that view e.g. the branches context has code relating specifically to branches, and writes the list of branches to the branches view. Views and contexts share some responsibilities for historical reasons.
- Controller: A controller defined keybindings with associated handlers. One controller can be assigned to multiple contexts and one context can have multiple controllers. For example the list controller handles keybindings relating to navigating a list, and is assigned to all list contexts (e.g. the branches context).
- Helper: A helper defines shared code used by controllers, or used by some other parts of the application. Often a controller will have a method that ends up needing to be used by another controller, so in that case we move the method out into a helper so that both controllers can use it. We need to do this because controllers cannot refer to other controllers' methods.
In terms of dependencies, controllers sit at the highest level, so they can refer to helpers, contexts, and views (although it's preferable for view-specific code to live in contexts). Helpers can refer to contexts and views, and contexts can only refer to views. Views can't refer to contexts, controllers, or helpers.
- Window: A window is a section of the screen which will render a view. Windows are named after the default view that appears there, so for example there is a 'stash' window that is so named because by default the stash view appears there. But if you press enter on a stash entry, the stash entry's files will be shown in a different view, but in the same window.
- Panel: The term 'panel' is still used in a few places to refer to either a view or a window, and it's a term that is now deprecated in favour of 'view' and 'window'.
- Tab: Each tab in a window (e.g. Files, Worktrees, Submodules) actually has a corresponding view which we bring to the front upon changing tabs.
- Model: Representation of a git object e.g. commits, branches, files.
- ViewModel: Used by a context to maintain state related to the view.
- Keybinding: A keybinding associates a key with an action. For example if you press the 'down' arrow, the action performed will be your cursor moving down a list by one.
- Action: An action is the thing that happens when you press a key. Often an action will invoke a git command, but not always: for example, navigation actions don't involve git.
- Common structs: Most structs have a field named
cwhich contains a 'common' struct: a struct containing a bag of dependencies that most structs of the same layer require. For example if you want to access a helper from a controller you can do so withself.c.Helpers.MyHelper.
Event loop and threads
The event loop is managed in the MainLoop function of vendor/github.com/jesseduffield/gocui/gui.go. Any time there is an event like a key press or a window resize, the event will be processed and then the screen will be redrawn. This involves calling the layout function defined in pkg/gui/layout.go, which lays out the windows and invokes some on-render hooks.
Often, as part of handling a keypress, we'll want to run some code asynchronously so that it doesn't block the UI thread. For this we'll typically run self.c.OnWorker(myFunc). If the worker wants to then do something on the UI thread again it can call self.c.OnUIThread(myOtherFunc).
Using UserConfig
The UserConfig struct is loaded from lazygit's global config file (and possibly repo-specific config files). It can be re-loaded while lazygit is running, e.g. when the user edits one of the config files. In this case we should make sure that any new or changed config values take effect immediately. The easiest way to achieve this is what we do in most controllers or helpers: these have a pointer to the common.Common struct, which contains the UserConfig, and access it from there. Since the UserConfig instance in common.Common is updated whenever we reload the config, the code can be sure that it always uses an up-to-date value, and there's nothing else to do.
If that's not possible for some reason, see if you can add code to Gui.onUserConfigLoaded to update things from the new config; there are some examples in that function to use as a guide. If that's too hard to do too, add the config to the list in Gui.checkForChangedConfigsThatDontAutoReload so that the user is asked to quit and restart lazygit.
Legacy code structure
Before we had controllers and contexts, all the code lived directly in the gui package under a gui God Struct. This was fairly bloated and so we split things out to have a better separation of concerns. Nonetheless, it's a big effort to migrate all the code so we still have some logic in the gui struct that ought to live somewhere else. Likewise, we have some keybindings defined in pkg/gui/keybindings.go that ought to live on a controller (all keybindings used to be defined in that one file).
The new structure has its own problems: we don't have a clear guide on whether code should live in a controller or helper. The current approach is to put code in a controller until it's needed by another controller, and to then extract it out into a helper. We may be better off just putting code in helpers to start with and leaving controllers super-thin, with the responsibility of just pairing keys with corresponding helper functions. But it's not clear to me if that would be better than the current approach.
File: docs/dev/Demo_Recordings.md
Demo Recordings
We want our demo recordings to be consistent and easy to update if we make changes to Lazygit's UI. Luckily for us, we have an existing recording system for the sake of our integration tests, so we can piggyback on that.
You'll want to familiarise yourself with how integration tests are written: see here.
Prerequisites
Ideally we'd run this whole thing through docker but we haven't got that working. So you will need:
# for recording
npm i terminalizer
# for gif compression
npm i gifsicle
# for mp4 conversion
brew install ffmpeg
# font with icons
wget https://github.com/ryanoasis/nerd-fonts/releases/download/v3.0.2/DejaVuSansMono.tar.xz && \
tar -xf DejaVuSansMono.tar.xz -C /usr/local/share/fonts && \
rm DejaVuSansMono.tar.xzCreating a demo
Demos are found in pkg/integration/tests/demo/. They are like regular integration tests but have IsDemo: true which has a few effects:
- The bottom row of the UI is quieter so that we can render captions
- Fetch/Push/Pull have artificial latency to mimic a network request
- The loader at the bottom-right does not appear
In demos, we don't need to be as strict in our assertions as we are in tests. But it's still good to have some basic assertions so that if we automate the process of updating demos we'll know if one of them has broken.
You can use the same flow as we use with integration tests when you're writing a demo:
- Setup the repo
- Run the demo in sandbox mode to get a feel of what needs to happen
- Come back and write the code to make it happen
Adding captions
It's good to add captions explaining what task if being performed. Use the existing demos as a guide.
Setting up the assets worktree
We store assets (which includes demo recordings) in the assets branch, which is a branch that shares no history with the main branch and exists purely for storing assets. Storing them separately means we don't clog up the code branches with large binaries.
The scripts and demo definitions live in the code branches but the output lives in the assets branch so to be able to create a video from a demo you'll need to create a linked worktree for the assets branch which you can do with:
git worktree add .worktrees/assets assetsOutputs will be stored in .worktrees/assets/demos/. We'll store three separate things:
- the yaml of the recording
- the original gif
- either the compressed gif or the mp4 depending on the output you chose (see below)
Recording the demo
Once you're happy with your demo you can record it using:
scripts/record_demo.sh [gif|mp4]
# e.g.
scripts/record_demo.sh gif pkg/integration/tests/demo/interactive_rebase.goThe gif format is for use in the first video of the readme (it has a larger size but has auto-play and looping)The mp4 format is for everything else (no looping, requires clicking, but smaller size).
Turns out that you can't store mp4s in a repo and link them from a README so we're gonna just use gifs across the board for now.
Including demos in README/docs
If you've followed the above steps you'll end up with your output in your assets worktree.
Within that worktree, stage all three output files and raise a PR against the assets branch.
Then back in the code branch, in the doc, you can embed the recording like so:
This means we can update assets without needing to update the docs that embed them.
File: docs/dev/Find_Base_Commit_For_Fixup_Design.md
About the mechanics of lazygit's "Find base commit for fixup" command
Background
Lazygit has a command called "Find base commit for fixup" that helps with
creating fixup commits. (It is bound to "ctrl-f" by default, and I'll call it
simply "the ctrl-f command" throughout the rest of this text for brevity.)
It's a heuristic that needs to make a few assumptions; it tends to work well in
practice if users are aware of its limitations. The user-facing side of the
topic is explained here. In this document we describe how
it works internally, and the design decisions behind it.
It is also interesting to compare it to the standalone tool
git-absorb which does a very similar
thing, but made different decisions in some cases. We'll explore these
differences in this document.
Design goals
I'll start with git-absorb's design goals (my interpretation, since I can't
speak for git-absorb's maintainer of course): its main goal seems to be minimum
user interaction required. The idea is that you have a PR in review, the
reviewer requested a bunch of changes, you make all these changes, so you have a
working copy with lots of modified files, and then you fire up git-absorb and it
creates all the necessary fixup commits automatically with no further user
intervention.
While this sounds attractive, it conflicts with ctrl-f's main design goal, which
is to support creating high-quality fixups. My philosophy is that fixup commits
should have the same high quality standards as normal commits; in particular:
- they should be atomic. This means that multiple diff hunks that belong
together to form one logical change should be in the same fixup commit. (Not
always possible if the logical change needs to be fixed up into several
different base commits.) - they should be minimal. Every fixup commit should ideally contain only one
logical change, not several unrelated ones.
Why is this important? Because fixup commits are mainly a tool for reviewing (if
they weren't, you might as well squash the changes into their base commits right
away). And reviewing fixup commits is easier if they are well-structured, just
like normal commits.
The only way to achieve this with git-absorb is to set the oneFixupPerCommit
config option (for the first goal), and then manually stage the changes that
belong together (for the second). This is close to what you have to do with
ctrl-f, with one exception that we'll get to below.
But ctrl-f enforces this by refusing to do the job if the staged hunks belong to
more than one base commit. Git-absorb will happily create multiple fixup commits
in this case; ctrl-f doesn't, to enforce that you pay attention to how you group
the changes. There's another reason for this behavior: ctrl-f doesn't create
fixup commits itself (unlike git-absorb), instead it just selects the found base
commit so that the user can decide whether to amend the changes right in, or
create a fixup commit from there (both are single-key commands in lazygit). And
lazygit doesn't support non-contiguous multiselections of commits, but even if
it did, it wouldn't help much in this case.
The mechanics
General approach
Git-absorb uses a relatively simple approach, and the benefit is of course that
it is easy to understand: it looks at every diff hunk separately, and for every
hunk it looks at all commits (starting from the newest one backwards) to find
the earliest commit that the change can be amended to without conflicts.
It is important to realize that "diff hunk" doesn't necessarily mean what you
see in the diff view. Git-absorb and ctrl-f both use a context of 0 when diffing
your code, so they often see more and smaller hunks than users do. For example,
moving a line of code down by one line is a single hunk for users, but it's two
separate hunks for git-absorb and ctrl-f; one for deleting the line at the old
place, and another one for adding the line at the new place, even if it's only
one line further down.
From this, it follows that there's one big problem with git-absorb's approach:
when moving code, it doesn't realize that the two related hunks of deleting the
code from the old place and inserting it at the new place belong together, and
often it will manage to create a fixup commit for the first hunk, but leave the
other hunk in your working copy as "don't know what to do with this". As an
example, suppose your PR is adding a line of code to an existing function, maybe
one that declares a new variable, and a reviewer suggests to move this line down
a bit, closer to where some other related variables are declared. Moving the
line down results in two diff hunks (from the perspective of git-absorb and
ctrl-f, as they both use a context of 0 when diffing), and when looking at the
second diff hunk in isolation there's no way to find a base commit in your PR
for it, because the surrounding code is already on main.
To solve this, the ctrl-f command makes a distinction between hunks that have
deleted lines and hunks that have only added lines. If the whole diff contains
any hunks that have deleted lines, it uses only those hunks to determine the
base commit, and then assumes that all the hunks that have only added lines
belong into the same commit. This nicely solves the above example of moving
code, but also other examples such as the following:
Click to show example
Suppose you have a PR in which you added the following function:
func findCommit(hash string) (*models.Commit, int, bool) {
for i, commit := range self.c.Model().Commits {
if commit.Hash == hash {
return commit, i, true
}
}
return nil, -1, false
}A reviewer suggests to replace the manual for loop with a call tolo.FindIndexOf since that's less code and more idiomatic. So your modification
is this:
--- a/my_file.go
+++ b/my_file.go
@@ -12,2 +12,3 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
+ "github.com/samber/lo"
"golang.org/x/sync/errgroup"
@@ -308,9 +309,5 @@ func (self *FixupHelper) blameAddedLines(addedLineHunks []*hunk) ([]string, error
func findCommit(hash string) (*models.Commit, int, bool) {
- for i, commit := range self.c.Model().Commits {
- if commit.Hash == hash {
- return commit, i, true
- }
- }
-
- return nil, -1, false
+ return lo.FindIndexOf(self.c.Model().Commits, func(commit *models.Commit) bool {
+ return commit.Hash == hash
+ })
}If we were to look at these two hunks separately, we'd easily find the base
commit for the second one, but we wouldn't find the one for the first hunk
because the imports around the added import have been on main for a long time.
In fact, git-absorb leaves this hunk in the working copy because it doesn't know
what to do with it.
Only if there are no hunks with deleted lines does ctrl-f look at the hunks with
only added lines and determines the base commit for them. This solves cases like
adding a comment above a function that you added in your PR.
The downside of this more complicated approach is that it relies on the user
staging related hunks correctly. However, in my experience this is easy to do
and not very error-prone, as long as users are aware of this behavior. Lazygit
tries to help making them aware of it by showing a warning whenever there are
hunks with only added lines in addition to hunks with deleted lines.
Finding the base commit for a given hunk
As explained above, git-absorb finds the base commit by walking the commits
backwards until it finds one that conflicts with the hunk, and then the found
base commit is the one just before that one. This works reliably, but it is
slow.
Ctrl-f uses a different approach that is usually much faster, but should always
yield the same result. Again, it makes a distinction between hunks with deleted
lines and hunks with only added lines. For hunks with deleted lines it performs
a line range blame for all the deleted lines (e.g. git blame -L42,+3 -- filename), and if the result is the same for all deleted lines, then that's the
base commit; otherwise it returns an error.
For hunks with only added lines, it gets a little more complicated. We blame the
single lines just before and just after the hunk (I'll ignore the edge cases of
either of those not existing because the hunk is at the beginning or end of the
file; read the code to see how we handle these cases). If the blame result is
the same for both, then that's the base commit. This is the case of adding a
line in the middle of a block of code that was added in the PR. Otherwise, the
base commit is the more recent of the two (and in this case it doesn't matter if
the other one is an earlier commit in the current branch, or a possibly very old
commit that's already on main). This covers the common case of adding a comment
to a function that was added in the PR, but also adding another line at the end
of a block of code that was added in the base commit.
It's interesting to discuss what "more recent" means here. You could say if
commit A is an ancestor of commit B (or in other words, A is reachable from B)
then B is the more recent one. And if none of the two commits is reachable from
the other, you have an error case because it's unclear which of the two should
be considered the base commit. The scenario in which this happens is a commit
history like this:
C---D
/ \
A---B---E---F---Gwhere, for instance, D and E are the two blame results.
Unfortunately, determining the ancestry relationship between two commits using
git commands is a bit expensive and not totally straightforward. Fortunately,
it's not necessary in lazygit because lazygit has the most recent 300 commits
cached in memory, and can simply search its linear list of commits to see which
one is closer to the beginning of the list. If only one of the two commits is
found within those 300 commits, then that's the more recent one; if neither is
found, we assume that both commits are on main and error out. In the merge
scenario pictured above, we arbitrarily return one of the two commits (this will
depend on the log order), but that's probably fine as this scenario should be
extremely rare in practice; in most cases, feature branches are simply linear.
Knowing where to stop searching
Git-absorb needs to know when to stop walking backwards searching for commits,
since it doesn't make sense to create fixups for commits that are already on
main. However, it doesn't know where the current branch ends and main starts, so
it needs to rely on user input for this. By default it searches the most recent
10 commits, but this can be overridden with a config setting. In longer branches
this is often not enough for finding the base commit; but setting it to a higher
value causes the command to take longer to complete when the base commit can't
be found.
Lazygit doesn't have this problem. For a given blame result it needs to
determine whether that commit is already on main, and if it can find the commit
in its cached list of the first 300 commits it can get that information from
there, because lazygit knows what the user's configured main branches are
(master and main by default, but it could also include branches like devel
or 1.0-hotfixes), and so it can tell for each commit whether it's contained in
one of those main branches. And if it can't find it among the first 300 commits,
it assumes the commit already on main, on the assumption that no feature branch
has more than 300 commits.
File: docs/dev/Profiling.md
Profiling Lazygit
If you want to investigate what's contributing to CPU or memory usage, start
lazygit with the -profile command line flag. This tells it to start an
integrated web server that listens for profiling requests.
Save profile data
CPU
While lazygit is running with the -profile flag, perform a CPU profile and
save it to a file by running this command in another terminal window:
curl -o cpu.out http://127.0.0.1:6060/debug/pprof/profileBy default, it profiles for 30 seconds. To change the duration, use
curl -o cpu.out 'http://127.0.0.1:6060/debug/pprof/profile?seconds=60'Memory
To save a heap profile (containing information about all memory allocated so
far since startup), use
curl -o mem.out http://127.0.0.1:6060/debug/pprof/heapSometimes it can be useful to get a delta log, i.e. to see how memory usage
developed from one point in time to another. For that, use
curl -o mem.out 'http://127.0.0.1:6060/debug/pprof/heap?seconds=20'This will log the memory usage difference between now and 20 seconds later, so
it gives you 20 seconds to perform the action in lazygit that you are interested
in measuring.
View profile data
To display the profile data, you can either use speedscope.app, or the pprof
tool that comes with go. I prefer the former because it has a nicer UI and is a
little more powerful; however, I have seen cases where it wasn't able to load a
profile for some reason, in which case it's good to have the pprof tool as a
fallback.
Speedscope.app
Go to https://www.speedscope.app/ in your browser, and drag the saved profile
onto the browser window. Refer to the
documentation
for how to navigate the data.
Pprof tool
To view a profile that you saved as cpu.out, use
go tool pprof -http=:8080 cpu.outBy default this shows the graph view, which I don't find very useful myself.
Choose "Flame Graph" from the View menu to show a much more useful
representation of the data.
File: docs/dev/README.md
Dev Documentation Overview
- Codebase Guide
- Busy/Idle Tracking
- Integration Tests
- Demo Recordings
- Find base commit for fixup design
- Profiling
File: docs/keybindings/Custom_Keybindings.md
Custom Keybindings
A keybinding is one of:
- A single printable character, e.g.
q,?,5. Uppercase letters mean
shift+letter β writeA, not<shift+a>. - A special key name in angle brackets, e.g.
<enter>,<f1>, ``. - A key with modifiers in angle brackets, e.g.
<ctrl+c>,<ctrl+shift+up>. - The literal string
<disabled>to disable a binding. - A list of any of the above, to bind multiple keys to the same action:
quit: [q, <ctrl+c>].
Modifiers
Prefix a key with one or more modifiers, joined by +:
| Prefix | Short form | Modifier |
|---|---|---|
ctrl+ |
c+ |
Ctrl |
alt+ |
a+ |
Alt |
shift+ |
s+ |
Shift |
meta+ |
m+ |
Depends on terminal; typically β on macOS or Super/Win key, when the terminal forwards it |
You can also use - instead of + as the separator. Modifiers may appear in
any order, and short and long forms can be mixed. The whole binding should be
wrapped in angle brackets when it has any modifiers. The following all express
the same binding:
<ctrl+shift+up><c+s+up><ctrl-shift-up><shift+ctrl+up>
Special key names
| Put in | You will get |
|---|---|
<f1> β <f12> |
F1 β F12 |
| `` | Insert |
<delete> |
Delete |
<home> |
Home |
<end> |
End |
| `` | PageUp |
| `` | PageDown |
| `` | ArrowUp |
<down> |
ArrowDown |
<left> |
ArrowLeft |
<right> |
ArrowRight |
<tab> |
Tab |
| `` | Shift+Tab |
<enter> |
Enter |
<esc> |
Escape |
| `` | Backspace |
<space> |
Space |
<mouse wheel up>/<mouse wheel down> |
Mouse wheel up/down |
These can be combined with modifiers, e.g. <ctrl+up>, <ctrl+shift+f1>, <alt+enter>.
Special characters with modifiers
<minus> and `` are keyword forms for - and + when combined with a
modifier (e.g. <ctrl+minus> for Ctrl+-). Without modifiers, write - and+ directly. <space> is the keyword for the space character.
Combinations that are rejected
These look reasonable but can't actually be delivered by a terminal:
<shift+a>(shift alone on a rune) β terminals fold shift into the rune
itself, so shift+a arrives asA. WriteAinstead.<ctrl+A>,<alt+A>, etc. (modifier on an uppercase ASCII letter) β write<ctrl+shift+a>instead.
Terminal compatibility
Support for combinations of modifiers, and in general keybindings beyond plain
letters and ctrl+letter, require a newer terminal protocol that not all
terminals support.
Terminals that are known to have good support include: Ghostty, kitty,
WezTerm, foot, Konsole, Alacritty, iTerm2, Windows Terminal.
The default terminal on macOS (Terminal.app) does not; I recommend to switch to
either Ghostty or iTerm2 as a replacement (or one of the others above).
On Windows, a popular terminal is the MinTTY console that comes with Git for
Windows; this also doesn't support the newer protocol. The recommended
replacement is Windows Terminal, which is very good these days, and Git Bash
runs just fine in it.
Inside tmux or screen, extended keys are stripped unless the multiplexer
is configured to forward them. For tmux 3.2+:
β set -g extended-keys on set -as terminal-features 'xterm*:extkeys' β
2. Official Technical Reference & Guides (jesseduffield/jesseduffield.github.io)
March, 2016: If you're on an old version of Jekyll Now and run into a) build warnings or b) syntax highlighting issues caused by Jekyll 3 and GitHub Pages updates, just :sparkles:update your _config.yml:sparkles: and you'll be set!
Jekyll Now
Jekyll is a static site generator that's perfect for GitHub hosted blogs (Jekyll Repository)
Jekyll Now makes it easier to create your Jekyll blog, by eliminating a lot of the up front setup.
- You don't need to touch the command line
- You don't need to install/configure ruby, rvm/rbenv, ruby gems :relaxed:
- You don't need to install runtime dependencies like markdown processors, Pygments, etc
- If you're on Windows, this will make setting up Jekyll a lot easier
- It's easy to try out, you can just delete your forked repository if you don't like it
In a few minutes you'll be set up with a minimal, responsive blog like the one below giving you more time to spend on writing epic blog posts!
Quick Start
Step 1) Fork Jekyll Now to your User Repository
Fork this repo, then rename the repository to yourgithubusername.github.io.
Your Jekyll blog will often be viewable immediately at https://yourgithubusername.github.io (if it's not, you can often force it to build by completing step 2)
Step 2) Customize and view your site
Enter your site name, description, avatar and many other options by editing the _config.yml file. You can easily turn on Google Analytics tracking, Disqus commenting and social icons here too.
Making a change to _config.yml (or any file in your repository) will force GitHub Pages to rebuild your site with jekyll. Your rebuilt site will be viewable a few seconds later at https://yourgithubusername.github.io - if not, give it ten minutes as GitHub suggests and it'll appear soon
There are 3 different ways that you can make changes to your blog's files:
- Edit files within your new username.github.io repository in the browser at GitHub.com (shown below).
- Use a third party GitHub content editor, like Prose by Development Seed. It's optimized for use with Jekyll making markdown editing, writing drafts, and uploading images really easy.
- Clone down your repository and make updates locally, then push them to your GitHub repository.
Step 3) Publish your first blog post
Edit /_posts/2014-3-3-Hello-World.md to publish your first blog post. This Markdown Cheatsheet might come in handy.
You can add additional posts in the browser on GitHub.com too! Just hit the + icon in
/_posts/to create new content. Just make sure to include the front-matter block at the top of each new blog post and make sure the post's filename is in this format: year-month-day-title.md
Local Development
- Install Jekyll and plug-ins in one fell swoop.
gem install github-pagesThis mirrors the plug-ins used by GitHub Pages on your local machine including Jekyll, Sass, etc. - Clone down your fork
git clone https://github.com/yourusername/yourusername.github.io.git - Serve the site and watch for markup/sass changes
jekyll serve - View your website at http://127.0.0.1:4000/
- Commit any changes and push everything to the master branch of your GitHub user repository. GitHub Pages will then rebuild and serve your website.
Moar!
I've created a more detailed walkthrough, Build A Blog With Jekyll And GitHub Pages over at the Smashing Magazine website. Check it out if you'd like a more detailed walkthrough and some background on Jekyll. :metal:
It covers:
- A more detailed walkthrough of setting up your Jekyll blog
- Common issues that you might encounter while using Jekyll
- Importing from Wordpress, using your own domain name, and blogging in your favorite editor
- Theming in Jekyll, with Liquid templating examples
- A quick look at Jekyll 2.0βs new features, including Sass/Coffeescript support and Collections
Jekyll Now Features
β Command-line free fork-first workflow, using GitHub.com to create, customize and post to your blog
β Fully responsive and mobile optimized base theme (Theme Demo)
β Sass/Coffeescript support using Jekyll 2.0
β Free hosting on your GitHub Pages user site
β Markdown blogging
β Syntax highlighting
β Disqus commenting
β Google Analytics integration
β SVG social icons for your footer
β 3 http requests, including your avatar
β No installing dependencies
β No need to set up local development
β No configuring plugins
β No need to spend time on theming
β More time to code other things ... wait β!
Questions?
Open an Issue and let's chat!
Other forkable themes
You can use the Quick Start workflow with other themes that are set up to be forked too! Here are some of my favorites:
- Hyde by MDO
- Lanyon by MDO
- mojombo.github.io by Tom Preston-Werner
- Left by Zach Holman
- Minimal Mistakes by Michael Rose
- Skinny Bones by Michael Rose
Credits
- Jekyll - Thanks to its creators, contributors and maintainers.
- SVG icons - Thanks, Neil Orange Peel. They're beautiful.
- Solarized Light Pygments - Thanks, Edward.
- Joel Glovier - Great Jekyll articles. I used Joel's feed.xml in this repository.
- David Furnes, Jon Uy, Luke Patton - Thanks for the design/code reviews.
- Bart Kiers, Florian Simon, Henry Stanley, Hun Jae Lee, Javier Cejudo, Peter Etelej, Ben Abbott, Ray Nicholus, Erin Grand, LΓ©o Colombaro, Dean Attali, Clayton Errington, Colton Fitzgerald, Trace Mayer - Thanks for your fantastic contributions to the project!
Contributing
Issues and Pull Requests are greatly appreciated. If you've never contributed to an open source project before I'm more than happy to walk you through how to create a pull request.
You can start by opening an issue describing the problem that you're looking to resolve and we'll go from there.
I want to keep Jekyll Now as minimal as possible. Every line of code should be one that's useful to 90% of the people using it. Please bear that in mind when submitting feature requests. If it's not something that most people will use, it probably won't get merged. :guardsman: