Open editor in diff view (#2904)

This commit is contained in:
WaterWhisperer 2026-04-11 02:38:29 +08:00 committed by GitHub
commit 7cd8c8f803
287 changed files with 57704 additions and 0 deletions

8
.cargo/config.toml Normal file
View file

@ -0,0 +1,8 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
[target.arm-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"

2
.clippy.toml Normal file
View file

@ -0,0 +1,2 @@
msrv = "1.88.0"
cognitive-complexity-threshold = 18

3
.editorconfig Normal file
View file

@ -0,0 +1,3 @@
root = true
[*.rs]
indent_style = tab

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
github: extrawurst

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,32 @@
---
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.
**Context (please complete the following information):**
- OS/Distro + Version: [e.g. `macOS 10.15.5`]
- GitUI Version [e.g. `0.5`]
- Rust version: [e.g `1.44`]
**Additional context**
Add any other context about the problem here.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'feature-request'
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.

16
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,16 @@
<!---
Thank you for contributing to GitUI! Please fill out the template below, and remove or add any
information as you feel necessary.
--->
This Pull Request fixes/closes #{issue_num}.
It changes the following:
-
-
I followed the checklist:
- [ ] I added unittests
- [ ] I ran `make check` without errors
- [ ] I tested the overall application
- [ ] I added an appropriate item to the changelog

16
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: cargo
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
groups:
cargo-minor:
patterns: ["*"]
update-types:
- 'minor'
cargo-patch:
patterns: ["*"]
update-types:
- 'patch'

18
.github/stale.yml vendored Normal file
View file

@ -0,0 +1,18 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 180
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 14
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- nostale
# Label to use when marking an issue as stale
staleLabel: dormant
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
any activity half a year. It will be closed in 14 days if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

23
.github/workflows/brew.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: brew update
on:
# only manually
workflow_dispatch:
inputs:
tag-name:
required: true
description: 'release tag'
jobs:
update_brew:
runs-on: ubuntu-latest
steps:
- name: Bump homebrew-core formula
uses: mislav/bump-homebrew-formula-action@v3
env:
COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }}
with:
formula-name: gitui
# https://github.com/mislav/bump-homebrew-formula-action/issues/58
formula-path: Formula/g/gitui.rb
tag-name: ${{ github.event.inputs.tag-name }}

134
.github/workflows/cd.yml vendored Normal file
View file

@ -0,0 +1,134 @@
name: CD
on:
push:
tags:
- "*"
workflow_dispatch:
permissions:
contents: write
jobs:
release:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-22.04]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Get version
id: get_version
run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
shared-key: ${{ matrix.os }}-${{ env.cache-name }}-stable
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: taiki-e/install-action@nextest
- name: Build
if: matrix.os != 'ubuntu-22.04'
env:
GITUI_RELEASE: 1
run: cargo build
- name: Run tests
if: matrix.os != 'ubuntu-22.04'
run: make test
- name: Run clippy
if: matrix.os != 'ubuntu-22.04'
run: |
cargo clean
make clippy
- name: Setup MUSL
if: matrix.os == 'ubuntu-latest'
run: |
rustup target add x86_64-unknown-linux-musl
sudo apt-get -qq install musl-tools
- name: Setup ARM toolchain
if: matrix.os == 'ubuntu-22.04'
run: |
rustup target add aarch64-unknown-linux-gnu
rustup target add armv7-unknown-linux-gnueabihf
rustup target add arm-unknown-linux-gnueabihf
curl -o $GITHUB_WORKSPACE/aarch64.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu.tar.xz
curl -o $GITHUB_WORKSPACE/arm.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf.tar.xz
tar xf $GITHUB_WORKSPACE/aarch64.tar.xz
tar xf $GITHUB_WORKSPACE/arm.tar.xz
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin" >> $GITHUB_PATH
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin" >> $GITHUB_PATH
- name: Build Release Mac
if: matrix.os == 'macos-latest'
env:
GITUI_RELEASE: 1
run: make release-mac
- name: Build Release Mac x86
if: matrix.os == 'macos-latest'
env:
GITUI_RELEASE: 1
run: |
rustup target add x86_64-apple-darwin
make release-mac-x86
- name: Build Release Linux
if: matrix.os == 'ubuntu-latest'
env:
GITUI_RELEASE: 1
run: make release-linux-musl
- name: Build Release Win
if: matrix.os == 'windows-latest'
env:
GITUI_RELEASE: 1
run: make release-win
- name: Build Release Linux ARM
if: matrix.os == 'ubuntu-22.04'
env:
GITUI_RELEASE: 1
run: make release-linux-arm
- name: Set SHA
if: matrix.os == 'macos-latest'
id: shasum
run: |
echo sha="$(shasum -a 256 ./release/gitui-mac.tar.gz | awk '{printf $1}')" >> $GITHUB_OUTPUT
- name: Extract release notes
if: matrix.os == 'ubuntu-latest'
id: release_notes
uses: ffurrer2/extract-release-notes@v2
- name: Release
uses: softprops/action-gh-release@v2
with:
body: ${{ steps.release_notes.outputs.release_notes }}
prerelease: ${{ contains(github.ref, '-') }}
files: |
./release/*.tar.gz
./release/*.zip
./release/*.msi
- name: Bump homebrew-core formula
uses: mislav/bump-homebrew-formula-action@v3
if: "matrix.os == 'macos-latest' && !contains(github.ref, '-')" # skip prereleases
env:
COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }}
with:
formula-name: gitui
# https://github.com/mislav/bump-homebrew-formula-action/issues/58
formula-path: Formula/g/gitui.rb

325
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,325 @@
name: CI
on:
schedule:
- cron: "0 2 * * *"
push:
branches: ["*"]
pull_request:
branches: [master]
env:
CARGO_TERM_COLOR: always
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [nightly, stable, "1.88"]
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.rust == 'nightly' }}
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
shared-key: ${{ matrix.os }}-${{ env.cache-name }}-${{ matrix.rust }}
- name: MacOS Workaround
if: matrix.os == 'macos-latest'
run: cargo clean -p serde_derive -p thiserror
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
components: clippy
- name: Override rust toolchain
run: rustup override set ${{ matrix.rust }}
- name: Rustup Show
run: rustup show
- uses: taiki-e/install-action@nextest
- name: Build Debug
run: |
cargo build
- name: Run tests
run: make test
- name: Run clippy
run: |
make clippy
- name: Build Release
run: make build-release
- name: Test Install
run: cargo install --path "." --force --locked
- name: Binary Size (unix)
if: matrix.os != 'windows-latest'
run: |
ls -l ./target/release/gitui
- name: Binary Size (win)
if: matrix.os == 'windows-latest'
run: |
ls -l ./target/release/gitui.exe
- name: Binary dependencies (mac)
if: matrix.os == 'macos-latest'
run: |
otool -L ./target/release/gitui
- name: Build MSI (windows)
if: matrix.os == 'windows-latest'
run: |
cargo install cargo-wix --version 0.3.3 --locked
cargo wix --version
cargo wix -p gitui --no-build --nocapture --output ./target/wix/gitui-win.msi
ls -l ./target/wix/gitui-win.msi
build-linux-musl:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust: [nightly, stable, "1.88"]
continue-on-error: ${{ matrix.rust == 'nightly' }}
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
key: ubuntu-latest-${{ env.cache-name }}-${{ matrix.rust }}
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
targets: x86_64-unknown-linux-musl
# The build would fail without manually installing the target.
# https://github.com/dtolnay/rust-toolchain/issues/83
- name: Manually install target
run: rustup target add x86_64-unknown-linux-musl
- name: Override rust toolchain
run: rustup override set ${{ matrix.rust }}
- name: Rustup Show
run: rustup show
- uses: taiki-e/install-action@nextest
- name: Setup MUSL
run: |
sudo apt-get -qq install musl-tools
- name: Build Debug
run: |
make build-linux-musl-debug
./target/x86_64-unknown-linux-musl/debug/gitui --version
- name: Build Release
run: |
make build-linux-musl-release
./target/x86_64-unknown-linux-musl/release/gitui --version
ls -l ./target/x86_64-unknown-linux-musl/release/gitui
- name: Test
run: |
make test-linux-musl
- name: Test Install
run: cargo install --path "." --force --locked
build-linux-arm:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust: [nightly, stable, "1.88"]
continue-on-error: ${{ matrix.rust == 'nightly' }}
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
key: ubuntu-latest-${{ env.cache-name }}-${{ matrix.rust }}
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: Override rust toolchain
run: rustup override set ${{ matrix.rust }}
- name: Setup ARM toolchain
run: |
rustup target add aarch64-unknown-linux-gnu
rustup target add armv7-unknown-linux-gnueabihf
rustup target add arm-unknown-linux-gnueabihf
curl -o $GITHUB_WORKSPACE/aarch64.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu.tar.xz
curl -o $GITHUB_WORKSPACE/arm.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf.tar.xz
tar xf $GITHUB_WORKSPACE/aarch64.tar.xz
tar xf $GITHUB_WORKSPACE/arm.tar.xz
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin" >> $GITHUB_PATH
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin" >> $GITHUB_PATH
- name: Rustup Show
run: rustup show
- name: Build Debug
run: |
make build-linux-arm-debug
- name: Build Release
run: |
make build-linux-arm-release
ls -l ./target/aarch64-unknown-linux-gnu/release/gitui || ls -l ./target/armv7-unknown-linux-gnueabihf/release/gitui || ls -l ./target/arm-unknown-linux-gnueabihf/release/gitui
build-apple-x86:
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
rust: [nightly, stable, "1.88"]
continue-on-error: ${{ matrix.rust == 'nightly' }}
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
key: apple-x86-${{ env.cache-name }}-${{ matrix.rust }}
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: Override rust toolchain
run: rustup override set ${{ matrix.rust }}
- name: Setup target
run: rustup target add x86_64-apple-darwin
- name: Rustup Show
run: rustup show
- name: Build Debug
run: |
make build-apple-x86-debug
- name: Build Release
run: |
make build-apple-x86-release
ls -l ./target/x86_64-apple-darwin/release/gitui
linting:
name: Lints
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
key: ubuntu-latest-${{ env.cache-name }}-stable
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt -- --check
- name: tombi install
uses: tombi-toml/setup-tombi@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
version: '0.9.0'
- name: tombi check
run: |
tombi format --check
- name: cargo-deny install
run: |
cargo install --locked cargo-deny
- name: cargo-deny checks
run: |
cargo deny check
udeps:
name: udeps
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
key: ubuntu-latest-${{ env.cache-name }}-nightly
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
- name: build cargo-udeps
run: cargo install --git https://github.com/est31/cargo-udeps --locked
- name: run cargo-udeps
run: cargo +nightly udeps --all-targets
log-test:
name: Changelog Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract release notes
id: extract_release_notes
uses: ffurrer2/extract-release-notes@v2
with:
release_notes_file: ./release-notes.txt
- uses: actions/upload-artifact@v4
with:
name: release-notes.txt
path: ./release-notes.txt
test-homebrew:
name: Test Homebrew Formula (macOS)
runs-on: macos-latest
steps:
- name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Install stable Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Let Homebrew build gitui from source
run: brew install --build-from-source gitui

125
.github/workflows/nightly.yml vendored Normal file
View file

@ -0,0 +1,125 @@
name: Build Nightly Releases
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
AWS_BUCKET_NAME: s3://gitui/nightly/
jobs:
release:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-22.04]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: Swatinem/rust-cache@v2
env:
cache-name: ci
with:
shared-key: ${{ matrix.os }}-${{ env.cache-name }}-stable
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: taiki-e/install-action@nextest
# ideally we trigger the nightly build/deploy only if the normal nightly CI finished successfully
- name: Run tests
if: matrix.os != 'ubuntu-22.04'
run: make test
- name: Run clippy
if: matrix.os != 'ubuntu-22.04'
run: |
cargo clean
make clippy
- name: Setup MUSL
if: matrix.os == 'ubuntu-latest'
run: |
rustup target add x86_64-unknown-linux-musl
sudo apt-get -qq install musl-tools
- name: Setup ARM toolchain
if: matrix.os == 'ubuntu-22.04'
run: |
rustup target add aarch64-unknown-linux-gnu
rustup target add armv7-unknown-linux-gnueabihf
rustup target add arm-unknown-linux-gnueabihf
curl -o $GITHUB_WORKSPACE/aarch64.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu.tar.xz
curl -o $GITHUB_WORKSPACE/arm.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf.tar.xz
tar xf $GITHUB_WORKSPACE/aarch64.tar.xz
tar xf $GITHUB_WORKSPACE/arm.tar.xz
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin" >> $GITHUB_PATH
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin" >> $GITHUB_PATH
- name: Build Release Mac
if: matrix.os == 'macos-latest'
run: make release-mac
- name: Build Release Mac x86
if: matrix.os == 'macos-latest'
run: |
rustup target add x86_64-apple-darwin
make release-mac-x86
- name: Build Release Linux
if: matrix.os == 'ubuntu-latest'
run: make release-linux-musl
- name: Build Release Win
if: matrix.os == 'windows-latest'
run: make release-win
- name: Build Release Linux ARM
if: matrix.os == 'ubuntu-22.04'
run: make release-linux-arm
- name: Ubuntu 22.04 Upload Artifact
if: matrix.os == 'ubuntu-22.04'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
aws s3 cp ./release/gitui-linux-armv7.tar.gz $AWS_BUCKET_NAME
aws s3 cp ./release/gitui-linux-arm.tar.gz $AWS_BUCKET_NAME
aws s3 cp ./release/gitui-linux-aarch64.tar.gz $AWS_BUCKET_NAME
- name: Ubuntu Latest Upload Artifact
if: matrix.os == 'ubuntu-latest'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
aws s3 cp ./release/gitui-linux-x86_64.tar.gz $AWS_BUCKET_NAME
- name: MacOS Upload Artifact
if: matrix.os == 'macos-latest'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
aws s3 cp ./release/gitui-mac.tar.gz $AWS_BUCKET_NAME
aws s3 cp ./release/gitui-mac-x86.tar.gz $AWS_BUCKET_NAME
- name: Windows Upload Artifact
if: matrix.os == 'windows-latest'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
aws s3 cp ./release/gitui-win.msi $env:AWS_BUCKET_NAME
aws s3 cp ./release/gitui-win.tar.gz $env:AWS_BUCKET_NAME

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/target
/release
.DS_Store
/.idea/
flamegraph.svg

13
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "(OSX) Launch",
"type": "lldb",
"request": "launch",
"program": "${workspaceRoot}/target/debug/gitui",
"args": [],
"cwd": "${workspaceRoot}",
}
]
}

4
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"editor.formatOnSave": true,
"workbench.settings.enableNaturalLanguageSearch": false,
}

1018
CHANGELOG.md Normal file

File diff suppressed because it is too large Load diff

26
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,26 @@
# Contributing
Were glad you found this document that is intended to make contributing to
GitUI as easy as possible!
## Building GitUI
In order to build GitUI on your machine, follow the instructions in the
[“Build” section](./README.md#build).
## Getting help
Theres a [Discord server][discord-server] you can join if you get stuck or
dont know where to start. People are happy to answer any questions you might
have!
## Getting started
If you are looking for something to work on, but dont yet know what might be a
good first issue, you can take a look at [issues labelled with
`good-first-issue`][good-first-issues]. They have been selected to not require
too much context so that people not familiar with the codebase yet can still
make a contribution.
[discord-server]: https://discord.gg/rZv4uxSQx3
[good-first-issues]: https://github.com/gitui-org/gitui/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22

5203
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

110
Cargo.toml Normal file
View file

@ -0,0 +1,110 @@
[package]
name = "gitui"
version = "0.28.1"
authors = ["extrawurst <mail@rusticorn.com>"]
description = "blazing fast terminal-ui for git"
edition = "2021"
rust-version = "1.88"
exclude = [".github/*", ".vscode/*", "assets/*"]
homepage = "https://github.com/gitui-org/gitui"
repository = "https://github.com/gitui-org/gitui"
readme = "README.md"
license = "MIT"
categories = ["command-line-utilities"]
keywords = ["cli", "git", "gui", "terminal", "ui"]
build = "build.rs"
[workspace]
members = [
"asyncgit",
"filetreelist",
"git2-hooks",
"git2-testing",
"scopetime",
]
[dependencies]
anyhow = "1.0"
asyncgit = { path = "./asyncgit", version = "0.28.1", default-features = false }
backtrace = "0.3"
base64 = "0.22"
bitflags = "2.10"
bugreport = "0.5.1"
bwrap = { version = "1.3", features = ["use_std"] }
bytesize = { version = "2.3", default-features = false }
chrono = { version = "0.4", default-features = false, features = ["clock"] }
clap = { version = "4.5", features = ["cargo", "env"] }
crossbeam-channel = "0.5"
crossterm = { version = "0.29", features = ["serde"] }
dirs = "6.0"
easy-cast = "0.5"
filetreelist = { path = "./filetreelist", version = ">=0.6" }
fuzzy-matcher = "0.3"
gh-emoji = { version = "1.0", optional = true }
indexmap = "2"
itertools = "0.14"
log = "0.4"
notify = "8"
notify-debouncer-mini = "0.7"
once_cell = "1"
parking_lot_core = "0.9"
ratatui = { version = "0.30", default-features = false, features = [
"crossterm",
"serde",
] }
ratatui-textarea = "0.8"
rayon-core = "1.13"
ron = "0.12"
scopeguard = "1.2"
scopetime = { path = "./scopetime", version = "0.1" }
serde = "1.0"
shellexpand = "3.1"
simplelog = { version = "0.12", default-features = false }
struct-patch = "0.10"
syntect = { version = "5.3", default-features = false, features = [
"default-syntaxes",
"default-themes",
"html",
"parsing",
"plist-load",
] }
two-face = { version = "0.4.4", default-features = false }
unicode-segmentation = "1.12"
unicode-truncate = "2.0"
unicode-width = "0.2"
which = "8.0"
[dev-dependencies]
env_logger = "0.11"
git2-testing = { path = "./git2-testing" }
insta = { version = "1.41.0", features = ["filters"] }
pretty_assertions = "1.4"
tempfile = "3"
[build-dependencies]
chrono = { version = "0.4", default-features = false, features = ["clock"] }
[badges]
maintenance = { status = "actively-developed" }
[features]
default = ["ghemoji", "regex-fancy", "trace-libgit", "vendor-openssl"]
ghemoji = ["gh-emoji"]
# regex-* features are mutually exclusive.
regex-fancy = ["syntect/regex-fancy", "two-face/syntect-fancy"]
regex-onig = ["syntect/regex-onig", "two-face/syntect-onig"]
timing = ["scopetime/enabled"]
trace-libgit = ["asyncgit/trace-libgit"]
vendor-openssl = ["asyncgit/vendor-openssl"]
# make debug build as fast as release
# usage of utf8 encoding inside tui
# makes their debug profile slow
[profile.dev.package."ratatui"]
opt-level = 3
[profile.release]
opt-level = "z" # Optimize for size.
strip = "debuginfo"
lto = true
codegen-units = 1

26
FAQ.md Normal file
View file

@ -0,0 +1,26 @@
## <a name="table-of-contents"></a> Table of Contents
1. ["Bad Credentials" Error](#credentials)
2. [Custom key bindings](#keybindings)
2. [Watcher](#watcher)
## 1. <a name="credentials"></a> "Bad Credentials" Error <small><sup>[Top ▲](#table-of-contents)</sup></small>
Some users have trouble pushing/pulling from remotes and adding their ssh-key to their ssh-agent solved the issue. The error they get is:
![](./assets/bad-credentials.png)
See Github's excellent documentation for [Adding your SSH Key to the ssh-agent](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#adding-your-ssh-key-to-the-ssh-agent)
Note that in some cases adding the line `ssh-add -K ~/.ssh/id_ed25519`(or whatever your key is called) to your bash init script is necessary too to survive restarts.
## 2. <a name="keybindings"></a> Custom key bindings <small><sup>[Top ▲](#table-of-contents)</sup></small>
If you want to use `vi`-style keys or customize your key bindings in any other fashion see the specific docs on that: [key config](./KEY_CONFIG.md)
## 3. <a name="watcher"></a> Watching for changes <small><sup>[Top ▲](#table-of-contents)</sup></small>
By default, `gitui` polls for changes in the working directory every 5 seconds. If you supply `--watcher` as an argument, it uses a `notify`-based approach instead. This is usually faster and was for some time the default update strategy. It turned out, however, that `notify`-based updates can cause issues on some platforms, so tick-based updates seemed like a safer default.
See #1444 for details.

51
KEY_CONFIG.md Normal file
View file

@ -0,0 +1,51 @@
# Key Config
The default keys are based on arrow keys to navigate.
However popular demand lead to fully customizability of the key bindings.
Create a `key_bindings.ron` file like this:
```
(
move_left: Some(( code: Char('h'), modifiers: "")),
move_right: Some(( code: Char('l'), modifiers: "")),
move_up: Some(( code: Char('k'), modifiers: "")),
move_down: Some(( code: Char('j'), modifiers: "")),
stash_open: Some(( code: Char('l'), modifiers: "")),
open_help: Some(( code: F(1), modifiers: "")),
status_reset_item: Some(( code: Char('U'), modifiers: "SHIFT")),
)
```
The config file format based on the [Ron file format](https://github.com/ron-rs/ron).
The location of the file depends on your OS:
* `$HOME/.config/gitui/key_bindings.ron` (mac)
* `$XDG_CONFIG_HOME/gitui/key_bindings.ron` (linux using XDG)
* `$HOME/.config/gitui/key_bindings.ron` (linux)
* `%APPDATA%/gitui/key_bindings.ron` (Windows)
See all possible keys to overwrite in gitui: [here](https://github.com/gitui-org/gitui/blob/master/src/keys/key_list.rs#L83)
Possible values for:
* `code` are defined by the type `KeyCode` in crossterm: [here](https://docs.rs/crossterm/latest/crossterm/event/enum.KeyCode.html)
* `modifiers` are defined by the type `KeyModifiers` in crossterm: [here](https://docs.rs/crossterm/latest/crossterm/event/struct.KeyModifiers.html)
Here is a [vim style key config](vim_style_key_config.ron) with `h`, `j`, `k`, `l` to navigate. Use it to copy the content into `key_bindings.ron` to get vim style key bindings.
# Key Symbols
Similar to the above GitUI allows you to change the way the UI visualizes key combos containing special keys like `enter`(default: `⏎`) and `shift`(default: `⇧`).
If we can find a file `key_symbols.ron` in the above folders we apply the overwrites in it.
Example content of this file looks like:
```
(
enter: Some("enter"),
shift: Some("shift-")
)
```
This example will only overwrite two symbols. Find all possible symbols to overwrite in `symbols.rs` in the type `KeySymbolsFile` ([src/keys/symbols.rs](https://github.com/gitui-org/gitui/blob/master/src/keys/symbols.rs))

21
LICENSE.md Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 gitui-org
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.

120
Makefile Normal file
View file

@ -0,0 +1,120 @@
.PHONY: debug build-release release-linux-musl test clippy clippy-pedantic install install-debug sort
ARGS=-l
# ARGS=-l -d ~/code/extern/kubernetes
# ARGS=-l -d ~/code/extern/linux
# ARGS=-l -d ~/code/git-bare-test.git -w ~/code/git-bare-test
profile:
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --features timing -- ${ARGS}
run-timing:
cargo run --features=timing --release -- ${ARGS}
debug:
RUST_BACKTRACE=true cargo run --features=timing -- ${ARGS}
build-release:
cargo build --release --locked
release-mac: build-release
strip target/release/gitui
otool -L target/release/gitui
ls -lisah target/release/gitui
mkdir -p release
tar -C ./target/release/ -czvf ./release/gitui-mac.tar.gz ./gitui
ls -lisah ./release/gitui-mac.tar.gz
release-mac-x86: build-apple-x86-release
strip target/x86_64-apple-darwin/release/gitui
otool -L target/x86_64-apple-darwin/release/gitui
ls -lisah target/x86_64-apple-darwin/release/gitui
mkdir -p release
tar -C ./target/x86_64-apple-darwin/release/ -czvf ./release/gitui-mac-x86.tar.gz ./gitui
ls -lisah ./release/gitui-mac-x86.tar.gz
release-win: build-release
mkdir -p release
tar -C ./target/release/ -czvf ./release/gitui-win.tar.gz ./gitui.exe
cargo install cargo-wix --version 0.3.3 --locked
cargo wix -p gitui --no-build --nocapture --output ./release/gitui-win.msi
ls -l ./release/gitui-win.msi
release-linux-musl: build-linux-musl-release
strip target/x86_64-unknown-linux-musl/release/gitui
mkdir -p release
tar -C ./target/x86_64-unknown-linux-musl/release/ -czvf ./release/gitui-linux-x86_64.tar.gz ./gitui
build-apple-x86-debug:
cargo build --target=x86_64-apple-darwin
build-apple-x86-release:
cargo build --release --target=x86_64-apple-darwin --locked
build-linux-musl-debug:
cargo build --target=x86_64-unknown-linux-musl
build-linux-musl-release:
cargo build --release --target=x86_64-unknown-linux-musl --locked
test-linux-musl:
cargo nextest run --workspace --target=x86_64-unknown-linux-musl
release-linux-arm: build-linux-arm-release
mkdir -p release
aarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/gitui
arm-linux-gnueabihf-strip target/armv7-unknown-linux-gnueabihf/release/gitui
arm-linux-gnueabihf-strip target/arm-unknown-linux-gnueabihf/release/gitui
tar -C ./target/aarch64-unknown-linux-gnu/release/ -czvf ./release/gitui-linux-aarch64.tar.gz ./gitui
tar -C ./target/armv7-unknown-linux-gnueabihf/release/ -czvf ./release/gitui-linux-armv7.tar.gz ./gitui
tar -C ./target/arm-unknown-linux-gnueabihf/release/ -czvf ./release/gitui-linux-arm.tar.gz ./gitui
build-linux-arm-debug:
cargo build --target=aarch64-unknown-linux-gnu
cargo build --target=armv7-unknown-linux-gnueabihf
cargo build --target=arm-unknown-linux-gnueabihf
build-linux-arm-release:
cargo build --release --target=aarch64-unknown-linux-gnu --locked
cargo build --release --target=armv7-unknown-linux-gnueabihf --locked
cargo build --release --target=arm-unknown-linux-gnueabihf --locked
test:
cargo nextest run --workspace
fmt:
cargo fmt -- --check
clippy:
cargo clippy --workspace --all-features
clippy-nightly:
cargo +nightly clippy --workspace --all-features
check: fmt clippy test sort deny
check-nightly:
cargo +nightly c
cargo +nightly clippy --workspace --all-features
cargo +nightly t
deny:
cargo deny check
sort:
tombi format --check
install:
cargo install --path "." --offline --locked
install-timing:
cargo install --features=timing --path "." --offline --locked
licenses:
cargo bundle-licenses --format toml --output THIRDPARTY.toml
clean:
cargo clean

14
NIGHTLIES.md Normal file
View file

@ -0,0 +1,14 @@
# Nightlies
**Use with caution as these binaries are build nightly and might be broken**
When you find problems please report them and always mention the version that you see in the `help popup` or when running `gitui -V`
* [gitui-linux-aarch64.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-aarch64.tar.gz)
* [gitui-linux-arm.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-arm.tar.gz)
* [gitui-linux-armv7.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-armv7.tar.gz)
* [gitui-linux-x86_64.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-x86_64.tar.gz)
* [gitui-mac.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-mac.tar.gz)
* [gitui-mac-x86.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-mac-x86.tar.gz)
* [gitui-win.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-win.tar.gz)
* [gitui-win.msi](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-win.msi)

301
README.md Normal file
View file

@ -0,0 +1,301 @@
<h1 align="center">
<img width="300px" src="assets/logo.png" />
[![CI][s0]][l0] [![crates][s1]][l1] ![MIT][s2] [![UNSAFE][s3]][l3] [![TWEET][s6]][l6] [![dep_status][s7]][l7] [![discord][s8]][l8]
</h1>
[s0]: https://github.com/gitui-org/gitui/workflows/CI/badge.svg
[l0]: https://github.com/gitui-org/gitui/actions
[s1]: https://img.shields.io/crates/v/gitui.svg
[l1]: https://crates.io/crates/gitui
[s2]: https://img.shields.io/badge/license-MIT-blue.svg
[s3]: https://img.shields.io/badge/unsafe-forbidden-success.svg
[l3]: https://github.com/rust-secure-code/safety-dance/
[s6]: https://img.shields.io/twitter/follow/extrawurst?label=follow&style=social
[l6]: https://twitter.com/intent/follow?screen_name=extrawurst
[s7]: https://deps.rs/repo/github/gitui-org/gitui/status.svg
[l7]: https://deps.rs/repo/github/gitui-org/gitui
[s8]: https://img.shields.io/discord/1176858176897953872
[l8]: https://discord.gg/rQNeEnMhus
<h5 align="center">GitUI provides you with the comfort of a git GUI but right in your terminal</h1>
![](demo.gif)
## <a name="table-of-contents"></a> Table of Contents
1. [Features](#features)
2. [Motivation](#motivation)
3. [Benchmarks](#bench)
4. [Roadmap](#roadmap)
5. [Limitations](#limitations)
6. [Installation](#installation)
7. [Build](#build)
8. [FAQs](#faqs)
9. [Diagnostics](#diagnostics)
10. [Color Theme](#theme)
11. [Key Bindings](#bindings)
12. [Sponsoring](#sponsoring)
13. [Inspiration](#inspiration)
14. [Contributing](#contributing)
15. [Contributors](#contributors)
## 1. <a name="features"></a> Features <small><sup>[Top ▲](#table-of-contents)</sup></small>
- Fast and intuitive **keyboard only** control
- Context based help (**no need to memorize** tons of hot-keys)
- Inspect, commit, and amend changes (incl. hooks: *pre-commit*,*commit-msg*,*post-commit*,*prepare-commit-msg*)
- Stage, unstage, revert and reset files, hunks and lines
- Stashing (save, pop, apply, drop, and inspect)
- Push / Fetch to / from remote
- Branch List (create, rename, delete, checkout, remotes)
- Browse / **Search** commit log, diff committed changes
- Responsive terminal UI
- Async git API for fluid control
- Submodule support
- gpg commit signing with shortcomings (see [#97](https://github.com/gitui-org/gitui/issues/97)))
## 2. <a name="motivation"></a> Motivation <small><sup>[Top ▲](#table-of-contents)</sup></small>
I do most of my git work in a terminal but I frequently found myself using git GUIs for some use-cases like: index, commit, diff, stash, blame and log.
Unfortunately popular git GUIs all fail on giant repositories or become unresponsive and unusable.
GitUI provides you with the user experience and comfort of a git GUI but right in your terminal while being portable, fast, free and opensource.
## 3. <a name="bench"></a> Benchmarks <small><sup>[Top ▲](#table-of-contents)</sup></small>
For a [RustBerlin meetup presentation](https://youtu.be/rpilJV-eIVw?t=5334) ([slides](https://github.com/extrawurst/gitui-presentation)) I compared `lazygit`,`tig` and `gitui` by parsing the entire Linux git repository (which contains over 900k commits):
| | Time | Memory (GB) | Binary (MB) | Freezes | Crashes |
| --------- | ---------- | ----------- | ----------- | --------- | --------- |
| `gitui` | **24 s** ✅ | **0.17** ✅ | 10 | **No** ✅ | **No** ✅ |
| `lazygit` | 57 s | 2.6 | 25 | Yes | Sometimes |
| `tig` | 4 m 20 s | 1.3 | **0.6** ✅ | Sometimes | **No** ✅ |
## 4. <a name="roadmap"></a> Road(map) to 1.0 <small><sup>[Top ▲](#table-of-contents)</sup></small>
These are the high level goals before calling out `1.0`:
* visualize branching structure in log tab ([#81](https://github.com/gitui-org/gitui/issues/81))
* interactive rebase ([#32](https://github.com/gitui-org/gitui/issues/32))
- no git-lfs support (see [#2812](https://github.com/gitui-org/gitui/issues/2812))
## 5. <a name="limitations"></a> Known Limitations <small><sup>[Top ▲](#table-of-contents)</sup></small>
- no sparse repo support (see [#1226](https://github.com/gitui-org/gitui/issues/1226))
- *credential.helper* for https needs to be **explicitly** configured (see [#800](https://github.com/gitui-org/gitui/issues/800))
Currently, this tool does not fully substitute the _git shell_, however both tools work well in tandem.
The priorities for `gitui` are on features that are making me mad when done on the _git shell_, like stashing, staging lines or hunks. Eventually, I will be able to work on making `gitui` a one stop solution - but for that I need help - this is just a spare time project for now.
All support is welcomed! Sponsors as well! ❤️
## 6. <a name="installation"></a> Installation <small><sup>[Top ▲](#table-of-contents)</sup></small>
GitUI is in beta and may contain bugs and missing features. However, for personal use it is reasonably stable and is being used while developing itself.
<a href="https://repology.org/project/gitui/versions">
<img src="https://repology.org/badge/vertical-allrepos/gitui.svg" alt="Packaging status" align="right">
</a>
### Various Package Managers
<details>
<summary>Install Instructions</summary>
##### [Arch Linux](https://archlinux.org/packages/extra/x86_64/gitui/)
```sh
pacman -S gitui
```
##### Fedora
```sh
sudo dnf install gitui
```
##### Gentoo
Available in [dm9pZCAq overlay](https://github.com/gentoo-mirror/dm9pZCAq)
```sh
sudo eselect repository enable dm9pZCAq
sudo emerge --sync dm9pZCAq
sudo emerge dev-vcs/gitui::dm9pZCAq
```
##### [openSUSE](https://software.opensuse.org/package/gitui)
```sh
sudo zypper install gitui
```
##### Homebrew (macOS)
```sh
brew install gitui
```
##### [MacPorts (macOS)](https://ports.macports.org/port/gitui/details/)
```sh
port install gitui
```
##### [Winget](https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/StephanDilly/gitui) (Windows)
```
winget install gitui
```
##### [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/gitui.json) (Windows)
```
scoop install gitui
```
##### [Chocolatey](https://chocolatey.org/packages/gitui) (Windows)
```
choco install gitui
```
##### [Mise](https://github.com/jdx/mise)
```shell
mise use -g gitui@latest
```
##### [Nix](https://search.nixos.org/packages?channel=unstable&show=gitui&from=0&size=50&sort=relevance&query=gitui) (Nix/NixOS)
Nixpkg
```
nix-env -iA nixpkgs.gitui
```
NixOS
```
nix-env -iA nixos.gitui
```
##### [Termux](https://github.com/termux/termux-packages/tree/master/packages/gitui) (Android)
```
pkg install gitui
```
##### [Anaconda](https://anaconda.org/conda-forge/gitui)
```
conda install -c conda-forge gitui
```
</details>
### Release Binaries
[Available for download in releases](https://github.com/gitui-org/gitui/releases)
Binaries available for:
#### Linux
- gitui-linux-x86_64.tar.gz (linux musl statically linked)
- gitui-linux-aarch64.tar.gz (linux on 64 bit arm)
- gitui-linux-arm.tar.gz
- gitui-linux-armv7.tar.gz
All contain a single binary file
#### macOS
- gitui-mac.tar.gz (arm64)
- gitui-mac-x86.tar.gz (intel x86)
#### Windows
- gitui-win.tar.gz (single 64bit binary)
- gitui-win.msi (64bit Installer package)
### Nightly Builds
see [NIGHTLIES.md](./NIGHTLIES.md)
## 7. <a name="build"></a> Build <small><sup>[Top ▲](#table-of-contents)</sup></small>
### Requirements
- Minimum supported `rust`/`cargo` version: `1.88`
- See [Install Rust](https://www.rust-lang.org/tools/install)
- To build openssl dependency (see https://docs.rs/openssl/latest/openssl/)
- perl >= 5.12 (strawberry perl works for windows https://strawberryperl.com/)
- a c compiler (msvc, gcc or clang, cargo will find it)
- To run the complete test suite python is required (and it must be invocable as `python`)
### Cargo Install
The simplest way to start playing around with `gitui` is to have `cargo` build and install it with `cargo install gitui --locked`. If you are not familiar with rust and cargo: [Getting Started with Rust](https://doc.rust-lang.org/book/ch01-00-getting-started.html)
### Cargo Features
#### trace-libgit
enable `libgit2` tracing
works if `libgit2` built with `-DENABLE_TRACE=ON`
this feature enabled by default, to disable: `cargo install --no-default-features`
## 8. <a name="faqs"></a> FAQs <small><sup>[Top ▲](#table-of-contents)</sup></small>
see [FAQs page](./FAQ.md)
## 9. <a name="diagnostics"></a> Diagnostics <small><sup>[Top ▲](#table-of-contents)</sup></small>
To run with logging enabled run `gitui -l`.
This will log to:
- macOS: `$HOME/Library/Caches/gitui/gitui.log`
- Linux using `XDG`: `$XDG_CACHE_HOME/gitui/gitui.log`
- Linux: `$HOME/.cache/gitui/gitui.log`
- Windows: `%LOCALAPPDATA%/gitui/gitui.log`
## 10. <a name="theme"></a> Color Theme <small><sup>[Top ▲](#table-of-contents)</sup></small>
![](assets/light-theme.png)
`gitui` should automatically work on both light and dark terminal themes.
However, you can customize everything to your liking: See [Themes](THEMES.md).
## 11. <a name="bindings"></a> Key Bindings <small><sup>[Top ▲](#table-of-contents)</sup></small>
The key bindings can be customized: See [Key Config](KEY_CONFIG.md) on how to set them to `vim`-like bindings.
## 12. <a name="sponsoring"></a> Sponsoring <small><sup>[Top ▲](#table-of-contents)</sup></small>
[![github](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors)](https://github.com/sponsors/extrawurst)
## 13. <a name="inspiration"></a> Inspiration <small><sup>[Top ▲](#table-of-contents)</sup></small>
- [lazygit](https://github.com/jesseduffield/lazygit)
- [tig](https://github.com/jonas/tig)
- [GitUp](https://github.com/git-up/GitUp)
- It would be nice to come up with a way to have the map view available in a terminal tool
- [git-brunch](https://github.com/andys8/git-brunch)
## 14. <a name="contributing"></a> Contributing <small><sup>[Top ▲](#table-of-contents)</sup></small>
See [CONTRIBUTING.md](CONTRIBUTING.md).
## 15. <a name="contributors"></a> Contributors <small><sup>[Top ▲](#table-of-contents)</sup></small>
Thanks goes to all the contributors that help make GitUI amazing! ❤️
Wanna become a co-maintainer? We are looking for [you](https://github.com/gitui-org/gitui/issues/2084)!
<a href="https://github.com/gitui-org/gitui/graphs/contributors">
<img src="https://contrib.rocks/image?repo=gitui-org/gitui" />
</a>

90
THEMES.md Normal file
View file

@ -0,0 +1,90 @@
# Themes
default on light terminal:
![](assets/light-theme.png)
## Configuration
To change the colors of the default theme you need to add a `theme.ron` file that contains the colors you want to override. Note that you dont have to specify the full theme anymore (as of 0.23). Instead, it is sufficient to override just the values that you want to differ from their default values.
The file uses the [Ron format](https://github.com/ron-rs/ron) and is located at one of the following paths, depending on your operating system:
* `$HOME/.config/gitui/theme.ron` (mac)
* `$XDG_CONFIG_HOME/gitui/theme.ron` (linux using XDG)
* `$HOME/.config/gitui/theme.ron` (linux)
* `%APPDATA%/gitui/theme.ron` (Windows)
Alternatively, you can create a theme in the same directory mentioned above and use it with the `-t` flag followed by the name of the file in the directory. E.g. If you are on linux calling `gitui -t arc.ron`, this will load the theme in `$XDG_CONFIG_HOME/gitui/arc.ron` or `$HOME/.config/gitui/arc.ron`.
Example theme override:
```ron
(
selection_bg: Some("Blue"),
selection_fg: Some("#ffffff"),
)
```
Note that you need to wrap values in `Some` due to the way the overrides work (as of 0.23).
Notes:
* rgb colors might not be supported in every terminal.
* using a color like `yellow` might appear in whatever your terminal/theme defines for `yellow`
* valid colors can be found in ratatui's [Color](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html) struct.
* all customizable theme elements can be found in [`style.rs` in the `impl Default for Theme` block](https://github.com/gitui-org/gitui/blob/master/src/ui/style.rs#L305)
## Preset Themes
You can find preset themes by Catppuccin [here](https://github.com/catppuccin/gitui.git).
## Syntax Highlighting
The syntax highlighting theme can be defined using the element `syntax`. Both [default themes of the syntect library](https://github.com/trishume/syntect/blob/7fe13c0fd53cdfa0f9fea1aa14c5ba37f81d8b71/src/dumps.rs#L215) and custom themes are supported.
Example syntax theme:
```ron
(
syntax: Some("InspiredGitHub"),
)
```
Custom themes are located in the [configuration directory](#configuration), are using TextMate's theme format and must have a `.tmTheme` file extension. To load a custom theme, `syntax` must be set to the file name without the file extension. For example, to load [`Blackboard.tmTheme`](https://raw.githubusercontent.com/filmgirl/TextMate-Themes/refs/heads/master/Blackboard.tmTheme), place the file next to `theme.ron` and set:
```ron
(
syntax: Some("Blackboard"),
)
```
[filmgirl/TextMate-Themes](https://github.com/filmgirl/TextMate-Themes) offers many [beautiful](https://inkdeep.github.io/TextMate-Themes) TextMate themes to choose from.
## Customizing line breaks
If you want to change how the line break is displayed in the diff, you can also specify `line_break` in your `theme.ron`:
```ron
(
line_break: Some("¶"),
)
```
Note that if you want to turn it off, you should use a blank string:
```ron
(
line_break: Some(""),
)
```
## Customizing selection
By default the `selection_fg` color is used to color the text of the selected line.
Diff line, filename, commit hashes, time and author are re-colored with `selection_fg` color.
This can be changed by specifying the `use_selection_fg` boolean in your `theme.ron`:
```
(
use_selection_fg: Some(false),
)
```
By default, `use_selection_fg` is set to `true`.

BIN
assets/add-remote.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
assets/amend.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
assets/bad-credentials.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
assets/binary_diff.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/blame-goto-line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

BIN
assets/blame.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

BIN
assets/branches.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
assets/by-line-ops.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
assets/checkout-remote.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

BIN
assets/cmdbar.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
assets/commit-details.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
assets/compact-tree.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
assets/compare.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

BIN
assets/diff-empty-line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

View file

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2020-05-23T13:11:59.516Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15" etag="nKCWIzIC1T2-_TwD7yCE" version="13.1.3" type="device"><diagram id="KDklGPkv8WkujI4HOg4Q" name="Page-1">5Zhdr5MwAIZ/DZcnGe2Acek+1GiciYsx8a6HdtBYKJZOxvn1tqOMsXLiNGESdrPRtx/Q9wH6Fgeu0uM7gfLkE8eEOWCGjw5cOwAEC1/9aqGqBQ+GtRALimvJbYUdfSFGnBn1QDEpOg0l50zSvCtGPMtIJDsaEoKX3WZ7zrpnzVFMLGEXIWar3yiWSa0uQNDq7wmNk+bMrm/ml6KmsZlJkSDMywsJbhy4EpzL+ig9rgjT3jW+1P3evlJ7vjBBMnlLh+rj9oO/3e23n+HL5vv8iX3FX57MKL8QO5gJm4uVVeOA4IcMEz3IzIHLMqGS7HIU6dpSIVdaIlOmSq46NMMRIcnx1et0z7NXdw3hKZGiUk2aDg366qpctv5Dz2jJhfdgZkRkmMfnsVtb1IFx5i9cAiN0CVy5BHpcgj0uucFQLkHLpYinKZVKc7xl5HhryzU1f9m1ppCC/yArzrhQSsYz1XK5p4xdSYjROFPFSPlFlL7UblL13L4xFSnFWJ+ml0WX1gA4QGjjCHrv2YFgzC0YgugZ1jDW04YRjgyGZ8EopF53Tixqz6bMA/gj4+FbPFIuDI7iZMaUccDgzzgW98QRjG95nfujCyGLEboU/GsI8YZyKXzgEHKN4/xS/V/v2eahecgU4s3HRsPe7D1UDPHdsQGx95WMFMWj5JDghj3TXXOIa+d0hLGjvzjprnF2Cok+SrUd2XOh/xQpOmlK1qLSQym8KyU7vSeE5fVTk0yahXfDRqo/lA7Fwo7uPw9N2HK8zbTzlrWgDLevVcX2S/Gp7uJzO9z8Bg==</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

BIN
assets/fuzzy-find.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

BIN
assets/gitui-signing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/light-theme.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

View file

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2020-06-02T16:58:00.925Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15" etag="XbmHwEoPIuusTyLd3JQZ" version="13.1.13" type="device"><diagram id="t1-bsFE1bwoXy9pVcyMy" name="Page-1">7VzbcuI4EP0aV+0+QNmyxeUxQJjULsluLTNJzb6kFCzAE9vy2CKQfP1KvttSKmQLYQ+Qh2BaQrLPabVa3Q2aOfZ2X0IUrG+JjV0N6PZOMycaAEA3DfbCJa+JpG8ME8EqdOxEZBSCufOGU6GeSjeOjaNKR0qIS52gKlwQ38cLWpGhMCTbarclcauzBmiFBcF8gVxR+uDYdJ1IB6BfyG+ws1pnMxu99Pk8lHVOnyRaI5tsSyLzWjPHISE0ufJ2Y+xy8DJcks9N32nNbyzEPt3nA4PHR3ezfQBvsEP/Ivff/ny5HnZ6ySgvyN2kD5zeLH3NEAjJxrcxH0TXzNF27VA8D9CCt24Z50y2pp7L3hnsUryp9D5fcEjxriRKb/ILJh6m4SvrkumMlQKWaoyVqcK2wN/op7J1GftBKkQp56t87AIWdpEi8wmUjBaiZFRRMiUomQMJSmCoCiWzhSiBGkqWRJekKFmqUIICSlcbuiahZl4x8ZQQjd9zD3kcDZfdzugJhZrFLBfpLoiXN61oDFEv6RKyq1gyQRQnQwGLveo6/2eI/eY3V0k33UBP5vIHeo5s1B9E9lAgjWFNq8xENCTPeExcftsTn/is52jpuG5NhFxn5bO3Ll7yEThvDjOtV6mYkuAdLajqScQaHX81i0eZwELylQ/QuK5AVarSb/+CsvSmQRoIIN3iKOK7+gH0WAPmMv77WJk9x7b5THvo8yF4MGvKOhR5gDK7poqGoUDDPY5v2CX+ik/o2+w/3gUhY8d5wbHL5nkO5a5SRpje7Xbjj4TY4xNGwYa/2pwN5gS2zjApWVBwvwWlbIMygEDlOGWqmQWlfvGY4uKxjrl4DNEpGLsk4itCg6PraKHBiQLtXzAIcdi8LastATgQ6RgelQ5xT2nfxgv33HgNUxlMos2fOu5h7PQvs/FaEi/RPKayZgPXWDBO2V7AKgW9hs03EE++nALRaJwuBRKTrYqCO++Ph1v9bvbPj+nu+/zf2f3wedQRfZbGLbZVj9DIzpOyONZBXDspSqKeThxmYk/Fr7PqJ3iJXweOqZVW+7Sy7kYcNbgqBUn0fRsHibk2TUZXpTC1MFLfyZWikfCqFKY2xczag4p4oEnCvlo9TKzPKQ7WyGdXE2be+VNMs+BxFMTyAsnezw1PXo0WySbAo8nh6gn9pvNzK2B3qkuvfueXHFl9SXzaWSLPYRPFH/eIT6KYi0qXKE4LxtHqYFfMm8WyNQDrGUjIwOHSOL2Wv8vAgjFcTDLh1/zGIMcHMow/6mvkfTOl+F/DgGKYhJS8pWhIgM4bMrXlAj5VRXW5MFZeLo/Vl0uM+G1pqkRn93mYvCl/jkKxIbcAec9h8dz6ayGGJXGi+MXYg1JbugAKZKxSI1sIecOqNH8d5fhtDnVZWFWAtJ+gKYmCs6WV6HimVgd331txhBXN9r6xBGWOkhhJuCTFmk2KNZEEk6pG5nm1aUeXBX3kwTdlO7oh1iScQ96rdqa2JCc8VWkvOQ1i4OGS9zroElK3gsRoyInlueoBqCOmueSIi/GQBPHGUgYKfKv+x6Af10KJ8RWIlj08uJz9Lme/y9nv7M9+/T33YXUGSoxszphjpMW1D0m665SrHwRGjln+ICekhTFUuG+dobI0haQo5PSrHerHLUmVoapiBzkJ8pKTUyx2yBRfknk6bmZdXl8imoTTgbzpcxIQYzvNGeBsm4LtS4lmsJTrGTBFjns+zgNsm/MgqYVqge7ukac2ezKYlEWfwBnEQmDbYiFA9GxTH46bi04tjXTShsPQPzYc0uI0ZdyI/vWcIrpJyTFiOlJ+/KcoUHNIbws9gsXKotUleqQGSxk9oued50+zoMmMrBKyQEJWFlh5KqIq50NgvRaxaQJN0Y8/4/UlhMEap0fm81/W1/sENri+kD79ubu9ebv7vvW+RnT+2Pu2lf5GQDl3kjC0f+rlyid0zZAG+hT5Cy7JspN6nuh/J3R9SdlcUjYnnLJBEbJtZP96mRvBvEmM4CdcQknmRn6IVWXyxKP+J6qKPzBqF+t1sV4nab3O3mxJio2Pa7bEyFviWM+c6CC1Xwo8Zm6Y0t9G42AfhBirV/0OJ5B8jdaQZfcO8YVyKTFipVKZGDYrt9F5hP9siBr2uzWmIOhKfiVkCEWuLKiIK7FoI9/J/ybBJjgffkwdHJUfLd1nirbSDmNe/wc=</diagram></mxfile>

BIN
assets/log-search.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

1
assets/logo.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="600" fill="none"><g clip-path="url(#a)"><mask id="c" width="438" height="484" x="38" y="57" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#D9D9D9" d="m340 541 76-39 60-52-76-43-35-8-13 42H231l-13-38-46-7-24-106 20-29v-44l85-47 101-6 48 26 67-40-60-67-150-25-189 78-31 201 116 182 185 22Z"/></mask><g stroke="#000" filter="url(#b)" mask="url(#c)"><path stroke-width="38.4" d="M290 483a183 183 0 1 0 0-366 183 183 0 0 0 0 366Z"/><path fill="#000" stroke-linejoin="round" stroke-width="12.8" d="m480 351 23-9-18-16-5 25Zm-14 36 25-4-15-20-10 24Zm-20 33 25 1-11-23-14 22Zm-26 28 24 6-6-24-18 18Zm-32 22 23 11-1-25-22 14Zm-35 16 20 15 4-25-24 10Zm-37 9 16 18 9-23-25 5Zm-39 1 13 21 13-21h-26Zm-38-6 9 23 16-18-25-5Zm-36-14 4 25 20-15-24-10Zm-33-20-1 25 23-11-22-14Zm-28-26-6 24 24-6-18-18Zm-22-32-11 23 25-1-14-22Zm-16-35-15 20 25 4-10-24Zm-9-37-18 16 23 9-5-25Zm-1-39-21 13 21 13v-26Zm6-38-23 9 18 16 5-25Zm14-36-25 4 15 20 10-24Zm20-33-25-1 11 23 14-22Zm26-28-24-6 6 24 18-18Zm32-22-23-11 1 25 22-14Zm35-16-20-15-4 25 24-10Zm37-9-16-18-9 23 25-5Zm39-1-13-21-13 21h26Zm38 6-9-23-16 18 25 5Zm36 14-4-25-20 15 24 10Zm33 20 1-25-23 11 22 14Zm28 26 6-24-24 6 18 18Zm22 32 11-23-25 1 14 22Zm16 36 15-20-25-4 10 24Zm9 36 18-16-23-9 5 25Z"/><path fill="#000" stroke-linejoin="round" stroke-width="25.6" d="m260 121 30 30 30-30h-60Zm191 95-19 38 38 19-19-57Zm-32 211-41-6-7 41 48-35Zm-210 35-7-41-41 6 48 35Zm-99-189 38-19-19-38-19 57Z"/></g></g><g filter="url(#d)"><path fill="#000" d="M305 307h81v7h-18v71l-35-6a98 98 0 0 1-92-5c-24-16-36-41-36-75 0-26 8-47 23-63 16-16 37-24 63-24 15 0 28 3 41 8l27-5 1 60h-9c-7-36-24-54-51-54h-7c-29 4-44 30-44 80 0 16 1 30 4 42 7 24 22 36 45 36 12 0 22-4 31-10v-55h-24v-7Zm157 71h3c2 0 4 3 4 7h-70v-7l6-1c7-1 10-6 10-14v-92h-16v-8h52v115h11Zm-8-152c0 4-1 7-3 10-4 7-10 11-18 11-4 0-7-1-10-3-8-4-11-10-11-18l2-10c4-7 10-11 19-11 3 0 7 1 10 3 7 4 11 10 11 18Zm24 46v-7h20v-27l36-6v33h37v7h-37v86l1 7c1 10 5 15 12 15l4-1c7-1 13-10 16-25l8 1c-1 8-4 14-6 19-6 9-16 14-30 14h-7c-23-3-34-18-34-45v-71h-20Zm101-49v-8h85v8h-22v97c0 11 0 20 2 28 5 21 16 31 36 31 30 0 45-19 45-59v-97h-22v-8h55v8h-22v96c0 13-2 25-5 34-8 23-27 35-57 35-48 0-72-22-72-65V223h-23Zm271 162h-85v-8h23V223h-23v-8h85v8h-22v154h15c4 1 6 3 7 8Z"/></g><defs><filter id="b" width="455.7" height="455.7" x="62" y="72" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset/><feGaussianBlur stdDeviation="2"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_1_2"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_1_2" result="shape"/></filter><filter id="d" width="653.4" height="191.2" x="201" y="201" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset/><feGaussianBlur stdDeviation="2"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_1_2"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_1_2" result="shape"/></filter><clipPath id="a"><path fill="#fff" d="M64 74h452v452H64z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 KiB

BIN
assets/msg-scrolling.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

BIN
assets/newlines.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

1
assets/options.drawio Normal file
View file

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2021-08-17T21:58:53.216Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15" etag="DR-vNI6rA-1d9_EpYLQC" version="14.9.7" type="device"><diagram id="cIq82w5ce00BbVejkL92" name="Page-1">5Zldc6IwFIZ/DZfrEJCvS0Xb7exHZ9bd6WxvdlIIkGlMmBir3V+/iQQVg+12Bqm7eqHwkkB43nMCJ1puPF9fc1gWX1iKiOXY6dpyJ5bjABCG8kcpz5Xi+0El5BynutFOmOHfSIu2Vpc4RYtGQ8EYEbhsigmjFCWioUHO2arZLGOkedUS5sgQZgkkpnqHU1FUaugEO/0jwnlRXxn4UXVkDuvG+k4WBUzZak9yp5Ybc8ZEtTVfx4goeDWXqt/VkaPbgXFExd90KLNx+X10/wl4v+6/5fx28jMVH/RZniBZ6hvWgxXPNQHOljRF6iS25Y5XBRZoVsJEHV1Jz6VWiDmRe0Bu6tMhLtD66DjB9u5l2CA2R4I/yyZ1h0B30REDvGDgVcpq54AbaazFHn0n1CLUrufbs+/AyA3N5g2cnPPj5Hh+g5Njm5S2wbdPCYTeiSi5BqVZIUPesX9QwWHyKOkcUpP3L4c1hgTnVCoEZWpXgcEyCUdanuM0VT3GTcgZJiRmhPHNudxs8+mIrn9A17UNutE2MBtBeKoYHBp0JyiDSyKOQd2Lt4Xg7BHVrCij6ACflmofEokJ8ZecaAvwpjsduDC03RrxCz74bVPBqVzwDBfuFInFhoR8DhWQ5vKB1b0lr6VGL4Y4fjBwDhwJTUcA6Dc1fMOUm5wyjv7nzBgOgteN6DU1AsOFmNENccf+jKnKCh/OFQ76sCi3HC4iS1zbNCfs05zQMGd4UdnRZkCv2REZBmDFqljSRymTKj8uJx+clvepPu2oC8w9P+wT8D/jhGhxoNeEAGbxORNQLM8zDTL5KJvpIQ07cKTtTQq8e40BzEJ3gmVNdamO+O/viFlU35YCM3r+aaL2CXxAZMx4ivjBNbtwzIsOZzUQvb9jZoloWPWmxaIDdzoB11zdcD2TWuva0fBk0MwSLt6U0moAkKby/SiXm0xORfZyt5pky2BW7032DZVf9ePDtrzxNRYJoxmWvWJ1aLMQpba+oWTJF/gJWd7kWAp1uxzVZm8nJgaDenl962PLGiDwt9PavpXuyaxsqQMJWxwtxs9u7Q8EzewYAq9tBbrXOcUs32YrGeFFFezW1LGiqTUC1vTKGodWeKWUUWyF3k7pK9xPMD+1OxBEfTpg1m9f4VOFfzD4p+DWL94vh3c3cOXu7i+uzbG9Pwrd6R8=</diagram></mxfile>

BIN
assets/options.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

BIN
assets/popup-stacking.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 KiB

BIN
assets/pull.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/push.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
assets/push_tags.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
assets/rebase.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
assets/reset_in_log.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
assets/revert-commit.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
assets/reword.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,000 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

BIN
assets/scrollbar.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

BIN
assets/select-copy.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

BIN
assets/spinner.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
assets/stash_pop.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

1
assets/stashing.drawio Normal file

File diff suppressed because one or more lines are too long

BIN
assets/stashing.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
assets/submodules.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

BIN
assets/tag-annotation.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

BIN
assets/tagging.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
assets/tags-list-popup.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
assets/termux-android.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 KiB

BIN
assets/undo-last-commit.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,019 KiB

BIN
assets/vi_support.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

52
asyncgit/Cargo.toml Normal file
View file

@ -0,0 +1,52 @@
[package]
name = "asyncgit"
version = "0.28.1"
authors = ["extrawurst <mail@rusticorn.com>"]
edition = "2021"
description = "allow using git2 in a asynchronous context"
homepage = "https://github.com/gitui-org/gitui"
repository = "https://github.com/gitui-org/gitui"
readme = "README.md"
license = "MIT"
categories = ["asynchronous", "concurrency"]
keywords = ["git"]
[dependencies]
bitflags = "2"
crossbeam-channel = "0.5"
dirs = "6.0"
easy-cast = "0.5"
fuzzy-matcher = "0.3"
git2 = "0.20"
git2-hooks = { path = "../git2-hooks", version = "0.7" }
gix = { version = "0.78.0", default-features = false, features = [
"mailmap",
"max-performance",
"revision",
"status",
] }
log = "0.4"
# git2 = { path = "../../extern/git2-rs", features = ["vendored-openssl"]}
# git2 = { git="https://github.com/extrawurst/git2-rs.git", rev="fc13dcc", features = ["vendored-openssl"]}
# pinning to vendored openssl, using the git2 feature this gets lost with new resolver
openssl-sys = { version = "0.9", features = ["vendored"], optional = true }
rayon = "1.11"
rayon-core = "1.13"
scopetime = { path = "../scopetime", version = "0.1" }
serde = { version = "1.0", features = ["derive"] }
ssh-key = { version = "0.6.7", features = ["crypto", "encryption"] }
thiserror = "2.0"
unicode-truncate = "2.0"
url = "2.5"
[dev-dependencies]
env_logger = "0.11"
invalidstring = { path = "../invalidstring", version = "0.1" }
pretty_assertions = "1.4"
serial_test = "3.3"
tempfile = "3"
[features]
default = ["trace-libgit"]
trace-libgit = []
vendor-openssl = ["openssl-sys"]

1
asyncgit/LICENSE.md Symbolic link
View file

@ -0,0 +1 @@
../LICENSE.md

12
asyncgit/README.md Normal file
View file

@ -0,0 +1,12 @@
# asyncgit
*allow using git2 in an asynchronous context*
This crate is designed as part of the [gitui](http://gitui.org) project.
`asyncgit` provides the primary interface to interact with *git* repositories. It is split into the main module and a `sync` part. The latter provides convenience wrapper for typical usage patterns against git repositories.
The primary goal however is to allow putting certain (potentially) long running [git2](https://github.com/rust-lang/git2-rs) calls onto a thread pool.[crossbeam-channel](https://github.com/crossbeam-rs/crossbeam) is then used to wait for a notification confirming the result.
In `gitui` this allows the main-thread and therefore the *ui* to stay responsive.

View file

@ -0,0 +1,302 @@
//! provides `AsyncJob` trait and `AsyncSingleJob` struct
#![deny(clippy::expect_used)]
use crate::error::Result;
use crossbeam_channel::Sender;
use std::sync::{Arc, Mutex, RwLock};
/// Passed to `AsyncJob::run` allowing sending intermediate progress notifications
pub struct RunParams<
T: Copy + Send,
P: Clone + Send + Sync + PartialEq,
> {
sender: Sender<T>,
progress: Arc<RwLock<P>>,
}
impl<T: Copy + Send, P: Clone + Send + Sync + PartialEq>
RunParams<T, P>
{
/// send an intermediate update notification.
/// do not confuse this with the return value of `run`.
/// `send` should only be used about progress notifications
/// and not for the final notification indicating the end of the async job.
/// see `run` for more info
pub fn send(&self, notification: T) -> Result<()> {
self.sender.send(notification)?;
Ok(())
}
/// set the current progress
pub fn set_progress(&self, p: P) -> Result<bool> {
Ok(if *self.progress.read()? == p {
false
} else {
*(self.progress.write()?) = p;
true
})
}
}
/// trait that defines an async task we can run on a threadpool
pub trait AsyncJob: Send + Sync + Clone {
/// defines what notification type is used to communicate outside
type Notification: Copy + Send;
/// type of progress
type Progress: Clone + Default + Send + Sync + PartialEq;
/// can run a synchronous time intensive task.
/// the returned notification is used to tell interested parties
/// that the job finished and the job can be access via `take_last`.
/// prior to this final notification it is not safe to assume `take_last`
/// will already return the correct job
fn run(
&mut self,
params: RunParams<Self::Notification, Self::Progress>,
) -> Result<Self::Notification>;
/// allows observers to get intermediate progress status if the job customizes it
/// by default this will be returning `Self::Progress::default()`
fn get_progress(&self) -> Self::Progress {
Self::Progress::default()
}
}
/// Abstraction for a FIFO task queue that will only queue up **one** `next` job.
/// It keeps overwriting the next job until it is actually taken to be processed
#[derive(Debug, Clone)]
pub struct AsyncSingleJob<J: AsyncJob> {
next: Arc<Mutex<Option<J>>>,
last: Arc<Mutex<Option<J>>>,
progress: Arc<RwLock<J::Progress>>,
sender: Sender<J::Notification>,
pending: Arc<Mutex<()>>,
}
impl<J: 'static + AsyncJob> AsyncSingleJob<J> {
///
pub fn new(sender: Sender<J::Notification>) -> Self {
Self {
next: Arc::new(Mutex::new(None)),
last: Arc::new(Mutex::new(None)),
pending: Arc::new(Mutex::new(())),
progress: Arc::new(RwLock::new(J::Progress::default())),
sender,
}
}
///
pub fn is_pending(&self) -> bool {
self.pending.try_lock().is_err()
}
/// makes sure `next` is cleared and returns `true` if it actually canceled something
pub fn cancel(&self) -> bool {
if let Ok(mut next) = self.next.lock() {
if next.is_some() {
*next = None;
return true;
}
}
false
}
/// take out last finished job
pub fn take_last(&self) -> Option<J> {
self.last.lock().map_or(None, |mut last| last.take())
}
/// spawns `task` if nothing is running currently,
/// otherwise schedules as `next` overwriting if `next` was set before.
/// return `true` if the new task gets started right away.
pub fn spawn(&self, task: J) -> bool {
self.schedule_next(task);
self.check_for_job()
}
///
pub fn progress(&self) -> Option<J::Progress> {
self.progress.read().ok().map(|d| (*d).clone())
}
fn check_for_job(&self) -> bool {
if self.is_pending() {
return false;
}
if let Some(task) = self.take_next() {
let self_clone = (*self).clone();
rayon_core::spawn(move || {
if let Err(e) = self_clone.run_job(task) {
log::error!("async job error: {e}");
}
});
return true;
}
false
}
fn run_job(&self, mut task: J) -> Result<()> {
//limit the pending scope
{
let _pending = self.pending.lock()?;
let notification = task.run(RunParams {
progress: self.progress.clone(),
sender: self.sender.clone(),
})?;
if let Ok(mut last) = self.last.lock() {
*last = Some(task);
}
self.sender.send(notification)?;
}
self.check_for_job();
Ok(())
}
fn schedule_next(&self, task: J) {
if let Ok(mut next) = self.next.lock() {
*next = Some(task);
}
}
fn take_next(&self) -> Option<J> {
self.next.lock().map_or(None, |mut next| next.take())
}
}
#[cfg(test)]
mod test {
use super::*;
use crossbeam_channel::unbounded;
use pretty_assertions::assert_eq;
use std::{
sync::atomic::{AtomicBool, AtomicU32, Ordering},
thread,
time::Duration,
};
#[derive(Clone)]
struct TestJob {
v: Arc<AtomicU32>,
finish: Arc<AtomicBool>,
value_to_add: u32,
}
type TestNotification = ();
impl AsyncJob for TestJob {
type Notification = TestNotification;
type Progress = ();
fn run(
&mut self,
_params: RunParams<Self::Notification, Self::Progress>,
) -> Result<Self::Notification> {
println!("[job] wait");
while !self.finish.load(Ordering::SeqCst) {
std::thread::yield_now();
}
println!("[job] sleep");
thread::sleep(Duration::from_millis(100));
println!("[job] done sleeping");
let res =
self.v.fetch_add(self.value_to_add, Ordering::SeqCst);
println!("[job] value: {res}");
Ok(())
}
}
#[test]
fn test_overwrite() {
let (sender, receiver) = unbounded();
let job: AsyncSingleJob<TestJob> =
AsyncSingleJob::new(sender);
let task = TestJob {
v: Arc::new(AtomicU32::new(1)),
finish: Arc::new(AtomicBool::new(false)),
value_to_add: 1,
};
assert!(job.spawn(task.clone()));
task.finish.store(true, Ordering::SeqCst);
thread::sleep(Duration::from_millis(10));
for _ in 0..5 {
println!("spawn");
assert!(!job.spawn(task.clone()));
}
println!("recv");
receiver.recv().unwrap();
receiver.recv().unwrap();
assert!(receiver.is_empty());
assert_eq!(
task.v.load(std::sync::atomic::Ordering::SeqCst),
3
);
}
fn wait_for_job(job: &AsyncSingleJob<TestJob>) {
while job.is_pending() {
thread::sleep(Duration::from_millis(10));
}
}
#[test]
fn test_cancel() {
let (sender, receiver) = unbounded();
let job: AsyncSingleJob<TestJob> =
AsyncSingleJob::new(sender);
let task = TestJob {
v: Arc::new(AtomicU32::new(1)),
finish: Arc::new(AtomicBool::new(false)),
value_to_add: 1,
};
assert!(job.spawn(task.clone()));
task.finish.store(true, Ordering::SeqCst);
thread::sleep(Duration::from_millis(10));
for _ in 0..5 {
println!("spawn");
assert!(!job.spawn(task.clone()));
}
println!("cancel");
assert!(job.cancel());
task.finish.store(true, Ordering::SeqCst);
wait_for_job(&job);
println!("recv");
receiver.recv().unwrap();
println!("received");
assert_eq!(
task.v.load(std::sync::atomic::Ordering::SeqCst),
2
);
}
}

188
asyncgit/src/blame.rs Normal file
View file

@ -0,0 +1,188 @@
use crate::{
error::Result,
hash,
sync::{self, CommitId, FileBlame, RepoPath},
AsyncGitNotification,
};
use crossbeam_channel::Sender;
use std::{
hash::Hash,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
},
};
///
#[derive(Hash, Clone, PartialEq, Eq)]
pub struct BlameParams {
/// path to the file to blame
pub file_path: String,
/// blame at a specific revision
pub commit_id: Option<CommitId>,
}
struct Request<R, A>(R, Option<A>);
#[derive(Default, Clone)]
struct LastResult<P, R> {
params: P,
result: R,
}
///
pub struct AsyncBlame {
current: Arc<Mutex<Request<u64, FileBlame>>>,
last: Arc<Mutex<Option<LastResult<BlameParams, FileBlame>>>>,
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicUsize>,
repo: RepoPath,
}
impl AsyncBlame {
///
pub fn new(
repo: RepoPath,
sender: &Sender<AsyncGitNotification>,
) -> Self {
Self {
repo,
current: Arc::new(Mutex::new(Request(0, None))),
last: Arc::new(Mutex::new(None)),
sender: sender.clone(),
pending: Arc::new(AtomicUsize::new(0)),
}
}
///
pub fn last(&self) -> Result<Option<(BlameParams, FileBlame)>> {
let last = self.last.lock()?;
Ok(last.clone().map(|last_result| {
(last_result.params, last_result.result)
}))
}
///
pub fn refresh(&self) -> Result<()> {
if let Ok(Some(param)) = self.get_last_param() {
self.clear_current()?;
self.request(param)?;
}
Ok(())
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed) > 0
}
///
pub fn request(
&self,
params: BlameParams,
) -> Result<Option<FileBlame>> {
log::trace!("request");
let hash = hash(&params);
{
let mut current = self.current.lock()?;
if current.0 == hash {
return Ok(current.1.clone());
}
current.0 = hash;
current.1 = None;
}
let arc_current = Arc::clone(&self.current);
let arc_last = Arc::clone(&self.last);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
let repo = self.repo.clone();
self.pending.fetch_add(1, Ordering::Relaxed);
rayon_core::spawn(move || {
let notify = Self::get_blame_helper(
&repo,
params,
&arc_last,
&arc_current,
hash,
);
let notify = match notify {
Err(err) => {
log::error!("get_blame_helper error: {err}");
true
}
Ok(notify) => notify,
};
arc_pending.fetch_sub(1, Ordering::Relaxed);
sender
.send(if notify {
AsyncGitNotification::Blame
} else {
AsyncGitNotification::FinishUnchanged
})
.expect("error sending blame");
});
Ok(None)
}
fn get_blame_helper(
repo_path: &RepoPath,
params: BlameParams,
arc_last: &Arc<
Mutex<Option<LastResult<BlameParams, FileBlame>>>,
>,
arc_current: &Arc<Mutex<Request<u64, FileBlame>>>,
hash: u64,
) -> Result<bool> {
let file_blame = sync::blame::blame_file(
repo_path,
&params.file_path,
params.commit_id,
)?;
let mut notify = false;
{
let mut current = arc_current.lock()?;
if current.0 == hash {
current.1 = Some(file_blame.clone());
notify = true;
}
}
{
let mut last = arc_last.lock()?;
*last = Some(LastResult {
result: file_blame,
params,
});
}
Ok(notify)
}
fn get_last_param(&self) -> Result<Option<BlameParams>> {
Ok(self
.last
.lock()?
.clone()
.map(|last_result| last_result.params))
}
fn clear_current(&self) -> Result<()> {
let mut current = self.current.lock()?;
current.0 = 0;
current.1 = None;
Ok(())
}
}

77
asyncgit/src/branches.rs Normal file
View file

@ -0,0 +1,77 @@
use crate::{
asyncjob::{AsyncJob, RunParams},
error::Result,
sync::{branch::get_branches_info, BranchInfo, RepoPath},
AsyncGitNotification,
};
use std::sync::{Arc, Mutex};
enum JobState {
Request {
local_branches: bool,
repo: RepoPath,
},
Response(Result<Vec<BranchInfo>>),
}
///
#[derive(Clone, Default)]
pub struct AsyncBranchesJob {
state: Arc<Mutex<Option<JobState>>>,
}
///
impl AsyncBranchesJob {
///
pub fn new(repo: RepoPath, local_branches: bool) -> Self {
Self {
state: Arc::new(Mutex::new(Some(JobState::Request {
repo,
local_branches,
}))),
}
}
///
pub fn result(&self) -> Option<Result<Vec<BranchInfo>>> {
if let Ok(mut state) = self.state.lock() {
if let Some(state) = state.take() {
return match state {
JobState::Request { .. } => None,
JobState::Response(result) => Some(result),
};
}
}
None
}
}
impl AsyncJob for AsyncBranchesJob {
type Notification = AsyncGitNotification;
type Progress = ();
fn run(
&mut self,
_params: RunParams<Self::Notification, Self::Progress>,
) -> Result<Self::Notification> {
if let Ok(mut state) = self.state.lock() {
*state = state.take().map(|state| match state {
JobState::Request {
local_branches,
repo,
} => {
let branches =
get_branches_info(&repo, local_branches);
JobState::Response(branches)
}
JobState::Response(result) => {
JobState::Response(result)
}
});
}
Ok(AsyncGitNotification::Branches)
}
}

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