Adding upstream version 0.6.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
4de83856e9
commit
5b48f7aed6
21 changed files with 5187 additions and 0 deletions
6
.cargo_vcs_info.json
Normal file
6
.cargo_vcs_info.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"git": {
|
||||
"sha1": "7b9bb72a310243cc53d906d1e7ec3c9aad1c75d2"
|
||||
},
|
||||
"path_in_vcs": ""
|
||||
}
|
25
.github/workflows/crates-io.yml
vendored
Normal file
25
.github/workflows/crates-io.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
name: Crates.io
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ created ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Publish binaries
|
||||
uses: katyo/publish-crates@v1
|
||||
with:
|
||||
registry-token: ${{ secrets.CRATES_IO_TOKEN }}
|
47
.github/workflows/release.yml
vendored
Normal file
47
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,47 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ created ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
REPO: git-graph
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release for ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
bin_extension: ""
|
||||
os_name: "linux-amd64"
|
||||
- os: windows-latest
|
||||
bin_extension: ".exe"
|
||||
os_name: "windows-amd64"
|
||||
- os: macos-latest
|
||||
bin_extension: ""
|
||||
os_name: "macos-amd64"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Get tag
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --release
|
||||
- name: Compress
|
||||
run: |
|
||||
cp -f target/release/$REPO${{ matrix.bin_extension }} .
|
||||
tar -czf release.tar.gz $REPO${{ matrix.bin_extension }}
|
||||
shell: bash
|
||||
- name: Upload binaries to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: release.tar.gz
|
||||
asset_name: ${{ env.REPO }}-${{ env.RELEASE_VERSION }}-${{ matrix.os_name }}.tar.gz
|
||||
tag: ${{ github.ref }}
|
58
.github/workflows/tests.yml
vendored
Normal file
58
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,58 @@
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all
|
||||
|
||||
lints:
|
||||
name: Lints
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Run cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Run cargo clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all --all-targets -- --deny warnings
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
*.iml
|
899
Cargo.lock
generated
Normal file
899
Cargo.lock
generated
Normal file
|
@ -0,0 +1,899 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91b9970d7505127a162fdaa9b96428d28a479ba78c9ec7550a63a5d9863db682"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_lex",
|
||||
"once_cell",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxxbridge-flags",
|
||||
"cxxbridge-macro",
|
||||
"link-cplusplus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"scratch",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf36e65a80337bea855cd4ef9b8401ffce06a7baedf2e85ec467b1ac3f6e82b6"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git-graph"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"chrono",
|
||||
"clap",
|
||||
"crossterm",
|
||||
"git2",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"platform-dirs",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"svg",
|
||||
"textwrap",
|
||||
"toml",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"cxx-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.137"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.14.0+1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
|
||||
|
||||
[[package]]
|
||||
name = "platform-dirs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e188d043c1a692985f78b5464853a263f1a27e5bd6322bad3a4078ee3c998a38"
|
||||
dependencies = [
|
||||
"dirs-next",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"redox_syscall",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
|
||||
dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "scratch"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "svg"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6e6ff893392e6a1eb94a210562432c6380cebf09d30962a012a655f7dde2ff8"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
91
Cargo.toml
Normal file
91
Cargo.toml
Normal file
|
@ -0,0 +1,91 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "git-graph"
|
||||
version = "0.6.0"
|
||||
authors = ["Martin Lange <martin_lange_@gmx.net>"]
|
||||
description = "Command line tool to show clear git graphs arranged for your branching model"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"git",
|
||||
"graph",
|
||||
]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/mlange-42/git-graph.git"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
debug = 0
|
||||
debug-assertions = false
|
||||
overflow-checks = false
|
||||
|
||||
[dependencies.atty]
|
||||
version = "0.2"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
optional = false
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.0"
|
||||
features = ["cargo"]
|
||||
optional = false
|
||||
|
||||
[dependencies.crossterm]
|
||||
version = "0.25"
|
||||
optional = false
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.15"
|
||||
optional = false
|
||||
default-features = false
|
||||
|
||||
[dependencies.itertools]
|
||||
version = "0.10"
|
||||
|
||||
[dependencies.lazy_static]
|
||||
version = "1.4"
|
||||
|
||||
[dependencies.platform-dirs]
|
||||
version = "0.3"
|
||||
|
||||
[dependencies.regex]
|
||||
version = "1.7"
|
||||
features = ["std"]
|
||||
optional = false
|
||||
default-features = false
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
|
||||
[dependencies.serde_derive]
|
||||
version = "1.0"
|
||||
optional = false
|
||||
default-features = false
|
||||
|
||||
[dependencies.svg]
|
||||
version = "0.12"
|
||||
|
||||
[dependencies.textwrap]
|
||||
version = "0.16"
|
||||
features = ["unicode-width"]
|
||||
optional = false
|
||||
default-features = false
|
||||
|
||||
[dependencies.toml]
|
||||
version = "0.5"
|
||||
|
||||
[dependencies.yansi]
|
||||
version = "0.5"
|
35
Cargo.toml.orig
generated
Normal file
35
Cargo.toml.orig
generated
Normal file
|
@ -0,0 +1,35 @@
|
|||
[package]
|
||||
name = "git-graph"
|
||||
version = "0.6.0"
|
||||
authors = ["Martin Lange <martin_lange_@gmx.net>"]
|
||||
description = "Command line tool to show clear git graphs arranged for your branching model"
|
||||
repository = "https://github.com/mlange-42/git-graph.git"
|
||||
keywords = ["git", "graph"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
debug = false
|
||||
debug-assertions = false
|
||||
overflow-checks = false
|
||||
|
||||
[dependencies]
|
||||
git2 = {version = "0.15", default-features = false, optional = false}
|
||||
regex = {version = "1.7", default-features = false, optional = false, features = ["std"]}
|
||||
serde = "1.0"
|
||||
serde_derive = {version = "1.0", default-features = false, optional = false}
|
||||
toml = "0.5"
|
||||
itertools = "0.10"
|
||||
svg = "0.12"
|
||||
clap = {version = "4.0", optional = false, features = ["cargo"]}
|
||||
lazy_static = "1.4"
|
||||
yansi = "0.5"
|
||||
atty = "0.2"
|
||||
platform-dirs = "0.3"
|
||||
crossterm = {version = "0.25", optional = false}
|
||||
chrono = {version = "0.4", optional = false}
|
||||
textwrap = {version = "0.16", default-features = false, optional = false, features = ["unicode-width"]}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Martin Lange
|
||||
|
||||
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.
|
111
README.md
Normal file
111
README.md
Normal file
|
@ -0,0 +1,111 @@
|
|||
# git-graph
|
||||
|
||||
[](https://github.com/mlange-42/git-graph/actions/workflows/tests.yml)
|
||||
[](https://github.com/mlange-42/git-graph)
|
||||
[](https://crates.io/crates/git-graph)
|
||||
[](https://github.com/mlange-42/git-graph/blob/master/LICENSE)
|
||||
|
||||
A command line tool to visualize Git history graphs in a comprehensible way, following different branching models.
|
||||
|
||||
The image below shows an example using the [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) branching model for a comparison between graphs generated by git-graph (far left) versus other tools and Git clients.
|
||||
|
||||
> GitFlow was chosen for its complexity, while any other branching model is supported, including user-defined ones.
|
||||
|
||||

|
||||
|
||||
Decide for yourself which graph is the most comprehensible. :sunglasses:
|
||||
|
||||
If you want an **interactive Git terminal application**, see [**git-igitt**](https://github.com/mlange-42/git-igitt), which is based on git-graph.
|
||||
|
||||
## Features
|
||||
|
||||
* View structured graphs directly in the terminal
|
||||
* Pre-defined and custom branching models and coloring
|
||||
* Different styles, including ASCII-only (i.e. no "special characters")
|
||||
* Custom commit formatting, like with `git log --format="..."`
|
||||
|
||||
## Installation
|
||||
|
||||
**Pre-compiled binaries**
|
||||
|
||||
1. Download the [latest binaries](https://github.com/mlange-42/git-graph/releases) for your platform
|
||||
2. Unzip somewhere
|
||||
3. *Optional:* add directory `git-graph` to your `PATH` environmental variable
|
||||
|
||||
**Using `cargo`**
|
||||
|
||||
In case you have [Rust](https://www.rust-lang.org/) installed, you can install with `cargo`:
|
||||
|
||||
```
|
||||
cargo install git-graph
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**For detailed information, see the [manual](docs/manual.md)**.
|
||||
|
||||
For basic usage, run the following command inside a Git repository's folder:
|
||||
|
||||
```
|
||||
git-graph
|
||||
```
|
||||
|
||||
> Note: git-graph needs to be on the PATH, or you need use the full path to git-graph:
|
||||
>
|
||||
> ```
|
||||
> C:/path/to/git-graph/git-graph
|
||||
> ```
|
||||
|
||||
**Branching models**
|
||||
|
||||
Run git-graph with a specific model, e.g. `simple`:
|
||||
|
||||
```
|
||||
git-graph --model simple
|
||||
```
|
||||
|
||||
Alternatively, set the model for the current repository permanently:
|
||||
|
||||
```
|
||||
git-graph model simple
|
||||
```
|
||||
|
||||
**Get help**
|
||||
|
||||
For the full CLI help describing all options, use:
|
||||
|
||||
```
|
||||
git-graph -h
|
||||
git-graph --help
|
||||
```
|
||||
|
||||
For **styles** and commit **formatting**, see the [manual](docs/manual.md).
|
||||
|
||||
## Custom branching models
|
||||
|
||||
Branching models are configured using the files in `APP_DATA/git-graph/models`.
|
||||
|
||||
* Windows: `C:\Users\<user>\AppData\Roaming\git-graph`
|
||||
* Linux: `~/.config/git-graph`
|
||||
* OSX: `~/Library/Application Support/git-graph`
|
||||
|
||||
File names of any `.toml` files in the `models` directory can be used in parameter `--model`, or via sub-command `model`. E.g., to use a branching model defined in `my-model.toml`, use:
|
||||
|
||||
```
|
||||
git-graph --model my-model
|
||||
```
|
||||
|
||||
**For details on how to create your own branching models see the manual, section [Custom branching models](docs/manual.md#custom-branching-models).**
|
||||
|
||||
## Limitations
|
||||
|
||||
* Summaries of merge commits (i.e. 1st line of message) should not be modified! git-graph needs them to categorize merged branches.
|
||||
* Supports only the primary remote repository `origin`.
|
||||
* Does currently not support "octopus merges" (i.e. no more than 2 parents)
|
||||
* On Windows PowerShell, piping to file output does not work properly (changes encoding), so you may want to use the default Windows console instead
|
||||
|
||||
## Contributing
|
||||
|
||||
Please report any issues and feature requests in the [issue tracker](https://github.com/mlange-42/git-graph/issues).
|
||||
|
||||
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
326
docs/manual.md
Normal file
326
docs/manual.md
Normal file
|
@ -0,0 +1,326 @@
|
|||
# git-graph manual
|
||||
|
||||
**Content**
|
||||
|
||||
* [Overview](#overview)
|
||||
* [Options](#options)
|
||||
* [Formatting](#formatting)
|
||||
* [Custom branching models](#custom-branching-models)
|
||||
|
||||
## Overview
|
||||
|
||||
The most basic usage is to simply call git-graph from inside a Git repository:
|
||||
|
||||
```
|
||||
git-graph
|
||||
```
|
||||
|
||||
This works also deeper down the directory tree, so no need to be in the repository's root folder.
|
||||
|
||||
Alternatively, the path to the repository to visualize can be specified with option `--path`:
|
||||
|
||||
```
|
||||
git-graph --path "path/to/repo"
|
||||
```
|
||||
|
||||
**Branching models**
|
||||
|
||||
The above call assumes the GitFlow branching model (the default). Different branching models can be used with the option `--model` or `-m`:
|
||||
|
||||
```
|
||||
git-graph --model simple
|
||||
```
|
||||
|
||||
To *permanently* set the branching model for a repository, use subcommand `model`, like
|
||||
|
||||
```
|
||||
git-graph model simple
|
||||
```
|
||||
|
||||
Use the subcommand without argument to view the currently set branching model of a repository:
|
||||
|
||||
```
|
||||
git-graph model
|
||||
```
|
||||
|
||||
To view all available branching models, use option `--list` or `-l` of the subcommand:
|
||||
|
||||
```
|
||||
git-graph model --list
|
||||
```
|
||||
|
||||
For **defining your own models**, see section [Custom branching models](#custom-branching-models).
|
||||
|
||||
**Styles**
|
||||
|
||||
Git-graph supports different styles. Besides the default `normal` (alias `thin`), supported styles are `round`, `bold`, `double` and `ascii`. Use a style with option `--style` or `-s`:
|
||||
|
||||
```
|
||||
git-graph --style round
|
||||
```
|
||||
|
||||

|
||||
|
||||
Style `ascii` can be used for devices and media that do not support Unicode/UTF-8 characters.
|
||||
|
||||
**Formatting**
|
||||
|
||||
Git-graph supports predefined as well as custom commit formatting through option `--format`. Available presets follow Git: `oneline` (the default), `short`, `medium` and `full`. For details and custom formatting, see section [Formatting](#formatting).
|
||||
|
||||
For a complete list of all available options, see the next section [Options](#options).
|
||||
|
||||
## Options
|
||||
|
||||
All options are explained in the CLI help. View it with `git-graph -h`:
|
||||
|
||||
```
|
||||
Structured Git graphs for your branching model.
|
||||
https://github.com/mlange-42/git-graph
|
||||
|
||||
EXAMPES:
|
||||
git-graph -> Show graph
|
||||
git-graph --style round -> Show graph in a different style
|
||||
git-graph --model <model> -> Show graph using a certain <model>
|
||||
git-graph model --list -> List available branching models
|
||||
git-graph model -> Show repo's current branching models
|
||||
git-graph model <model> -> Permanently set model <model> for this repo
|
||||
|
||||
USAGE:
|
||||
git-graph [FLAGS] [OPTIONS] [SUBCOMMAND]
|
||||
|
||||
FLAGS:
|
||||
-d, --debug Additional debug output and graphics.
|
||||
-h, --help Prints help information
|
||||
-l, --local Show only local branches, no remotes.
|
||||
--no-color Print without colors. Missing color support should be detected
|
||||
automatically (e.g. when piping to a file).
|
||||
Overrides option '--color'
|
||||
--no-pager Use no pager (print everything at once without prompt).
|
||||
-S, --sparse Print a less compact graph: merge lines point to target lines
|
||||
rather than merge commits.
|
||||
--svg Render graph as SVG instead of text-based.
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
--color <color> Specify when colors should be used. One of [auto|always|never].
|
||||
Default: auto.
|
||||
-f, --format <format> Commit format. One of [oneline|short|medium|full|"<string>"].
|
||||
(First character can be used as abbreviation, e.g. '-f m')
|
||||
Default: oneline.
|
||||
For placeholders supported in "<string>", consult 'git-graph --help'
|
||||
-n, --max-count <n> Maximum number of commits
|
||||
-m, --model <model> Branching model. Available presets are [simple|git-flow|none].
|
||||
Default: git-flow.
|
||||
Permanently set the model for a repository with
|
||||
> git-graph model <model>
|
||||
-p, --path <path> Open repository from this path or above. Default '.'
|
||||
-s, --style <style> Output style. One of [normal/thin|round|bold|double|ascii].
|
||||
(First character can be used as abbreviation, e.g. '-s r')
|
||||
-w, --wrap <wrap> Line wrapping for formatted commit text. Default: 'auto 0 8'
|
||||
Argument format: [<width>|auto|none[ <indent1>[ <indent2>]]]
|
||||
For examples, consult 'git-graph --help'
|
||||
|
||||
SUBCOMMANDS:
|
||||
help Prints this message or the help of the given subcommand(s)
|
||||
model Prints or permanently sets the branching model for a repository.
|
||||
```
|
||||
|
||||
For longer explanations, use `git-graph --help`.
|
||||
|
||||
## Formatting
|
||||
|
||||
Formatting can be specified with the `--format` option.
|
||||
|
||||
Predefined formats are `oneline` (the default), `short`, `medium` and `full`. They should behave like the Git formatting presets described in the [Git documentation](https://git-scm.com/docs/pretty-formats).
|
||||
|
||||
**oneline**
|
||||
|
||||
```
|
||||
<hash> [<refs>] <title line>
|
||||
```
|
||||
|
||||
**short**
|
||||
|
||||
```
|
||||
commit <hash> [<refs>]
|
||||
Author: <author>
|
||||
|
||||
<title line>
|
||||
```
|
||||
|
||||
**medium**
|
||||
|
||||
```
|
||||
commit <hash> [<refs>]
|
||||
Author: <author>
|
||||
Date: <author date>
|
||||
|
||||
<title line>
|
||||
|
||||
<full commit message>
|
||||
```
|
||||
|
||||
**full**
|
||||
|
||||
```
|
||||
commit <hash> [<refs>]
|
||||
Author: <author>
|
||||
Commit: <committer>
|
||||
Date: <author date>
|
||||
|
||||
<title line>
|
||||
|
||||
<full commit message>
|
||||
```
|
||||
|
||||
### Custom formatting
|
||||
|
||||
Formatting strings use a subset of the placeholders available in `git log --format="..."`:
|
||||
|
||||
| Placeholder | Replaced with |
|
||||
| ----------- | ------------------------------------------- |
|
||||
| %n | newline |
|
||||
| %H | commit hash |
|
||||
| %h | abbreviated commit hash |
|
||||
| %P | parent commit hashes |
|
||||
| %p | abbreviated parent commit hashes |
|
||||
| %d | refs (branches, tags) |
|
||||
| %s | commit summary |
|
||||
| %b | commit message body |
|
||||
| %B | raw body (subject and body) |
|
||||
| %an | author name |
|
||||
| %ae | author email |
|
||||
| %ad | author date |
|
||||
| %as | author date in short format `YYYY-MM-DD` |
|
||||
| %cn | committer name |
|
||||
| %ce | committer email |
|
||||
| %cd | committer date |
|
||||
| %cs | committer date in short format `YYYY-MM-DD` |
|
||||
|
||||
If you add a '+' (plus sign) after % of a placeholder, a line-feed is inserted immediately before the expansion if and only if the placeholder expands to a non-empty string.
|
||||
|
||||
If you add a '-' (minus sign) after % of a placeholder, all consecutive line-feeds immediately preceding the expansion are deleted if and only if the placeholder expands to an empty string.
|
||||
|
||||
If you add a ' ' (space) after % of a placeholder, a space is inserted immediately before the expansion if and only if the placeholder expands to a non-empty string.
|
||||
|
||||
See also the [Git documentation](https://git-scm.com/docs/pretty-formats).
|
||||
|
||||
More formatting placeholders are planned for later releases.
|
||||
|
||||
**Examples**
|
||||
|
||||
Format recreating `oneline`:
|
||||
|
||||
```
|
||||
git-graph --format "%h%d %s"
|
||||
```
|
||||
|
||||
Format similar to `short`:
|
||||
|
||||
```
|
||||
git-graph --format "commit %H%nAuthor: %an %ae%n%n %s%n"
|
||||
```
|
||||
|
||||
## Custom branching models
|
||||
|
||||
Branching models are configured using the files in `APP_DATA/git-graph/models`.
|
||||
|
||||
* Windows: `C:\Users\<user>\AppData\Roaming\git-graph`
|
||||
* Linux: `~/.config/git-graph`
|
||||
* OSX: `~/Library/Application Support/git-graph`
|
||||
|
||||
File names of any `.toml` files in the `models` directory can be used in parameter `--model`, or via sub-command `model`. E.g., to use a branching model defined in `my-model.toml`, use:
|
||||
|
||||
```
|
||||
git-graph --model my-model
|
||||
```
|
||||
|
||||
**Branching model files** are in [TOML](https://toml.io/en/) format and have several sections, relying on Regular Expressions to categorize branches. The listing below shows the `git-flow` model (slightly abbreviated) with explanatory comments.
|
||||
|
||||
```toml
|
||||
# RegEx patterns for branch groups by persistence, from most persistent
|
||||
# to most short-leved branches. This is used to back-trace branches.
|
||||
# Branches not matching any pattern are assumed least persistent.
|
||||
persistence = [
|
||||
'^(master|main)$', # Matches exactly `master` or `main`
|
||||
'^(develop|dev)$',
|
||||
'^feature.*$', # Matches everything starting with `feature`
|
||||
'^release.*$',
|
||||
'^hotfix.*$',
|
||||
'^bugfix.*$',
|
||||
]
|
||||
|
||||
# RegEx patterns for visual ordering of branches, from left to right.
|
||||
# Here, `master` or `main` are shown left-most, followed by branches
|
||||
# starting with `hotfix` or `release`, followed by `develop` or `dev`.
|
||||
# Branches not matching any pattern (e.g. starting with `feature`)
|
||||
# are displayed further to the right.
|
||||
order = [
|
||||
'^(master|main)$', # Matches exactly `master` or `main`
|
||||
'^(hotfix|release).*$', # Matches everything starting with `hotfix` or `release`
|
||||
'^(develop|dev)$', # Matches exactly `develop` or `dev`
|
||||
]
|
||||
|
||||
# Colors of branches in terminal output.
|
||||
# For supported colors, see section Colors (below this listing).
|
||||
[terminal_colors]
|
||||
# Each entry is composed of a RegEx pattern and a list of colors that
|
||||
# will be used alternating (see e.g. `feature...`).
|
||||
matches = [
|
||||
[
|
||||
'^(master|main)$',
|
||||
['bright_blue'],
|
||||
],
|
||||
[
|
||||
'^(develop|dev)$',
|
||||
['bright_yellow'],
|
||||
],
|
||||
[ # Branches obviously merged in from forks are prefixed with 'fork/'.
|
||||
# The 'fork/' prefix is only available in order and colors, but not in persistence!
|
||||
'^(feature|fork/).*$',
|
||||
['bright_magenta', 'bright_cyan'], # Multiple colors for alternating use
|
||||
],
|
||||
[
|
||||
'^release.*$',
|
||||
['bright_green'],
|
||||
],
|
||||
[
|
||||
'^(bugfix|hotfix).*$',
|
||||
['bright_red'],
|
||||
],
|
||||
[
|
||||
'^tags/.*$',
|
||||
['bright_green'],
|
||||
],
|
||||
]
|
||||
# A list of colors that are used (alternating) for all branches
|
||||
# not matching any of the above pattern.
|
||||
unknown = ['white']
|
||||
|
||||
# Colors of branches in SVG output.
|
||||
# Same structure as terminal_colors.
|
||||
# For supported colors, see section Colors (below this listing).
|
||||
[svg_colors]
|
||||
matches = [
|
||||
[
|
||||
'^(master|main)$',
|
||||
['blue'],
|
||||
],
|
||||
[
|
||||
'...',
|
||||
]
|
||||
]
|
||||
unknown = ['gray']
|
||||
```
|
||||
|
||||
**Tags**
|
||||
|
||||
Internally, all tags start with `tag/`. To match Git tags, use RegEx patterns like `^tags/.*$`. However, only tags that are not on any branch are ordered and colored separately.
|
||||
|
||||
**Colors**
|
||||
|
||||
**Terminal colors** support the 8 system color names `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan` and `white`, as well as each of them prefixed with `bright_` (e.g. `bright_blue`).
|
||||
|
||||
Further, indices of the 256-color palette are supported. For a full list, see [here](https://jonasjacek.github.io/colors/). Indices must be quoted as strings (e.g. `'16'`)
|
||||
|
||||
**SVG colors** support all named web colors (full list [here](https://htmlcolorcodes.com/color-names/)), as well as RGB colors in hex notation, like `#ffffff`.
|
153
src/config.rs
Normal file
153
src/config.rs
Normal file
|
@ -0,0 +1,153 @@
|
|||
use crate::settings::{BranchSettingsDef, RepoSettings};
|
||||
use git2::Repository;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Creates the directory `APP_DATA/git-graph/models` if it does not exist,
|
||||
/// and writes the files for built-in branching models there.
|
||||
pub fn create_config<P: AsRef<Path> + AsRef<OsStr>>(app_model_path: &P) -> Result<(), String> {
|
||||
let path: &Path = app_model_path.as_ref();
|
||||
if !path.exists() {
|
||||
std::fs::create_dir_all(app_model_path).map_err(|err| err.to_string())?;
|
||||
|
||||
let models = [
|
||||
(BranchSettingsDef::git_flow(), "git-flow.toml"),
|
||||
(BranchSettingsDef::simple(), "simple.toml"),
|
||||
(BranchSettingsDef::none(), "none.toml"),
|
||||
];
|
||||
for (model, file) in &models {
|
||||
let mut path = PathBuf::from(&app_model_path);
|
||||
path.push(file);
|
||||
let str = toml::to_string_pretty(&model).map_err(|err| err.to_string())?;
|
||||
std::fs::write(&path, str).map_err(|err| err.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get models available in `APP_DATA/git-graph/models`.
|
||||
pub fn get_available_models<P: AsRef<Path>>(app_model_path: &P) -> Result<Vec<String>, String> {
|
||||
let models = std::fs::read_dir(app_model_path)
|
||||
.map_err(|err| err.to_string())?
|
||||
.filter_map(|e| match e {
|
||||
Ok(e) => {
|
||||
if let (Some(name), Some(ext)) = (e.path().file_name(), e.path().extension()) {
|
||||
if ext == "toml" {
|
||||
name.to_str()
|
||||
.map(|name| (name[..(name.len() - 5)]).to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(models)
|
||||
}
|
||||
|
||||
/// Get the currently set branching model for a repo.
|
||||
pub fn get_model_name(repository: &Repository, file_name: &str) -> Result<Option<String>, String> {
|
||||
let mut config_path = PathBuf::from(repository.path());
|
||||
config_path.push(file_name);
|
||||
|
||||
if config_path.exists() {
|
||||
let repo_config: RepoSettings =
|
||||
toml::from_str(&std::fs::read_to_string(config_path).map_err(|err| err.to_string())?)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
Ok(Some(repo_config.model))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to get the branch settings for a given model.
|
||||
/// If no model name is given, returns the branch settings set for the repo, or the default otherwise.
|
||||
pub fn get_model<P: AsRef<Path> + AsRef<OsStr>>(
|
||||
repository: &Repository,
|
||||
model: Option<&str>,
|
||||
repo_config_file: &str,
|
||||
app_model_path: &P,
|
||||
) -> Result<BranchSettingsDef, String> {
|
||||
match model {
|
||||
Some(model) => read_model(model, app_model_path),
|
||||
None => {
|
||||
let mut config_path = PathBuf::from(repository.path());
|
||||
config_path.push(repo_config_file);
|
||||
|
||||
if config_path.exists() {
|
||||
let repo_config: RepoSettings = toml::from_str(
|
||||
&std::fs::read_to_string(config_path).map_err(|err| err.to_string())?,
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
read_model(&repo_config.model, app_model_path)
|
||||
} else {
|
||||
Ok(read_model("git-flow", app_model_path)
|
||||
.unwrap_or_else(|_| BranchSettingsDef::git_flow()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a branching model file.
|
||||
fn read_model<P: AsRef<Path> + AsRef<OsStr>>(
|
||||
model: &str,
|
||||
app_model_path: &P,
|
||||
) -> Result<BranchSettingsDef, String> {
|
||||
let mut model_file = PathBuf::from(&app_model_path);
|
||||
model_file.push(format!("{}.toml", model));
|
||||
|
||||
if model_file.exists() {
|
||||
toml::from_str::<BranchSettingsDef>(
|
||||
&std::fs::read_to_string(model_file).map_err(|err| err.to_string())?,
|
||||
)
|
||||
.map_err(|err| err.to_string())
|
||||
} else {
|
||||
let models = get_available_models(&app_model_path)?;
|
||||
let path: &Path = app_model_path.as_ref();
|
||||
Err(format!(
|
||||
"ERROR: No branching model named '{}' found in {}\n Available models are: {}",
|
||||
model,
|
||||
path.display(),
|
||||
itertools::join(models, ", ")
|
||||
))
|
||||
}
|
||||
}
|
||||
/// Permanently sets the branching model for a repository
|
||||
pub fn set_model<P: AsRef<Path>>(
|
||||
repository: &Repository,
|
||||
model: &str,
|
||||
repo_config_file: &str,
|
||||
app_model_path: &P,
|
||||
) -> Result<(), String> {
|
||||
let models = get_available_models(&app_model_path)?;
|
||||
|
||||
if !models.contains(&model.to_string()) {
|
||||
return Err(format!(
|
||||
"ERROR: No branching model named '{}' found in {}\n Available models are: {}",
|
||||
model,
|
||||
app_model_path.as_ref().display(),
|
||||
itertools::join(models, ", ")
|
||||
));
|
||||
}
|
||||
|
||||
let mut config_path = PathBuf::from(repository.path());
|
||||
config_path.push(repo_config_file);
|
||||
|
||||
let config = RepoSettings {
|
||||
model: model.to_string(),
|
||||
};
|
||||
|
||||
let str = toml::to_string_pretty(&config).map_err(|err| err.to_string())?;
|
||||
std::fs::write(&config_path, str).map_err(|err| err.to_string())?;
|
||||
|
||||
eprint!("Branching model set to '{}'", model);
|
||||
|
||||
Ok(())
|
||||
}
|
984
src/graph.rs
Normal file
984
src/graph.rs
Normal file
|
@ -0,0 +1,984 @@
|
|||
//! A graph structure representing the history of a Git repository.
|
||||
|
||||
use crate::print::colors::to_terminal_color;
|
||||
use crate::settings::{BranchOrder, BranchSettings, MergePatterns, Settings};
|
||||
use git2::{BranchType, Commit, Error, Oid, Reference, Repository};
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
const ORIGIN: &str = "origin/";
|
||||
const FORK: &str = "fork/";
|
||||
|
||||
/// Represents a git history graph.
|
||||
pub struct GitGraph {
|
||||
pub repository: Repository,
|
||||
pub commits: Vec<CommitInfo>,
|
||||
/// Mapping from commit id to index in `commits`
|
||||
pub indices: HashMap<Oid, usize>,
|
||||
/// All detected branches and tags, including merged and deleted
|
||||
pub all_branches: Vec<BranchInfo>,
|
||||
/// Indices of all real (still existing) branches in `all_branches`
|
||||
pub branches: Vec<usize>,
|
||||
/// Indices of all tags in `all_branches`
|
||||
pub tags: Vec<usize>,
|
||||
/// The current HEAD
|
||||
pub head: HeadInfo,
|
||||
}
|
||||
|
||||
impl GitGraph {
|
||||
pub fn new(
|
||||
mut repository: Repository,
|
||||
settings: &Settings,
|
||||
max_count: Option<usize>,
|
||||
) -> Result<Self, String> {
|
||||
let mut stashes = HashSet::new();
|
||||
repository
|
||||
.stash_foreach(|_, _, oid| {
|
||||
stashes.insert(*oid);
|
||||
true
|
||||
})
|
||||
.map_err(|err| err.message().to_string())?;
|
||||
|
||||
let mut walk = repository
|
||||
.revwalk()
|
||||
.map_err(|err| err.message().to_string())?;
|
||||
|
||||
walk.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME)
|
||||
.map_err(|err| err.message().to_string())?;
|
||||
|
||||
walk.push_glob("*")
|
||||
.map_err(|err| err.message().to_string())?;
|
||||
|
||||
if repository.is_shallow() {
|
||||
return Err("ERROR: git-graph does not support shallow clones due to a missing feature in the underlying libgit2 library.".to_string());
|
||||
}
|
||||
|
||||
let head = HeadInfo::new(&repository.head().map_err(|err| err.message().to_string())?)?;
|
||||
|
||||
let mut commits = Vec::new();
|
||||
let mut indices = HashMap::new();
|
||||
let mut idx = 0;
|
||||
for oid in walk {
|
||||
if let Some(max) = max_count {
|
||||
if idx >= max {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Ok(oid) = oid {
|
||||
if !stashes.contains(&oid) {
|
||||
let commit = repository.find_commit(oid).unwrap();
|
||||
|
||||
commits.push(CommitInfo::new(&commit));
|
||||
indices.insert(oid, idx);
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assign_children(&mut commits, &indices);
|
||||
|
||||
let mut all_branches = assign_branches(&repository, &mut commits, &indices, settings)?;
|
||||
correct_fork_merges(&commits, &indices, &mut all_branches, settings)?;
|
||||
assign_sources_targets(&commits, &indices, &mut all_branches);
|
||||
|
||||
let (shortest_first, forward) = match settings.branch_order {
|
||||
BranchOrder::ShortestFirst(fwd) => (true, fwd),
|
||||
BranchOrder::LongestFirst(fwd) => (false, fwd),
|
||||
};
|
||||
|
||||
assign_branch_columns(
|
||||
&commits,
|
||||
&indices,
|
||||
&mut all_branches,
|
||||
&settings.branches,
|
||||
shortest_first,
|
||||
forward,
|
||||
);
|
||||
|
||||
let filtered_commits: Vec<CommitInfo> = commits
|
||||
.into_iter()
|
||||
.filter(|info| info.branch_trace.is_some())
|
||||
.collect();
|
||||
|
||||
let filtered_indices: HashMap<Oid, usize> = filtered_commits
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, info)| (info.oid, idx))
|
||||
.collect();
|
||||
|
||||
let index_map: HashMap<usize, Option<&usize>> = indices
|
||||
.iter()
|
||||
.map(|(oid, index)| (*index, filtered_indices.get(oid)))
|
||||
.collect();
|
||||
|
||||
for branch in all_branches.iter_mut() {
|
||||
if let Some(mut start_idx) = branch.range.0 {
|
||||
let mut idx0 = index_map[&start_idx];
|
||||
while idx0.is_none() {
|
||||
start_idx += 1;
|
||||
idx0 = index_map[&start_idx];
|
||||
}
|
||||
branch.range.0 = Some(*idx0.unwrap());
|
||||
}
|
||||
if let Some(mut end_idx) = branch.range.1 {
|
||||
let mut idx0 = index_map[&end_idx];
|
||||
while idx0.is_none() {
|
||||
end_idx -= 1;
|
||||
idx0 = index_map[&end_idx];
|
||||
}
|
||||
branch.range.1 = Some(*idx0.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
let branches = all_branches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, br)| {
|
||||
if !br.is_merged && !br.is_tag {
|
||||
Some(idx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let tags = all_branches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, br)| {
|
||||
if !br.is_merged && br.is_tag {
|
||||
Some(idx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(GitGraph {
|
||||
repository,
|
||||
commits: filtered_commits,
|
||||
indices: filtered_indices,
|
||||
all_branches,
|
||||
branches,
|
||||
tags,
|
||||
head,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn take_repository(self) -> Repository {
|
||||
self.repository
|
||||
}
|
||||
|
||||
pub fn commit(&self, id: Oid) -> Result<Commit, Error> {
|
||||
self.repository.find_commit(id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the current HEAD
|
||||
pub struct HeadInfo {
|
||||
pub oid: Oid,
|
||||
pub name: String,
|
||||
pub is_branch: bool,
|
||||
}
|
||||
impl HeadInfo {
|
||||
fn new(head: &Reference) -> Result<Self, String> {
|
||||
let name = head.name().ok_or_else(|| "No name for HEAD".to_string())?;
|
||||
let name = if name == "HEAD" {
|
||||
name.to_string()
|
||||
} else {
|
||||
name[11..].to_string()
|
||||
};
|
||||
|
||||
let h = HeadInfo {
|
||||
oid: head.target().ok_or_else(|| "No id for HEAD".to_string())?,
|
||||
name,
|
||||
is_branch: head.is_branch(),
|
||||
};
|
||||
Ok(h)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a commit.
|
||||
pub struct CommitInfo {
|
||||
pub oid: Oid,
|
||||
pub is_merge: bool,
|
||||
pub parents: [Option<Oid>; 2],
|
||||
pub children: Vec<Oid>,
|
||||
pub branches: Vec<usize>,
|
||||
pub tags: Vec<usize>,
|
||||
pub branch_trace: Option<usize>,
|
||||
}
|
||||
|
||||
impl CommitInfo {
|
||||
fn new(commit: &Commit) -> Self {
|
||||
CommitInfo {
|
||||
oid: commit.id(),
|
||||
is_merge: commit.parent_count() > 1,
|
||||
parents: [commit.parent_id(0).ok(), commit.parent_id(1).ok()],
|
||||
children: Vec::new(),
|
||||
branches: Vec::new(),
|
||||
tags: Vec::new(),
|
||||
branch_trace: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a branch (real or derived from merge summary).
|
||||
pub struct BranchInfo {
|
||||
pub target: Oid,
|
||||
pub merge_target: Option<Oid>,
|
||||
pub source_branch: Option<usize>,
|
||||
pub target_branch: Option<usize>,
|
||||
pub name: String,
|
||||
pub persistence: u8,
|
||||
pub is_remote: bool,
|
||||
pub is_merged: bool,
|
||||
pub is_tag: bool,
|
||||
pub visual: BranchVis,
|
||||
pub range: (Option<usize>, Option<usize>),
|
||||
}
|
||||
impl BranchInfo {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
target: Oid,
|
||||
merge_target: Option<Oid>,
|
||||
name: String,
|
||||
persistence: u8,
|
||||
is_remote: bool,
|
||||
is_merged: bool,
|
||||
is_tag: bool,
|
||||
visual: BranchVis,
|
||||
end_index: Option<usize>,
|
||||
) -> Self {
|
||||
BranchInfo {
|
||||
target,
|
||||
merge_target,
|
||||
target_branch: None,
|
||||
source_branch: None,
|
||||
name,
|
||||
persistence,
|
||||
is_remote,
|
||||
is_merged,
|
||||
is_tag,
|
||||
visual,
|
||||
range: (end_index, None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Branch properties for visualization.
|
||||
pub struct BranchVis {
|
||||
/// The branch's column group (left to right)
|
||||
pub order_group: usize,
|
||||
/// The branch's merge target column group (left to right)
|
||||
pub target_order_group: Option<usize>,
|
||||
/// The branch's source branch column group (left to right)
|
||||
pub source_order_group: Option<usize>,
|
||||
/// The branch's terminal color (index in 256-color palette)
|
||||
pub term_color: u8,
|
||||
/// SVG color (name or RGB in hex annotation)
|
||||
pub svg_color: String,
|
||||
/// The column the branch is located in
|
||||
pub column: Option<usize>,
|
||||
}
|
||||
|
||||
impl BranchVis {
|
||||
fn new(order_group: usize, term_color: u8, svg_color: String) -> Self {
|
||||
BranchVis {
|
||||
order_group,
|
||||
target_order_group: None,
|
||||
source_order_group: None,
|
||||
term_color,
|
||||
svg_color,
|
||||
column: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Walks through the commits and adds each commit's Oid to the children of its parents.
|
||||
fn assign_children(commits: &mut [CommitInfo], indices: &HashMap<Oid, usize>) {
|
||||
for idx in 0..commits.len() {
|
||||
let (oid, parents) = {
|
||||
let info = &commits[idx];
|
||||
(info.oid, info.parents)
|
||||
};
|
||||
for par_oid in &parents {
|
||||
if let Some(par_idx) = par_oid.and_then(|oid| indices.get(&oid)) {
|
||||
commits[*par_idx].children.push(oid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts branches from repository and merge summaries, assigns branches and branch traces to commits.
|
||||
///
|
||||
/// Algorithm:
|
||||
/// * Find all actual branches (incl. target oid) and all extract branches from merge summaries (incl. parent oid)
|
||||
/// * Sort all branches by persistence
|
||||
/// * Iterating over all branches in persistence order, trace back over commit parents until a trace is already assigned
|
||||
fn assign_branches(
|
||||
repository: &Repository,
|
||||
commits: &mut [CommitInfo],
|
||||
indices: &HashMap<Oid, usize>,
|
||||
settings: &Settings,
|
||||
) -> Result<Vec<BranchInfo>, String> {
|
||||
let mut branch_idx = 0;
|
||||
|
||||
let mut branches = extract_branches(repository, commits, indices, settings)?;
|
||||
|
||||
let mut index_map: Vec<_> = (0..branches.len())
|
||||
.map(|old_idx| {
|
||||
let (target, is_tag, is_merged) = {
|
||||
let branch = &branches[old_idx];
|
||||
(branch.target, branch.is_tag, branch.is_merged)
|
||||
};
|
||||
if let Some(&idx) = &indices.get(&target) {
|
||||
let info = &mut commits[idx];
|
||||
if is_tag {
|
||||
info.tags.push(old_idx);
|
||||
} else if !is_merged {
|
||||
info.branches.push(old_idx);
|
||||
}
|
||||
let oid = info.oid;
|
||||
let any_assigned =
|
||||
trace_branch(repository, commits, indices, &mut branches, oid, old_idx)
|
||||
.unwrap_or(false);
|
||||
|
||||
if any_assigned || !is_merged {
|
||||
branch_idx += 1;
|
||||
Some(branch_idx - 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut commit_count = vec![0; branches.len()];
|
||||
for info in commits.iter_mut() {
|
||||
if let Some(trace) = info.branch_trace {
|
||||
commit_count[trace] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut count_skipped = 0;
|
||||
for (idx, branch) in branches.iter().enumerate() {
|
||||
if let Some(mapped) = index_map[idx] {
|
||||
if commit_count[idx] == 0 && branch.is_merged && !branch.is_tag {
|
||||
index_map[idx] = None;
|
||||
count_skipped += 1;
|
||||
} else {
|
||||
index_map[idx] = Some(mapped - count_skipped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for info in commits.iter_mut() {
|
||||
if let Some(trace) = info.branch_trace {
|
||||
info.branch_trace = index_map[trace];
|
||||
for br in info.branches.iter_mut() {
|
||||
*br = index_map[*br].unwrap();
|
||||
}
|
||||
for tag in info.tags.iter_mut() {
|
||||
*tag = index_map[*tag].unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let branches: Vec<_> = branches
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(arr_index, branch)| {
|
||||
if index_map[arr_index].is_some() {
|
||||
Some(branch)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
fn correct_fork_merges(
|
||||
commits: &[CommitInfo],
|
||||
indices: &HashMap<Oid, usize>,
|
||||
branches: &mut [BranchInfo],
|
||||
settings: &Settings,
|
||||
) -> Result<(), String> {
|
||||
for idx in 0..branches.len() {
|
||||
if let Some(merge_target) = branches[idx]
|
||||
.merge_target
|
||||
.and_then(|oid| indices.get(&oid))
|
||||
.and_then(|idx| commits.get(*idx))
|
||||
.and_then(|info| info.branch_trace)
|
||||
.and_then(|trace| branches.get(trace))
|
||||
{
|
||||
if branches[idx].name == merge_target.name {
|
||||
let name = format!("{}{}", FORK, branches[idx].name);
|
||||
let term_col = to_terminal_color(
|
||||
&branch_color(
|
||||
&name,
|
||||
&settings.branches.terminal_colors[..],
|
||||
&settings.branches.terminal_colors_unknown,
|
||||
idx,
|
||||
)[..],
|
||||
)?;
|
||||
let pos = branch_order(&name, &settings.branches.order);
|
||||
let svg_col = branch_color(
|
||||
&name,
|
||||
&settings.branches.svg_colors,
|
||||
&settings.branches.svg_colors_unknown,
|
||||
idx,
|
||||
);
|
||||
|
||||
branches[idx].name = format!("{}{}", FORK, branches[idx].name);
|
||||
branches[idx].visual.order_group = pos;
|
||||
branches[idx].visual.term_color = term_col;
|
||||
branches[idx].visual.svg_color = svg_col;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn assign_sources_targets(
|
||||
commits: &[CommitInfo],
|
||||
indices: &HashMap<Oid, usize>,
|
||||
branches: &mut [BranchInfo],
|
||||
) {
|
||||
for idx in 0..branches.len() {
|
||||
let target_branch_idx = branches[idx]
|
||||
.merge_target
|
||||
.and_then(|oid| indices.get(&oid))
|
||||
.and_then(|idx| commits.get(*idx))
|
||||
.and_then(|info| info.branch_trace);
|
||||
|
||||
branches[idx].target_branch = target_branch_idx;
|
||||
|
||||
let group = target_branch_idx
|
||||
.and_then(|trace| branches.get(trace))
|
||||
.map(|br| br.visual.order_group);
|
||||
|
||||
branches[idx].visual.target_order_group = group;
|
||||
}
|
||||
for info in commits {
|
||||
let mut max_par_order = None;
|
||||
let mut source_branch_id = None;
|
||||
for par_oid in info.parents.iter() {
|
||||
let par_info = par_oid
|
||||
.and_then(|oid| indices.get(&oid))
|
||||
.and_then(|idx| commits.get(*idx));
|
||||
if let Some(par_info) = par_info {
|
||||
if par_info.branch_trace != info.branch_trace {
|
||||
if let Some(trace) = par_info.branch_trace {
|
||||
source_branch_id = Some(trace);
|
||||
}
|
||||
|
||||
let group = par_info
|
||||
.branch_trace
|
||||
.and_then(|trace| branches.get(trace))
|
||||
.map(|br| br.visual.order_group);
|
||||
if let Some(gr) = max_par_order {
|
||||
if let Some(p_group) = group {
|
||||
if p_group > gr {
|
||||
max_par_order = group;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
max_par_order = group;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let branch = info.branch_trace.and_then(|trace| branches.get_mut(trace));
|
||||
if let Some(branch) = branch {
|
||||
if let Some(order) = max_par_order {
|
||||
branch.visual.source_order_group = Some(order);
|
||||
}
|
||||
if let Some(source_id) = source_branch_id {
|
||||
branch.source_branch = Some(source_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts (real or derived from merge summary) and assigns basic properties.
|
||||
fn extract_branches(
|
||||
repository: &Repository,
|
||||
commits: &[CommitInfo],
|
||||
indices: &HashMap<Oid, usize>,
|
||||
settings: &Settings,
|
||||
) -> Result<Vec<BranchInfo>, String> {
|
||||
let filter = if settings.include_remote {
|
||||
None
|
||||
} else {
|
||||
Some(BranchType::Local)
|
||||
};
|
||||
let actual_branches = repository
|
||||
.branches(filter)
|
||||
.map_err(|err| err.message().to_string())?
|
||||
.collect::<Result<Vec<_>, Error>>()
|
||||
.map_err(|err| err.message().to_string())?;
|
||||
|
||||
let mut counter = 0;
|
||||
|
||||
let mut valid_branches = actual_branches
|
||||
.iter()
|
||||
.filter_map(|(br, tp)| {
|
||||
br.get().name().and_then(|n| {
|
||||
br.get().target().map(|t| {
|
||||
counter += 1;
|
||||
let start_index = match tp {
|
||||
BranchType::Local => 11,
|
||||
BranchType::Remote => 13,
|
||||
};
|
||||
let name = &n[start_index..];
|
||||
let end_index = indices.get(&t).cloned();
|
||||
|
||||
let term_color = match to_terminal_color(
|
||||
&branch_color(
|
||||
name,
|
||||
&settings.branches.terminal_colors[..],
|
||||
&settings.branches.terminal_colors_unknown,
|
||||
counter,
|
||||
)[..],
|
||||
) {
|
||||
Ok(col) => col,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
Ok(BranchInfo::new(
|
||||
t,
|
||||
None,
|
||||
name.to_string(),
|
||||
branch_order(name, &settings.branches.persistence) as u8,
|
||||
&BranchType::Remote == tp,
|
||||
false,
|
||||
false,
|
||||
BranchVis::new(
|
||||
branch_order(name, &settings.branches.order),
|
||||
term_color,
|
||||
branch_color(
|
||||
name,
|
||||
&settings.branches.svg_colors,
|
||||
&settings.branches.svg_colors_unknown,
|
||||
counter,
|
||||
),
|
||||
),
|
||||
end_index,
|
||||
))
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
|
||||
for (idx, info) in commits.iter().enumerate() {
|
||||
let commit = repository
|
||||
.find_commit(info.oid)
|
||||
.map_err(|err| err.message().to_string())?;
|
||||
if info.is_merge {
|
||||
if let Some(summary) = commit.summary() {
|
||||
counter += 1;
|
||||
|
||||
let parent_oid = commit
|
||||
.parent_id(1)
|
||||
.map_err(|err| err.message().to_string())?;
|
||||
|
||||
let branch_name = parse_merge_summary(summary, &settings.merge_patterns)
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let persistence = branch_order(&branch_name, &settings.branches.persistence) as u8;
|
||||
|
||||
let pos = branch_order(&branch_name, &settings.branches.order);
|
||||
|
||||
let term_col = to_terminal_color(
|
||||
&branch_color(
|
||||
&branch_name,
|
||||
&settings.branches.terminal_colors[..],
|
||||
&settings.branches.terminal_colors_unknown,
|
||||
counter,
|
||||
)[..],
|
||||
)?;
|
||||
let svg_col = branch_color(
|
||||
&branch_name,
|
||||
&settings.branches.svg_colors,
|
||||
&settings.branches.svg_colors_unknown,
|
||||
counter,
|
||||
);
|
||||
|
||||
let branch_info = BranchInfo::new(
|
||||
parent_oid,
|
||||
Some(info.oid),
|
||||
branch_name,
|
||||
persistence,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
BranchVis::new(pos, term_col, svg_col),
|
||||
Some(idx + 1),
|
||||
);
|
||||
valid_branches.push(branch_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
valid_branches.sort_by_cached_key(|branch| (branch.persistence, !branch.is_merged));
|
||||
|
||||
let mut tags = Vec::new();
|
||||
|
||||
repository
|
||||
.tag_foreach(|oid, name| {
|
||||
tags.push((oid, name.to_vec()));
|
||||
true
|
||||
})
|
||||
.map_err(|err| err.message().to_string())?;
|
||||
|
||||
for (oid, name) in tags {
|
||||
let name = std::str::from_utf8(&name[5..]).map_err(|err| err.to_string())?;
|
||||
|
||||
let target = repository
|
||||
.find_tag(oid)
|
||||
.map(|tag| tag.target_id())
|
||||
.or_else(|_| repository.find_commit(oid).map(|_| oid));
|
||||
|
||||
if let Ok(target_oid) = target {
|
||||
if let Some(target_index) = indices.get(&target_oid) {
|
||||
counter += 1;
|
||||
let term_col = to_terminal_color(
|
||||
&branch_color(
|
||||
name,
|
||||
&settings.branches.terminal_colors[..],
|
||||
&settings.branches.terminal_colors_unknown,
|
||||
counter,
|
||||
)[..],
|
||||
)?;
|
||||
let pos = branch_order(name, &settings.branches.order);
|
||||
let svg_col = branch_color(
|
||||
name,
|
||||
&settings.branches.svg_colors,
|
||||
&settings.branches.svg_colors_unknown,
|
||||
counter,
|
||||
);
|
||||
let tag_info = BranchInfo::new(
|
||||
target_oid,
|
||||
None,
|
||||
name.to_string(),
|
||||
settings.branches.persistence.len() as u8 + 1,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
BranchVis::new(pos, term_col, svg_col),
|
||||
Some(*target_index),
|
||||
);
|
||||
valid_branches.push(tag_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(valid_branches)
|
||||
}
|
||||
|
||||
/// Traces back branches by following 1st commit parent,
|
||||
/// until a commit is reached that already has a trace.
|
||||
fn trace_branch(
|
||||
repository: &Repository,
|
||||
commits: &mut [CommitInfo],
|
||||
indices: &HashMap<Oid, usize>,
|
||||
branches: &mut [BranchInfo],
|
||||
oid: Oid,
|
||||
branch_index: usize,
|
||||
) -> Result<bool, Error> {
|
||||
let mut curr_oid = oid;
|
||||
let mut prev_index: Option<usize> = None;
|
||||
let mut start_index: Option<i32> = None;
|
||||
let mut any_assigned = false;
|
||||
while let Some(index) = indices.get(&curr_oid) {
|
||||
let info = &mut commits[*index];
|
||||
if let Some(old_trace) = info.branch_trace {
|
||||
let (old_name, old_term, old_svg, old_range) = {
|
||||
let old_branch = &branches[old_trace];
|
||||
(
|
||||
&old_branch.name.clone(),
|
||||
old_branch.visual.term_color,
|
||||
old_branch.visual.svg_color.clone(),
|
||||
old_branch.range,
|
||||
)
|
||||
};
|
||||
let new_name = &branches[branch_index].name;
|
||||
let old_end = old_range.0.unwrap_or(0);
|
||||
let new_end = branches[branch_index].range.0.unwrap_or(0);
|
||||
if new_name == old_name && old_end >= new_end {
|
||||
let old_branch = &mut branches[old_trace];
|
||||
if let Some(old_end) = old_range.1 {
|
||||
if index > &old_end {
|
||||
old_branch.range = (None, None);
|
||||
} else {
|
||||
old_branch.range = (Some(*index), old_branch.range.1);
|
||||
}
|
||||
} else {
|
||||
old_branch.range = (Some(*index), old_branch.range.1);
|
||||
}
|
||||
} else {
|
||||
let branch = &mut branches[branch_index];
|
||||
if branch.name.starts_with(ORIGIN) && branch.name[7..] == old_name[..] {
|
||||
branch.visual.term_color = old_term;
|
||||
branch.visual.svg_color = old_svg;
|
||||
}
|
||||
match prev_index {
|
||||
None => start_index = Some(*index as i32 - 1),
|
||||
Some(prev_index) => {
|
||||
// TODO: in cases where no crossings occur, the rule for merge commits can also be applied to normal commits
|
||||
// see also print::get_deviate_index()
|
||||
if commits[prev_index].is_merge {
|
||||
let mut temp_index = prev_index;
|
||||
for sibling_oid in &commits[*index].children {
|
||||
if sibling_oid != &curr_oid {
|
||||
let sibling_index = indices[sibling_oid];
|
||||
if sibling_index > temp_index {
|
||||
temp_index = sibling_index;
|
||||
}
|
||||
}
|
||||
}
|
||||
start_index = Some(temp_index as i32);
|
||||
} else {
|
||||
start_index = Some(*index as i32 - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
info.branch_trace = Some(branch_index);
|
||||
any_assigned = true;
|
||||
|
||||
let commit = repository.find_commit(curr_oid)?;
|
||||
match commit.parent_count() {
|
||||
0 => {
|
||||
start_index = Some(*index as i32);
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
prev_index = Some(*index);
|
||||
curr_oid = commit.parent_id(0)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let branch = &mut branches[branch_index];
|
||||
if let Some(end) = branch.range.0 {
|
||||
if let Some(start_index) = start_index {
|
||||
if start_index < end as i32 {
|
||||
// TODO: find a better solution (bool field?) to identify non-deleted branches that were not assigned to any commits, and thus should not occupy a column.
|
||||
branch.range = (None, None);
|
||||
} else {
|
||||
branch.range = (branch.range.0, Some(start_index as usize));
|
||||
}
|
||||
} else {
|
||||
branch.range = (branch.range.0, None);
|
||||
}
|
||||
} else {
|
||||
branch.range = (branch.range.0, start_index.map(|si| si as usize));
|
||||
}
|
||||
Ok(any_assigned)
|
||||
}
|
||||
|
||||
/// Sorts branches into columns for visualization, that all branches can be
|
||||
/// visualizes linearly and without overlaps. Uses Shortest-First scheduling.
|
||||
fn assign_branch_columns(
|
||||
commits: &[CommitInfo],
|
||||
indices: &HashMap<Oid, usize>,
|
||||
branches: &mut [BranchInfo],
|
||||
settings: &BranchSettings,
|
||||
shortest_first: bool,
|
||||
forward: bool,
|
||||
) {
|
||||
let mut occupied: Vec<Vec<Vec<(usize, usize)>>> = vec![vec![]; settings.order.len() + 1];
|
||||
|
||||
let length_sort_factor = if shortest_first { 1 } else { -1 };
|
||||
let start_sort_factor = if forward { 1 } else { -1 };
|
||||
|
||||
let mut branches_sort: Vec<_> = branches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_idx, br)| br.range.0.is_some() || br.range.1.is_some())
|
||||
.map(|(idx, br)| {
|
||||
(
|
||||
idx,
|
||||
br.range.0.unwrap_or(0),
|
||||
br.range.1.unwrap_or(branches.len() - 1),
|
||||
br.visual
|
||||
.source_order_group
|
||||
.unwrap_or(settings.order.len() + 1),
|
||||
br.visual
|
||||
.target_order_group
|
||||
.unwrap_or(settings.order.len() + 1),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
branches_sort.sort_by_cached_key(|tup| {
|
||||
(
|
||||
std::cmp::max(tup.3, tup.4),
|
||||
(tup.2 as i32 - tup.1 as i32) * length_sort_factor,
|
||||
tup.1 as i32 * start_sort_factor,
|
||||
)
|
||||
});
|
||||
|
||||
for (branch_idx, start, end, _, _) in branches_sort {
|
||||
let branch = &branches[branch_idx];
|
||||
let group = branch.visual.order_group;
|
||||
let group_occ = &mut occupied[group];
|
||||
|
||||
let align_right = branch
|
||||
.source_branch
|
||||
.map(|src| branches[src].visual.order_group > branch.visual.order_group)
|
||||
.unwrap_or(false)
|
||||
|| branch
|
||||
.target_branch
|
||||
.map(|trg| branches[trg].visual.order_group > branch.visual.order_group)
|
||||
.unwrap_or(false);
|
||||
|
||||
let len = group_occ.len();
|
||||
let mut found = len;
|
||||
for i in 0..len {
|
||||
let index = if align_right { len - i - 1 } else { i };
|
||||
let column_occ = &group_occ[index];
|
||||
let mut occ = false;
|
||||
for (s, e) in column_occ {
|
||||
if start <= *e && end >= *s {
|
||||
occ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !occ {
|
||||
if let Some(merge_trace) = branch
|
||||
.merge_target
|
||||
.and_then(|t| indices.get(&t))
|
||||
.and_then(|t_idx| commits[*t_idx].branch_trace)
|
||||
{
|
||||
let merge_branch = &branches[merge_trace];
|
||||
if merge_branch.visual.order_group == branch.visual.order_group {
|
||||
if let Some(merge_column) = merge_branch.visual.column {
|
||||
if merge_column == index {
|
||||
occ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !occ {
|
||||
found = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let branch = &mut branches[branch_idx];
|
||||
branch.visual.column = Some(found);
|
||||
if found == group_occ.len() {
|
||||
group_occ.push(vec![]);
|
||||
}
|
||||
group_occ[found].push((start, end));
|
||||
}
|
||||
|
||||
let group_offset: Vec<usize> = occupied
|
||||
.iter()
|
||||
.scan(0, |acc, group| {
|
||||
*acc += group.len();
|
||||
Some(*acc)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for branch in branches {
|
||||
if let Some(column) = branch.visual.column {
|
||||
let offset = if branch.visual.order_group == 0 {
|
||||
0
|
||||
} else {
|
||||
group_offset[branch.visual.order_group - 1]
|
||||
};
|
||||
branch.visual.column = Some(column + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the index for a branch name from a slice of prefixes
|
||||
fn branch_order(name: &str, order: &[Regex]) -> usize {
|
||||
order
|
||||
.iter()
|
||||
.position(|b| (name.starts_with(ORIGIN) && b.is_match(&name[7..])) || b.is_match(name))
|
||||
.unwrap_or(order.len())
|
||||
}
|
||||
|
||||
/// Finds the svg color for a branch name.
|
||||
fn branch_color<T: Clone>(
|
||||
name: &str,
|
||||
order: &[(Regex, Vec<T>)],
|
||||
unknown: &[T],
|
||||
counter: usize,
|
||||
) -> T {
|
||||
let color = order
|
||||
.iter()
|
||||
.find_position(|(b, _)| {
|
||||
(name.starts_with(ORIGIN) && b.is_match(&name[7..])) || b.is_match(name)
|
||||
})
|
||||
.map(|(_pos, col)| &col.1[counter % col.1.len()])
|
||||
.unwrap_or_else(|| &unknown[counter % unknown.len()]);
|
||||
color.clone()
|
||||
}
|
||||
|
||||
/// Tries to extract the name of a merged-in branch from the merge commit summary.
|
||||
pub fn parse_merge_summary(summary: &str, patterns: &MergePatterns) -> Option<String> {
|
||||
for regex in &patterns.patterns {
|
||||
if let Some(captures) = regex.captures(summary) {
|
||||
if captures.len() == 2 && captures.get(1).is_some() {
|
||||
return captures.get(1).map(|m| m.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::settings::MergePatterns;
|
||||
|
||||
#[test]
|
||||
fn parse_merge_summary() {
|
||||
let patterns = MergePatterns::default();
|
||||
|
||||
let gitlab_pull = "Merge branch 'feature/my-feature' into 'master'";
|
||||
let git_default = "Merge branch 'feature/my-feature' into dev";
|
||||
let git_master = "Merge branch 'feature/my-feature'";
|
||||
let github_pull = "Merge pull request #1 from user-x/feature/my-feature";
|
||||
let github_pull_2 = "Merge branch 'feature/my-feature' of github.com:user-x/repo";
|
||||
let bitbucket_pull = "Merged in feature/my-feature (pull request #1)";
|
||||
|
||||
assert_eq!(
|
||||
super::parse_merge_summary(gitlab_pull, &patterns),
|
||||
Some("feature/my-feature".to_string()),
|
||||
);
|
||||
assert_eq!(
|
||||
super::parse_merge_summary(git_default, &patterns),
|
||||
Some("feature/my-feature".to_string()),
|
||||
);
|
||||
assert_eq!(
|
||||
super::parse_merge_summary(git_master, &patterns),
|
||||
Some("feature/my-feature".to_string()),
|
||||
);
|
||||
assert_eq!(
|
||||
super::parse_merge_summary(github_pull, &patterns),
|
||||
Some("feature/my-feature".to_string()),
|
||||
);
|
||||
assert_eq!(
|
||||
super::parse_merge_summary(github_pull_2, &patterns),
|
||||
Some("feature/my-feature".to_string()),
|
||||
);
|
||||
assert_eq!(
|
||||
super::parse_merge_summary(bitbucket_pull, &patterns),
|
||||
Some("feature/my-feature".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
13
src/lib.rs
Normal file
13
src/lib.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
//! Command line tool to show clear git graphs arranged for your branching model.
|
||||
|
||||
use git2::Repository;
|
||||
use std::path::Path;
|
||||
|
||||
pub mod config;
|
||||
pub mod graph;
|
||||
pub mod print;
|
||||
pub mod settings;
|
||||
|
||||
pub fn get_repo<P: AsRef<Path>>(path: P) -> Result<Repository, git2::Error> {
|
||||
Repository::discover(path)
|
||||
}
|
539
src/main.rs
Normal file
539
src/main.rs
Normal file
|
@ -0,0 +1,539 @@
|
|||
use clap::{crate_version, Arg, Command};
|
||||
use crossterm::cursor::MoveToColumn;
|
||||
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||
use crossterm::style::Print;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType};
|
||||
use crossterm::{ErrorKind, ExecutableCommand};
|
||||
use git2::Repository;
|
||||
use git_graph::config::{
|
||||
create_config, get_available_models, get_model, get_model_name, set_model,
|
||||
};
|
||||
use git_graph::get_repo;
|
||||
use git_graph::graph::GitGraph;
|
||||
use git_graph::print::format::CommitFormat;
|
||||
use git_graph::print::svg::print_svg;
|
||||
use git_graph::print::unicode::print_unicode;
|
||||
use git_graph::settings::{BranchOrder, BranchSettings, Characters, MergePatterns, Settings};
|
||||
use platform_dirs::AppDirs;
|
||||
use std::io::stdout;
|
||||
use std::str::FromStr;
|
||||
use std::time::Instant;
|
||||
|
||||
const REPO_CONFIG_FILE: &str = "git-graph.toml";
|
||||
|
||||
fn main() {
|
||||
std::process::exit(match from_args() {
|
||||
Ok(_) => 0,
|
||||
Err(err) => {
|
||||
eprintln!("{}", err);
|
||||
1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn from_args() -> Result<(), String> {
|
||||
let app_dir = AppDirs::new(Some("git-graph"), false).unwrap().config_dir;
|
||||
let mut models_dir = app_dir;
|
||||
models_dir.push("models");
|
||||
|
||||
create_config(&models_dir)?;
|
||||
|
||||
let app = Command::new("git-graph")
|
||||
.version(crate_version!())
|
||||
.about(
|
||||
"Structured Git graphs for your branching model.\n \
|
||||
https://github.com/mlange-42/git-graph\n\
|
||||
\n\
|
||||
EXAMPES:\n \
|
||||
git-graph -> Show graph\n \
|
||||
git-graph --style round -> Show graph in a different style\n \
|
||||
git-graph --model <model> -> Show graph using a certain <model>\n \
|
||||
git-graph model --list -> List available branching models\n \
|
||||
git-graph model -> Show repo's current branching models\n \
|
||||
git-graph model <model> -> Permanently set model <model> for this repo",
|
||||
)
|
||||
.arg(
|
||||
Arg::new("reverse")
|
||||
.long("reverse")
|
||||
.short('r')
|
||||
.help("Reverse the order of commits.")
|
||||
.required(false)
|
||||
.num_args(0),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("path")
|
||||
.long("path")
|
||||
.short('p')
|
||||
.help("Open repository from this path or above. Default '.'")
|
||||
.required(false)
|
||||
.num_args(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("max-count")
|
||||
.long("max-count")
|
||||
.short('n')
|
||||
.help("Maximum number of commits")
|
||||
.required(false)
|
||||
.num_args(1)
|
||||
.value_name("n"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("model")
|
||||
.long("model")
|
||||
.short('m')
|
||||
.help("Branching model. Available presets are [simple|git-flow|none].\n\
|
||||
Default: git-flow. \n\
|
||||
Permanently set the model for a repository with\n\
|
||||
> git-graph model <model>")
|
||||
.required(false)
|
||||
.num_args(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("local")
|
||||
.long("local")
|
||||
.short('l')
|
||||
.help("Show only local branches, no remotes.")
|
||||
.required(false)
|
||||
.num_args(0),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("svg")
|
||||
.long("svg")
|
||||
.help("Render graph as SVG instead of text-based.")
|
||||
.required(false)
|
||||
.num_args(0),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("debug")
|
||||
.long("debug")
|
||||
.short('d')
|
||||
.help("Additional debug output and graphics.")
|
||||
.required(false)
|
||||
.num_args(0),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("sparse")
|
||||
.long("sparse")
|
||||
.short('S')
|
||||
.help("Print a less compact graph: merge lines point to target lines\n\
|
||||
rather than merge commits.")
|
||||
.required(false)
|
||||
.num_args(0),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("color")
|
||||
.long("color")
|
||||
.help("Specify when colors should be used. One of [auto|always|never].\n\
|
||||
Default: auto.")
|
||||
.required(false)
|
||||
.num_args(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-color")
|
||||
.long("no-color")
|
||||
.help("Print without colors. Missing color support should be detected\n\
|
||||
automatically (e.g. when piping to a file).\n\
|
||||
Overrides option '--color'")
|
||||
.required(false)
|
||||
.num_args(0),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-pager")
|
||||
.long("no-pager")
|
||||
.help("Use no pager (print everything at once without prompt).")
|
||||
.required(false)
|
||||
.num_args(0),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("style")
|
||||
.long("style")
|
||||
.short('s')
|
||||
.help("Output style. One of [normal/thin|round|bold|double|ascii].\n \
|
||||
(First character can be used as abbreviation, e.g. '-s r')")
|
||||
.required(false)
|
||||
.num_args(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("wrap")
|
||||
.long("wrap")
|
||||
.short('w')
|
||||
.help("Line wrapping for formatted commit text. Default: 'auto 0 8'\n\
|
||||
Argument format: [<width>|auto|none[ <indent1>[ <indent2>]]]\n\
|
||||
For examples, consult 'git-graph --help'")
|
||||
.long_help("Line wrapping for formatted commit text. Default: 'auto 0 8'\n\
|
||||
Argument format: [<width>|auto|none[ <indent1>[ <indent2>]]]\n\
|
||||
Examples:\n \
|
||||
git-graph --wrap auto\n \
|
||||
git-graph --wrap auto 0 8\n \
|
||||
git-graph --wrap none\n \
|
||||
git-graph --wrap 80\n \
|
||||
git-graph --wrap 80 0 8\n\
|
||||
'auto' uses the terminal's width if on a terminal.")
|
||||
.required(false)
|
||||
.num_args(0..=3),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("format")
|
||||
.long("format")
|
||||
.short('f')
|
||||
.help("Commit format. One of [oneline|short|medium|full|\"<string>\"].\n \
|
||||
(First character can be used as abbreviation, e.g. '-f m')\n\
|
||||
Default: oneline.\n\
|
||||
For placeholders supported in \"<string>\", consult 'git-graph --help'")
|
||||
.long_help("Commit format. One of [oneline|short|medium|full|\"<string>\"].\n \
|
||||
(First character can be used as abbreviation, e.g. '-f m')\n\
|
||||
Formatting placeholders for \"<string>\":\n \
|
||||
%n newline\n \
|
||||
%H commit hash\n \
|
||||
%h abbreviated commit hash\n \
|
||||
%P parent commit hashes\n \
|
||||
%p abbreviated parent commit hashes\n \
|
||||
%d refs (branches, tags)\n \
|
||||
%s commit summary\n \
|
||||
%b commit message body\n \
|
||||
%B raw body (subject and body)\n \
|
||||
%an author name\n \
|
||||
%ae author email\n \
|
||||
%ad author date\n \
|
||||
%as author date in short format 'YYYY-MM-DD'\n \
|
||||
%cn committer name\n \
|
||||
%ce committer email\n \
|
||||
%cd committer date\n \
|
||||
%cs committer date in short format 'YYYY-MM-DD'\n \
|
||||
\n \
|
||||
If you add a + (plus sign) after % of a placeholder,\n \
|
||||
a line-feed is inserted immediately before the expansion if\n \
|
||||
and only if the placeholder expands to a non-empty string.\n \
|
||||
If you add a - (minus sign) after % of a placeholder, all\n \
|
||||
consecutive line-feeds immediately preceding the expansion are\n \
|
||||
deleted if and only if the placeholder expands to an empty string.\n \
|
||||
If you add a ' ' (space) after % of a placeholder, a space is\n \
|
||||
inserted immediately before the expansion if and only if\n \
|
||||
the placeholder expands to a non-empty string.\n\
|
||||
\n \
|
||||
See also the respective git help: https://git-scm.com/docs/pretty-formats\n")
|
||||
.required(false)
|
||||
.num_args(1),
|
||||
)
|
||||
.subcommand(Command::new("model")
|
||||
.about("Prints or permanently sets the branching model for a repository.")
|
||||
.arg(
|
||||
Arg::new("model")
|
||||
.help("The branching model to be used. Available presets are [simple|git-flow|none].\n\
|
||||
When not given, prints the currently set model.")
|
||||
.value_name("model")
|
||||
.num_args(1)
|
||||
.required(false)
|
||||
.index(1))
|
||||
.arg(
|
||||
Arg::new("list")
|
||||
.long("list")
|
||||
.short('l')
|
||||
.help("List all available branching models.")
|
||||
.required(false)
|
||||
.num_args(0),
|
||||
));
|
||||
|
||||
let matches = app.get_matches();
|
||||
|
||||
if let Some(matches) = matches.subcommand_matches("model") {
|
||||
if matches.get_flag("list") {
|
||||
println!(
|
||||
"{}",
|
||||
itertools::join(get_available_models(&models_dir)?, "\n")
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let dot = ".".to_string();
|
||||
let path = matches.get_one::<String>("path").unwrap_or(&dot);
|
||||
let repository = get_repo(path)
|
||||
.map_err(|err| format!("ERROR: {}\n Navigate into a repository before running git-graph, or use option --path", err.message()))?;
|
||||
|
||||
if let Some(matches) = matches.subcommand_matches("model") {
|
||||
match matches.get_one::<String>("model") {
|
||||
None => {
|
||||
let curr_model = get_model_name(&repository, REPO_CONFIG_FILE)?;
|
||||
match curr_model {
|
||||
None => print!("No branching model set"),
|
||||
Some(model) => print!("{}", model),
|
||||
}
|
||||
}
|
||||
Some(model) => set_model(&repository, model, REPO_CONFIG_FILE, &models_dir)?,
|
||||
};
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let commit_limit = match matches.get_one::<String>("max-count") {
|
||||
None => None,
|
||||
Some(str) => match str.parse::<usize>() {
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => {
|
||||
return Err(format![
|
||||
"Option max-count must be a positive number, but got '{}'",
|
||||
str
|
||||
])
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let include_remote = !matches.get_flag("local");
|
||||
|
||||
let reverse_commit_order = matches.get_flag("reverse");
|
||||
|
||||
let svg = matches.get_flag("svg");
|
||||
let pager = !matches.get_flag("no-pager");
|
||||
let compact = !matches.get_flag("sparse");
|
||||
let debug = matches.get_flag("debug");
|
||||
let style = matches
|
||||
.get_one::<String>("style")
|
||||
.map(|s| Characters::from_str(s))
|
||||
.unwrap_or_else(|| Ok(Characters::thin()))?;
|
||||
|
||||
let style = if reverse_commit_order {
|
||||
style.reverse()
|
||||
} else {
|
||||
style
|
||||
};
|
||||
|
||||
let model = get_model(
|
||||
&repository,
|
||||
matches.get_one::<String>("model").map(|s| &s[..]),
|
||||
REPO_CONFIG_FILE,
|
||||
&models_dir,
|
||||
)?;
|
||||
|
||||
let format = match matches.get_one::<String>("format") {
|
||||
None => CommitFormat::OneLine,
|
||||
Some(str) => CommitFormat::from_str(str)?,
|
||||
};
|
||||
|
||||
let colored = if matches.get_flag("no-color") {
|
||||
false
|
||||
} else if let Some(mode) = matches.get_one::<String>("color") {
|
||||
match &mode[..] {
|
||||
"auto" => {
|
||||
atty::is(atty::Stream::Stdout)
|
||||
&& (!cfg!(windows) || yansi::Paint::enable_windows_ascii())
|
||||
}
|
||||
"always" => {
|
||||
if cfg!(windows) {
|
||||
yansi::Paint::enable_windows_ascii();
|
||||
}
|
||||
true
|
||||
}
|
||||
"never" => false,
|
||||
other => {
|
||||
return Err(format!(
|
||||
"Unknown color mode '{}'. Supports [auto|always|never].",
|
||||
other
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
atty::is(atty::Stream::Stdout) && (!cfg!(windows) || yansi::Paint::enable_windows_ascii())
|
||||
};
|
||||
|
||||
let wrapping = if let Some(wrap_values) = matches.get_many::<String>("wrap") {
|
||||
let strings = wrap_values.map(|s| s.as_str()).collect::<Vec<_>>();
|
||||
if strings.is_empty() {
|
||||
Some((None, Some(0), Some(8)))
|
||||
} else {
|
||||
match strings[0] {
|
||||
"none" => None,
|
||||
"auto" => {
|
||||
let wrap = strings
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|str| str.parse::<usize>())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"ERROR: Can't parse option --wrap '{}' to integers.",
|
||||
strings.join(" ")
|
||||
)
|
||||
})?;
|
||||
Some((None, wrap.first().cloned(), wrap.get(1).cloned()))
|
||||
}
|
||||
_ => {
|
||||
let wrap = strings
|
||||
.iter()
|
||||
.map(|str| str.parse::<usize>())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"ERROR: Can't parse option --wrap '{}' to integers.",
|
||||
strings.join(" ")
|
||||
)
|
||||
})?;
|
||||
Some((
|
||||
wrap.first().cloned(),
|
||||
wrap.get(1).cloned(),
|
||||
wrap.get(2).cloned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Some((None, Some(0), Some(8)))
|
||||
};
|
||||
|
||||
let settings = Settings {
|
||||
reverse_commit_order,
|
||||
debug,
|
||||
colored,
|
||||
compact,
|
||||
include_remote,
|
||||
format,
|
||||
wrapping,
|
||||
characters: style,
|
||||
branch_order: BranchOrder::ShortestFirst(true),
|
||||
branches: BranchSettings::from(model).map_err(|err| err.to_string())?,
|
||||
merge_patterns: MergePatterns::default(),
|
||||
};
|
||||
|
||||
run(repository, &settings, svg, commit_limit, pager)
|
||||
}
|
||||
|
||||
fn run(
|
||||
repository: Repository,
|
||||
settings: &Settings,
|
||||
svg: bool,
|
||||
max_commits: Option<usize>,
|
||||
pager: bool,
|
||||
) -> Result<(), String> {
|
||||
let now = Instant::now();
|
||||
let graph = GitGraph::new(repository, settings, max_commits)?;
|
||||
|
||||
let duration_graph = now.elapsed().as_micros();
|
||||
|
||||
if settings.debug {
|
||||
for branch in &graph.all_branches {
|
||||
eprintln!(
|
||||
"{} (col {}) ({:?}) {} s: {:?}, t: {:?}",
|
||||
branch.name,
|
||||
branch.visual.column.unwrap_or(99),
|
||||
branch.range,
|
||||
if branch.is_merged { "m" } else { "" },
|
||||
branch.visual.source_order_group,
|
||||
branch.visual.target_order_group
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
if svg {
|
||||
println!("{}", print_svg(&graph, settings)?);
|
||||
} else {
|
||||
let (g_lines, t_lines, _indices) = print_unicode(&graph, settings)?;
|
||||
if pager && atty::is(atty::Stream::Stdout) {
|
||||
print_paged(&g_lines, &t_lines).map_err(|err| err.to_string())?;
|
||||
} else {
|
||||
print_unpaged(&g_lines, &t_lines);
|
||||
}
|
||||
};
|
||||
|
||||
let duration_print = now.elapsed().as_micros();
|
||||
|
||||
if settings.debug {
|
||||
eprintln!(
|
||||
"Graph construction: {:.1} ms, printing: {:.1} ms ({} commits)",
|
||||
duration_graph as f32 / 1000.0,
|
||||
duration_print as f32 / 1000.0,
|
||||
graph.commits.len()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print the graph, paged (i.e. wait for user input once the terminal is filled).
|
||||
fn print_paged(graph_lines: &[String], text_lines: &[String]) -> Result<(), ErrorKind> {
|
||||
let (width, height) = crossterm::terminal::size()?;
|
||||
let width = width as usize;
|
||||
|
||||
let mut line_idx = 0;
|
||||
let mut print_lines = height - 2;
|
||||
let mut clear = false;
|
||||
let mut abort = false;
|
||||
|
||||
let help = "\r >>> Down: line, PgDown/Enter: page, End: all, Esc/Q/^C: quit\r";
|
||||
let help = if help.len() > width {
|
||||
&help[0..width]
|
||||
} else {
|
||||
help
|
||||
};
|
||||
|
||||
while line_idx < graph_lines.len() {
|
||||
if print_lines > 0 {
|
||||
if clear {
|
||||
stdout()
|
||||
.execute(Clear(ClearType::CurrentLine))?
|
||||
.execute(MoveToColumn(0))?;
|
||||
}
|
||||
|
||||
stdout().execute(Print(format!(
|
||||
" {} {}\n",
|
||||
graph_lines[line_idx], text_lines[line_idx]
|
||||
)))?;
|
||||
|
||||
if print_lines == 1 && line_idx < graph_lines.len() - 1 {
|
||||
stdout().execute(Print(help))?;
|
||||
}
|
||||
print_lines -= 1;
|
||||
line_idx += 1;
|
||||
} else {
|
||||
enable_raw_mode()?;
|
||||
let input = crossterm::event::read()?;
|
||||
if let Event::Key(evt) = input {
|
||||
match evt.code {
|
||||
KeyCode::Down => {
|
||||
clear = true;
|
||||
print_lines = 1;
|
||||
}
|
||||
KeyCode::Enter | KeyCode::PageDown => {
|
||||
clear = true;
|
||||
print_lines = height - 2;
|
||||
}
|
||||
KeyCode::End => {
|
||||
clear = true;
|
||||
print_lines = graph_lines.len() as u16;
|
||||
}
|
||||
KeyCode::Char(c) => match c {
|
||||
'q' => {
|
||||
abort = true;
|
||||
break;
|
||||
}
|
||||
'c' if evt.modifiers == KeyModifiers::CONTROL => {
|
||||
abort = true;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Esc => {
|
||||
abort = true;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if abort {
|
||||
stdout()
|
||||
.execute(Clear(ClearType::CurrentLine))?
|
||||
.execute(MoveToColumn(0))?
|
||||
.execute(Print(" ...\n"))?;
|
||||
}
|
||||
disable_raw_mode()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print the graph, un-paged.
|
||||
fn print_unpaged(graph_lines: &[String], text_lines: &[String]) {
|
||||
for (g_line, t_line) in graph_lines.iter().zip(text_lines.iter()) {
|
||||
println!(" {} {}", g_line, t_line);
|
||||
}
|
||||
}
|
45
src/print/colors.rs
Normal file
45
src/print/colors.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
//! ANSI terminal color handling.
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Converts a color name to the index in the 256-color palette.
|
||||
pub fn to_terminal_color(color: &str) -> Result<u8, String> {
|
||||
match NAMED_COLORS.get(color) {
|
||||
None => match color.parse::<u8>() {
|
||||
Ok(col) => Ok(col),
|
||||
Err(_) => Err(format!("Color {} not found", color)),
|
||||
},
|
||||
Some(rgb) => Ok(*rgb),
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! hashmap {
|
||||
($( $key: expr => $val: expr ),*) => {{
|
||||
let mut map = ::std::collections::HashMap::new();
|
||||
$( map.insert($key, $val); )*
|
||||
map
|
||||
}}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// Named ANSI colors
|
||||
pub static ref NAMED_COLORS: HashMap<&'static str, u8> = hashmap![
|
||||
"black" => 0,
|
||||
"red" => 1,
|
||||
"green" => 2,
|
||||
"yellow" => 3,
|
||||
"blue" => 4,
|
||||
"magenta" => 5,
|
||||
"cyan" => 6,
|
||||
"white" => 7,
|
||||
"bright_black" => 8,
|
||||
"bright_red" => 9,
|
||||
"bright_green" => 10,
|
||||
"bright_yellow" => 11,
|
||||
"bright_blue" => 12,
|
||||
"bright_magenta" => 13,
|
||||
"bright_cyan" => 14,
|
||||
"bright_white" => 15
|
||||
];
|
||||
}
|
548
src/print/format.rs
Normal file
548
src/print/format.rs
Normal file
|
@ -0,0 +1,548 @@
|
|||
//! Formatting of commits.
|
||||
|
||||
use chrono::{FixedOffset, Local, TimeZone};
|
||||
use git2::{Commit, Time};
|
||||
use lazy_static::lazy_static;
|
||||
use std::fmt::Write;
|
||||
use std::str::FromStr;
|
||||
use textwrap::Options;
|
||||
use yansi::Paint;
|
||||
|
||||
/// Commit formatting options.
|
||||
#[derive(Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum CommitFormat {
|
||||
OneLine,
|
||||
Short,
|
||||
Medium,
|
||||
Full,
|
||||
Format(String),
|
||||
}
|
||||
|
||||
impl FromStr for CommitFormat {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(str: &str) -> Result<Self, Self::Err> {
|
||||
match str {
|
||||
"oneline" | "o" => Ok(CommitFormat::OneLine),
|
||||
"short" | "s" => Ok(CommitFormat::Short),
|
||||
"medium" | "m" => Ok(CommitFormat::Medium),
|
||||
"full" | "f" => Ok(CommitFormat::Full),
|
||||
str => Ok(CommitFormat::Format(str.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NEW_LINE: usize = 0;
|
||||
const HASH: usize = 1;
|
||||
const HASH_ABBREV: usize = 2;
|
||||
const PARENT_HASHES: usize = 3;
|
||||
const PARENT_HASHES_ABBREV: usize = 4;
|
||||
const REFS: usize = 5;
|
||||
const SUBJECT: usize = 6;
|
||||
const AUTHOR: usize = 7;
|
||||
const AUTHOR_EMAIL: usize = 8;
|
||||
const AUTHOR_DATE: usize = 9;
|
||||
const AUTHOR_DATE_SHORT: usize = 10;
|
||||
const COMMITTER: usize = 11;
|
||||
const COMMITTER_EMAIL: usize = 12;
|
||||
const COMMITTER_DATE: usize = 13;
|
||||
const COMMITTER_DATE_SHORT: usize = 14;
|
||||
const BODY: usize = 15;
|
||||
const BODY_RAW: usize = 16;
|
||||
|
||||
const MODE_SPACE: usize = 1;
|
||||
const MODE_PLUS: usize = 2;
|
||||
const MODE_MINUS: usize = 3;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref PLACEHOLDERS: Vec<[String; 4]> = {
|
||||
let base = vec![
|
||||
"n", "H", "h", "P", "p", "d", "s", "an", "ae", "ad", "as", "cn", "ce", "cd", "cs", "b",
|
||||
"B",
|
||||
];
|
||||
base.iter()
|
||||
.map(|b| {
|
||||
[
|
||||
format!("%{}", b),
|
||||
format!("% {}", b),
|
||||
format!("%+{}", b),
|
||||
format!("%-{}", b),
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
}
|
||||
|
||||
/// Format a commit for `CommitFormat::Format(String)`.
|
||||
pub fn format_commit(
|
||||
format: &str,
|
||||
commit: &Commit,
|
||||
branches: String,
|
||||
wrapping: &Option<Options>,
|
||||
hash_color: Option<u8>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let mut replacements = vec![];
|
||||
|
||||
for (idx, arr) in PLACEHOLDERS.iter().enumerate() {
|
||||
let mut curr = 0;
|
||||
loop {
|
||||
let mut found = false;
|
||||
for (mode, str) in arr.iter().enumerate() {
|
||||
if let Some(start) = &format[curr..format.len()].find(str) {
|
||||
replacements.push((curr + start, str.len(), idx, mode));
|
||||
curr += start + str.len();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replacements.sort_by_key(|p| p.0);
|
||||
|
||||
let mut lines = vec![];
|
||||
let mut out = String::new();
|
||||
if replacements.is_empty() {
|
||||
write!(out, "{}", format).unwrap();
|
||||
add_line(&mut lines, &mut out, wrapping);
|
||||
} else {
|
||||
let mut curr = 0;
|
||||
for (start, len, idx, mode) in replacements {
|
||||
if idx == NEW_LINE {
|
||||
write!(out, "{}", &format[curr..start]).unwrap();
|
||||
add_line(&mut lines, &mut out, wrapping);
|
||||
} else {
|
||||
write!(out, "{}", &format[curr..start]).unwrap();
|
||||
match idx {
|
||||
HASH => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(color) = hash_color {
|
||||
write!(out, "{}", Paint::fixed(color, commit.id()))
|
||||
} else {
|
||||
write!(out, "{}", commit.id())
|
||||
}
|
||||
}
|
||||
HASH_ABBREV => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(color) = hash_color {
|
||||
write!(
|
||||
out,
|
||||
"{}",
|
||||
Paint::fixed(color, &commit.id().to_string()[..7])
|
||||
)
|
||||
} else {
|
||||
write!(out, "{}", &commit.id().to_string()[..7])
|
||||
}
|
||||
}
|
||||
PARENT_HASHES => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
for i in 0..commit.parent_count() {
|
||||
write!(out, "{}", commit.parent_id(i).unwrap()).unwrap();
|
||||
if i < commit.parent_count() - 1 {
|
||||
write!(out, " ").unwrap();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
PARENT_HASHES_ABBREV => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
for i in 0..commit.parent_count() {
|
||||
write!(
|
||||
out,
|
||||
"{}",
|
||||
&commit
|
||||
.parent_id(i)
|
||||
.map_err(|err| err.to_string())?
|
||||
.to_string()[..7]
|
||||
)
|
||||
.unwrap();
|
||||
if i < commit.parent_count() - 1 {
|
||||
write!(out, " ").unwrap();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
REFS => {
|
||||
match mode {
|
||||
MODE_SPACE => {
|
||||
if !branches.is_empty() {
|
||||
write!(out, " ").unwrap()
|
||||
}
|
||||
}
|
||||
MODE_PLUS => {
|
||||
if !branches.is_empty() {
|
||||
add_line(&mut lines, &mut out, wrapping)
|
||||
}
|
||||
}
|
||||
MODE_MINUS => {
|
||||
if branches.is_empty() {
|
||||
out = remove_empty_lines(&mut lines, out)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
write!(out, "{}", branches)
|
||||
}
|
||||
SUBJECT => {
|
||||
let summary = commit.summary().unwrap_or("");
|
||||
match mode {
|
||||
MODE_SPACE => {
|
||||
if !summary.is_empty() {
|
||||
write!(out, " ").unwrap()
|
||||
}
|
||||
}
|
||||
MODE_PLUS => {
|
||||
if !summary.is_empty() {
|
||||
add_line(&mut lines, &mut out, wrapping)
|
||||
}
|
||||
}
|
||||
MODE_MINUS => {
|
||||
if summary.is_empty() {
|
||||
out = remove_empty_lines(&mut lines, out)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
write!(out, "{}", summary)
|
||||
}
|
||||
AUTHOR => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
write!(out, "{}", &commit.author().name().unwrap_or(""))
|
||||
}
|
||||
AUTHOR_EMAIL => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
write!(out, "{}", &commit.author().email().unwrap_or(""))
|
||||
}
|
||||
AUTHOR_DATE => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
write!(
|
||||
out,
|
||||
"{}",
|
||||
format_date(commit.author().when(), "%a %b %e %H:%M:%S %Y %z")
|
||||
)
|
||||
}
|
||||
AUTHOR_DATE_SHORT => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
write!(out, "{}", format_date(commit.author().when(), "%F"))
|
||||
}
|
||||
COMMITTER => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
write!(out, "{}", &commit.committer().name().unwrap_or(""))
|
||||
}
|
||||
COMMITTER_EMAIL => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
write!(out, "{}", &commit.committer().email().unwrap_or(""))
|
||||
}
|
||||
COMMITTER_DATE => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
write!(
|
||||
out,
|
||||
"{}",
|
||||
format_date(commit.committer().when(), "%a %b %e %H:%M:%S %Y %z")
|
||||
)
|
||||
}
|
||||
COMMITTER_DATE_SHORT => {
|
||||
match mode {
|
||||
MODE_SPACE => write!(out, " ").unwrap(),
|
||||
MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
|
||||
_ => {}
|
||||
}
|
||||
write!(out, "{}", format_date(commit.committer().when(), "%F"))
|
||||
}
|
||||
BODY => {
|
||||
let message = commit
|
||||
.message()
|
||||
.unwrap_or("")
|
||||
.lines()
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
let num_parts = message.len();
|
||||
match mode {
|
||||
MODE_SPACE => {
|
||||
if num_parts > 2 {
|
||||
write!(out, " ").unwrap()
|
||||
}
|
||||
}
|
||||
MODE_PLUS => {
|
||||
if num_parts > 2 {
|
||||
add_line(&mut lines, &mut out, wrapping)
|
||||
}
|
||||
}
|
||||
MODE_MINUS => {
|
||||
if num_parts <= 2 {
|
||||
out = remove_empty_lines(&mut lines, out)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for (cnt, line) in message.iter().enumerate() {
|
||||
if cnt > 1 && (cnt < num_parts - 1 || !line.is_empty()) {
|
||||
write!(out, "{}", line).unwrap();
|
||||
add_line(&mut lines, &mut out, wrapping);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
BODY_RAW => {
|
||||
let message = commit
|
||||
.message()
|
||||
.unwrap_or("")
|
||||
.lines()
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
let num_parts = message.len();
|
||||
|
||||
match mode {
|
||||
MODE_SPACE => {
|
||||
if !message.is_empty() {
|
||||
write!(out, " ").unwrap()
|
||||
}
|
||||
}
|
||||
MODE_PLUS => {
|
||||
if !message.is_empty() {
|
||||
add_line(&mut lines, &mut out, wrapping)
|
||||
}
|
||||
}
|
||||
MODE_MINUS => {
|
||||
if message.is_empty() {
|
||||
out = remove_empty_lines(&mut lines, out)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for (cnt, line) in message.iter().enumerate() {
|
||||
if cnt < num_parts - 1 || !line.is_empty() {
|
||||
write!(out, "{}", line).unwrap();
|
||||
add_line(&mut lines, &mut out, wrapping);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
x => return Err(format!("No commit field at index {}", x)),
|
||||
}
|
||||
.unwrap();
|
||||
}
|
||||
curr = start + len;
|
||||
}
|
||||
write!(out, "{}", &format[curr..(format.len())]).unwrap();
|
||||
if !out.is_empty() {
|
||||
add_line(&mut lines, &mut out, wrapping);
|
||||
}
|
||||
}
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
/// Format a commit for `CommitFormat::OneLine`.
|
||||
pub fn format_oneline(
|
||||
commit: &Commit,
|
||||
branches: String,
|
||||
wrapping: &Option<Options>,
|
||||
hash_color: Option<u8>,
|
||||
) -> Vec<String> {
|
||||
let mut out = String::new();
|
||||
if let Some(color) = hash_color {
|
||||
write!(
|
||||
out,
|
||||
"{}",
|
||||
Paint::fixed(color, &commit.id().to_string()[..7])
|
||||
)
|
||||
} else {
|
||||
write!(out, "{}", &commit.id().to_string()[..7])
|
||||
}
|
||||
.unwrap();
|
||||
|
||||
write!(out, "{} {}", branches, commit.summary().unwrap_or("")).unwrap();
|
||||
|
||||
if let Some(wrap) = wrapping {
|
||||
textwrap::fill(&out, wrap)
|
||||
.lines()
|
||||
.map(|str| str.to_string())
|
||||
.collect()
|
||||
} else {
|
||||
vec![out]
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a commit for `CommitFormat::Short`, `CommitFormat::Medium` or `CommitFormat::Full`.
|
||||
pub fn format(
|
||||
commit: &Commit,
|
||||
branches: String,
|
||||
wrapping: &Option<Options>,
|
||||
hash_color: Option<u8>,
|
||||
format: &CommitFormat,
|
||||
) -> Result<Vec<String>, String> {
|
||||
match format {
|
||||
CommitFormat::OneLine => return Ok(format_oneline(commit, branches, wrapping, hash_color)),
|
||||
CommitFormat::Format(format) => {
|
||||
return format_commit(format, commit, branches, wrapping, hash_color)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut out_vec = vec![];
|
||||
let mut out = String::new();
|
||||
|
||||
if let Some(color) = hash_color {
|
||||
write!(out, "commit {}", Paint::fixed(color, &commit.id()))
|
||||
} else {
|
||||
write!(out, "commit {}", &commit.id())
|
||||
}
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
write!(out, "{}", branches).map_err(|err| err.to_string())?;
|
||||
append_wrapped(&mut out_vec, out, wrapping);
|
||||
|
||||
if commit.parent_count() > 1 {
|
||||
out = String::new();
|
||||
write!(
|
||||
out,
|
||||
"Merge: {} {}",
|
||||
&commit.parent_id(0).unwrap().to_string()[..7],
|
||||
&commit.parent_id(1).unwrap().to_string()[..7]
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
append_wrapped(&mut out_vec, out, wrapping);
|
||||
}
|
||||
|
||||
out = String::new();
|
||||
write!(
|
||||
out,
|
||||
"Author: {} <{}>",
|
||||
commit.author().name().unwrap_or(""),
|
||||
commit.author().email().unwrap_or("")
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
append_wrapped(&mut out_vec, out, wrapping);
|
||||
|
||||
if format > &CommitFormat::Medium {
|
||||
out = String::new();
|
||||
write!(
|
||||
out,
|
||||
"Commit: {} <{}>",
|
||||
commit.committer().name().unwrap_or(""),
|
||||
commit.committer().email().unwrap_or("")
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
append_wrapped(&mut out_vec, out, wrapping);
|
||||
}
|
||||
|
||||
if format > &CommitFormat::Short {
|
||||
out = String::new();
|
||||
write!(
|
||||
out,
|
||||
"Date: {}",
|
||||
format_date(commit.author().when(), "%a %b %e %H:%M:%S %Y %z")
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
append_wrapped(&mut out_vec, out, wrapping);
|
||||
}
|
||||
|
||||
if format == &CommitFormat::Short {
|
||||
out_vec.push("".to_string());
|
||||
append_wrapped(
|
||||
&mut out_vec,
|
||||
format!(" {}", commit.summary().unwrap_or("")),
|
||||
wrapping,
|
||||
);
|
||||
out_vec.push("".to_string());
|
||||
} else {
|
||||
out_vec.push("".to_string());
|
||||
let mut add_line = true;
|
||||
for line in commit.message().unwrap_or("").lines() {
|
||||
if line.is_empty() {
|
||||
out_vec.push(line.to_string());
|
||||
} else {
|
||||
append_wrapped(&mut out_vec, format!(" {}", line), wrapping);
|
||||
}
|
||||
add_line = !line.trim().is_empty();
|
||||
}
|
||||
if add_line {
|
||||
out_vec.push("".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out_vec)
|
||||
}
|
||||
|
||||
pub fn format_date(time: Time, format: &str) -> String {
|
||||
let date =
|
||||
Local::from_offset(&FixedOffset::east(time.offset_minutes())).timestamp(time.seconds(), 0);
|
||||
format!("{}", date.format(format))
|
||||
}
|
||||
|
||||
fn append_wrapped(vec: &mut Vec<String>, str: String, wrapping: &Option<Options>) {
|
||||
if str.is_empty() {
|
||||
vec.push(str);
|
||||
} else if let Some(wrap) = wrapping {
|
||||
vec.extend(
|
||||
textwrap::fill(&str, wrap)
|
||||
.lines()
|
||||
.map(|str| str.to_string()),
|
||||
)
|
||||
} else {
|
||||
vec.push(str);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_line(lines: &mut Vec<String>, line: &mut String, wrapping: &Option<Options>) {
|
||||
let mut temp = String::new();
|
||||
std::mem::swap(&mut temp, line);
|
||||
append_wrapped(lines, temp, wrapping);
|
||||
}
|
||||
|
||||
fn remove_empty_lines(lines: &mut Vec<String>, mut line: String) -> String {
|
||||
while !lines.is_empty() && lines.last().unwrap().is_empty() {
|
||||
line = lines.remove(lines.len() - 1);
|
||||
}
|
||||
if !lines.is_empty() {
|
||||
line = lines.remove(lines.len() - 1);
|
||||
}
|
||||
line
|
||||
}
|
45
src/print/mod.rs
Normal file
45
src/print/mod.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
//! Create visual representations of git graphs.
|
||||
|
||||
use crate::graph::GitGraph;
|
||||
use std::cmp::max;
|
||||
|
||||
pub mod colors;
|
||||
pub mod format;
|
||||
pub mod svg;
|
||||
pub mod unicode;
|
||||
|
||||
/// Find the index at which a between-branch connection
|
||||
/// has to deviate from the current branch's column.
|
||||
///
|
||||
/// Returns the last index on the current column.
|
||||
fn get_deviate_index(graph: &GitGraph, index: usize, par_index: usize) -> usize {
|
||||
let info = &graph.commits[index];
|
||||
|
||||
let par_info = &graph.commits[par_index];
|
||||
let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
|
||||
|
||||
let mut min_split_idx = index;
|
||||
for sibling_oid in &par_info.children {
|
||||
if let Some(&sibling_index) = graph.indices.get(sibling_oid) {
|
||||
if let Some(sibling) = graph.commits.get(sibling_index) {
|
||||
if let Some(sibling_trace) = sibling.branch_trace {
|
||||
let sibling_branch = &graph.all_branches[sibling_trace];
|
||||
if sibling_oid != &info.oid
|
||||
&& sibling_branch.visual.column == par_branch.visual.column
|
||||
&& sibling_index > min_split_idx
|
||||
{
|
||||
min_split_idx = sibling_index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: in cases where no crossings occur, the rule for merge commits can also be applied to normal commits
|
||||
// See also branch::trace_branch()
|
||||
if info.is_merge {
|
||||
max(index, min_split_idx)
|
||||
} else {
|
||||
(par_index as i32 - 1) as usize
|
||||
}
|
||||
}
|
161
src/print/svg.rs
Normal file
161
src/print/svg.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
//! Create graphs in SVG format (Scalable Vector Graphics).
|
||||
|
||||
use crate::graph::GitGraph;
|
||||
use crate::settings::Settings;
|
||||
use svg::node::element::path::Data;
|
||||
use svg::node::element::{Circle, Line, Path};
|
||||
use svg::Document;
|
||||
|
||||
/// Creates a SVG visual representation of a graph.
|
||||
pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result<String, String> {
|
||||
let mut document = Document::new();
|
||||
|
||||
let max_idx = graph.commits.len();
|
||||
let mut max_column = 0;
|
||||
|
||||
if settings.debug {
|
||||
for branch in &graph.all_branches {
|
||||
if let (Some(start), Some(end)) = branch.range {
|
||||
document = document.add(bold_line(
|
||||
start,
|
||||
branch.visual.column.unwrap(),
|
||||
end,
|
||||
branch.visual.column.unwrap(),
|
||||
"cyan",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (idx, info) in graph.commits.iter().enumerate() {
|
||||
if let Some(trace) = info.branch_trace {
|
||||
let branch = &graph.all_branches[trace];
|
||||
let branch_color = &branch.visual.svg_color;
|
||||
|
||||
if branch.visual.column.unwrap() > max_column {
|
||||
max_column = branch.visual.column.unwrap();
|
||||
}
|
||||
|
||||
for p in 0..2 {
|
||||
if let Some(par_oid) = info.parents[p] {
|
||||
if let Some(par_idx) = graph.indices.get(&par_oid) {
|
||||
let par_info = &graph.commits[*par_idx];
|
||||
let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
|
||||
|
||||
let color = if info.is_merge {
|
||||
&par_branch.visual.svg_color
|
||||
} else {
|
||||
branch_color
|
||||
};
|
||||
|
||||
if branch.visual.column == par_branch.visual.column {
|
||||
document = document.add(line(
|
||||
idx,
|
||||
branch.visual.column.unwrap(),
|
||||
*par_idx,
|
||||
par_branch.visual.column.unwrap(),
|
||||
color,
|
||||
));
|
||||
} else {
|
||||
let split_index = super::get_deviate_index(graph, idx, *par_idx);
|
||||
document = document.add(path(
|
||||
idx,
|
||||
branch.visual.column.unwrap(),
|
||||
*par_idx,
|
||||
par_branch.visual.column.unwrap(),
|
||||
split_index,
|
||||
color,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document = document.add(commit_dot(
|
||||
idx,
|
||||
branch.visual.column.unwrap(),
|
||||
branch_color,
|
||||
!info.is_merge,
|
||||
));
|
||||
}
|
||||
}
|
||||
let (x_max, y_max) = commit_coord(max_idx + 1, max_column + 1);
|
||||
document = document
|
||||
.set("viewBox", (0, 0, x_max, y_max))
|
||||
.set("width", x_max)
|
||||
.set("height", y_max);
|
||||
|
||||
let mut out: Vec<u8> = vec![];
|
||||
svg::write(&mut out, &document).map_err(|err| err.to_string())?;
|
||||
Ok(String::from_utf8(out).unwrap_or_else(|_| "Invalid UTF8 character.".to_string()))
|
||||
}
|
||||
|
||||
fn commit_dot(index: usize, column: usize, color: &str, filled: bool) -> Circle {
|
||||
let (x, y) = commit_coord(index, column);
|
||||
Circle::new()
|
||||
.set("cx", x)
|
||||
.set("cy", y)
|
||||
.set("r", 4)
|
||||
.set("fill", if filled { color } else { "white" })
|
||||
.set("stroke", color)
|
||||
.set("stroke-width", 1)
|
||||
}
|
||||
|
||||
fn line(index1: usize, column1: usize, index2: usize, column2: usize, color: &str) -> Line {
|
||||
let (x1, y1) = commit_coord(index1, column1);
|
||||
let (x2, y2) = commit_coord(index2, column2);
|
||||
Line::new()
|
||||
.set("x1", x1)
|
||||
.set("y1", y1)
|
||||
.set("x2", x2)
|
||||
.set("y2", y2)
|
||||
.set("stroke", color)
|
||||
.set("stroke-width", 1)
|
||||
}
|
||||
|
||||
fn bold_line(index1: usize, column1: usize, index2: usize, column2: usize, color: &str) -> Line {
|
||||
let (x1, y1) = commit_coord(index1, column1);
|
||||
let (x2, y2) = commit_coord(index2, column2);
|
||||
Line::new()
|
||||
.set("x1", x1)
|
||||
.set("y1", y1)
|
||||
.set("x2", x2)
|
||||
.set("y2", y2)
|
||||
.set("stroke", color)
|
||||
.set("stroke-width", 5)
|
||||
}
|
||||
|
||||
fn path(
|
||||
index1: usize,
|
||||
column1: usize,
|
||||
index2: usize,
|
||||
column2: usize,
|
||||
split_idx: usize,
|
||||
color: &str,
|
||||
) -> Path {
|
||||
let c0 = commit_coord(index1, column1);
|
||||
|
||||
let c1 = commit_coord(split_idx, column1);
|
||||
let c2 = commit_coord(split_idx + 1, column2);
|
||||
|
||||
let c3 = commit_coord(index2, column2);
|
||||
|
||||
let m = (0.5 * (c1.0 + c2.0), 0.5 * (c1.1 + c2.1));
|
||||
|
||||
let data = Data::new()
|
||||
.move_to(c0)
|
||||
.line_to(c1)
|
||||
.quadratic_curve_to((c1.0, m.1, m.0, m.1))
|
||||
.quadratic_curve_to((c2.0, m.1, c2.0, c2.1))
|
||||
.line_to(c3);
|
||||
|
||||
Path::new()
|
||||
.set("d", data)
|
||||
.set("fill", "none")
|
||||
.set("stroke", color)
|
||||
.set("stroke-width", 1)
|
||||
}
|
||||
|
||||
fn commit_coord(index: usize, column: usize) -> (f32, f32) {
|
||||
(15.0 * (column as f32 + 1.0), 15.0 * (index as f32 + 1.0))
|
||||
}
|
725
src/print/unicode.rs
Normal file
725
src/print/unicode.rs
Normal file
|
@ -0,0 +1,725 @@
|
|||
//! Create graphs in SVG format (Scalable Vector Graphics).
|
||||
|
||||
use crate::graph::{CommitInfo, GitGraph, HeadInfo};
|
||||
use crate::print::format::CommitFormat;
|
||||
use crate::settings::{Characters, Settings};
|
||||
use itertools::Itertools;
|
||||
use std::cmp::max;
|
||||
use std::collections::hash_map::Entry::{Occupied, Vacant};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use textwrap::Options;
|
||||
use yansi::Paint;
|
||||
|
||||
const SPACE: u8 = 0;
|
||||
const DOT: u8 = 1;
|
||||
const CIRCLE: u8 = 2;
|
||||
const VER: u8 = 3;
|
||||
const HOR: u8 = 4;
|
||||
const CROSS: u8 = 5;
|
||||
const R_U: u8 = 6;
|
||||
const R_D: u8 = 7;
|
||||
const L_D: u8 = 8;
|
||||
const L_U: u8 = 9;
|
||||
const VER_L: u8 = 10;
|
||||
const VER_R: u8 = 11;
|
||||
const HOR_U: u8 = 12;
|
||||
const HOR_D: u8 = 13;
|
||||
|
||||
const ARR_L: u8 = 14;
|
||||
const ARR_R: u8 = 15;
|
||||
|
||||
const WHITE: u8 = 7;
|
||||
const HEAD_COLOR: u8 = 14;
|
||||
const HASH_COLOR: u8 = 11;
|
||||
|
||||
type UnicodeGraphInfo = (Vec<String>, Vec<String>, Vec<usize>);
|
||||
|
||||
/// Creates a text-based visual representation of a graph.
|
||||
pub fn print_unicode(graph: &GitGraph, settings: &Settings) -> Result<UnicodeGraphInfo, String> {
|
||||
let num_cols = 2 * graph
|
||||
.all_branches
|
||||
.iter()
|
||||
.map(|b| b.visual.column.unwrap_or(0))
|
||||
.max()
|
||||
.unwrap()
|
||||
+ 1;
|
||||
|
||||
let head_idx = graph.indices.get(&graph.head.oid);
|
||||
|
||||
let inserts = get_inserts(graph, settings.compact);
|
||||
|
||||
let (indent1, indent2) = if let Some((_, ind1, ind2)) = settings.wrapping {
|
||||
(" ".repeat(ind1.unwrap_or(0)), " ".repeat(ind2.unwrap_or(0)))
|
||||
} else {
|
||||
("".to_string(), "".to_string())
|
||||
};
|
||||
|
||||
let wrap_options = if let Some((width, _, _)) = settings.wrapping {
|
||||
create_wrapping_options(width, &indent1, &indent2, num_cols + 4)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut index_map = vec![];
|
||||
let mut text_lines = vec![];
|
||||
let mut offset = 0;
|
||||
for (idx, info) in graph.commits.iter().enumerate() {
|
||||
index_map.push(idx + offset);
|
||||
let cnt_inserts = if let Some(inserts) = inserts.get(&idx) {
|
||||
inserts
|
||||
.iter()
|
||||
.filter(|vec| {
|
||||
vec.iter().all(|occ| match occ {
|
||||
Occ::Commit(_, _) => false,
|
||||
Occ::Range(_, _, _, _) => true,
|
||||
})
|
||||
})
|
||||
.count()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let head = if head_idx.map_or(false, |h| h == &idx) {
|
||||
Some(&graph.head)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let lines = format(
|
||||
&settings.format,
|
||||
graph,
|
||||
info,
|
||||
head,
|
||||
settings.colored,
|
||||
&wrap_options,
|
||||
)?;
|
||||
|
||||
let num_lines = if lines.is_empty() { 0 } else { lines.len() - 1 };
|
||||
let max_inserts = max(cnt_inserts, num_lines);
|
||||
let add_lines = max_inserts - num_lines;
|
||||
|
||||
text_lines.extend(lines.into_iter().map(Some));
|
||||
text_lines.extend((0..add_lines).map(|_| None));
|
||||
|
||||
offset += max_inserts;
|
||||
}
|
||||
|
||||
let mut grid = Grid::new(
|
||||
num_cols,
|
||||
graph.commits.len() + offset,
|
||||
[SPACE, WHITE, settings.branches.persistence.len() as u8 + 2],
|
||||
);
|
||||
|
||||
for (idx, info) in graph.commits.iter().enumerate() {
|
||||
if let Some(trace) = info.branch_trace {
|
||||
let branch = &graph.all_branches[trace];
|
||||
let column = branch.visual.column.unwrap();
|
||||
let idx_map = index_map[idx];
|
||||
|
||||
let branch_color = branch.visual.term_color;
|
||||
|
||||
grid.set(
|
||||
column * 2,
|
||||
idx_map,
|
||||
if info.is_merge { CIRCLE } else { DOT },
|
||||
branch_color,
|
||||
branch.persistence,
|
||||
);
|
||||
|
||||
for p in 0..2 {
|
||||
if let Some(par_oid) = info.parents[p] {
|
||||
if let Some(par_idx) = graph.indices.get(&par_oid) {
|
||||
let par_idx_map = index_map[*par_idx];
|
||||
let par_info = &graph.commits[*par_idx];
|
||||
let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
|
||||
let par_column = par_branch.visual.column.unwrap();
|
||||
|
||||
let (color, pers) = if info.is_merge {
|
||||
(par_branch.visual.term_color, par_branch.persistence)
|
||||
} else {
|
||||
(branch_color, branch.persistence)
|
||||
};
|
||||
|
||||
if branch.visual.column == par_branch.visual.column {
|
||||
if par_idx_map > idx_map + 1 {
|
||||
vline(&mut grid, (idx_map, par_idx_map), column, color, pers);
|
||||
}
|
||||
} else {
|
||||
let split_index = super::get_deviate_index(graph, idx, *par_idx);
|
||||
let split_idx_map = index_map[split_index];
|
||||
let inserts = &inserts[&split_index];
|
||||
for (insert_idx, sub_entry) in inserts.iter().enumerate() {
|
||||
for occ in sub_entry {
|
||||
match occ {
|
||||
Occ::Commit(_, _) => {}
|
||||
Occ::Range(i1, i2, _, _) => {
|
||||
if *i1 == idx && i2 == par_idx {
|
||||
vline(
|
||||
&mut grid,
|
||||
(idx_map, split_idx_map + insert_idx),
|
||||
column,
|
||||
color,
|
||||
pers,
|
||||
);
|
||||
hline(
|
||||
&mut grid,
|
||||
split_idx_map + insert_idx,
|
||||
(par_column, column),
|
||||
info.is_merge && p > 0,
|
||||
color,
|
||||
pers,
|
||||
);
|
||||
vline(
|
||||
&mut grid,
|
||||
(split_idx_map + insert_idx, par_idx_map),
|
||||
par_column,
|
||||
color,
|
||||
pers,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if settings.reverse_commit_order {
|
||||
text_lines.reverse();
|
||||
grid.reverse();
|
||||
}
|
||||
|
||||
let lines = print_graph(&settings.characters, &grid, text_lines, settings.colored);
|
||||
|
||||
Ok((lines.0, lines.1, index_map))
|
||||
}
|
||||
|
||||
/// Create `textwrap::Options` from width and indent.
|
||||
fn create_wrapping_options<'a>(
|
||||
width: Option<usize>,
|
||||
indent1: &'a str,
|
||||
indent2: &'a str,
|
||||
graph_width: usize,
|
||||
) -> Result<Option<Options<'a>>, String> {
|
||||
let wrapping = if let Some(width) = width {
|
||||
Some(
|
||||
textwrap::Options::new(width)
|
||||
.initial_indent(indent1)
|
||||
.subsequent_indent(indent2),
|
||||
)
|
||||
} else if atty::is(atty::Stream::Stdout) {
|
||||
let width = crossterm::terminal::size()
|
||||
.map_err(|err| err.to_string())?
|
||||
.0;
|
||||
let width = if width as usize > graph_width {
|
||||
width as usize - graph_width
|
||||
} else {
|
||||
1
|
||||
};
|
||||
Some(
|
||||
textwrap::Options::new(width)
|
||||
.initial_indent(indent1)
|
||||
.subsequent_indent(indent2),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(wrapping)
|
||||
}
|
||||
|
||||
/// Draws a vertical line
|
||||
fn vline(grid: &mut Grid, (from, to): (usize, usize), column: usize, color: u8, pers: u8) {
|
||||
for i in (from + 1)..to {
|
||||
let (curr, _, old_pers) = grid.get_tuple(column * 2, i);
|
||||
let (new_col, new_pers) = if pers < old_pers {
|
||||
(Some(color), Some(pers))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
match curr {
|
||||
DOT | CIRCLE => {}
|
||||
HOR => {
|
||||
grid.set_opt(column * 2, i, Some(CROSS), Some(color), Some(pers));
|
||||
}
|
||||
HOR_U | HOR_D => {
|
||||
grid.set_opt(column * 2, i, Some(CROSS), Some(color), Some(pers));
|
||||
}
|
||||
CROSS | VER | VER_L | VER_R => grid.set_opt(column * 2, i, None, new_col, new_pers),
|
||||
L_D | L_U => {
|
||||
grid.set_opt(column * 2, i, Some(VER_L), new_col, new_pers);
|
||||
}
|
||||
R_D | R_U => {
|
||||
grid.set_opt(column * 2, i, Some(VER_R), new_col, new_pers);
|
||||
}
|
||||
_ => {
|
||||
grid.set_opt(column * 2, i, Some(VER), new_col, new_pers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a horizontal line
|
||||
fn hline(
|
||||
grid: &mut Grid,
|
||||
index: usize,
|
||||
(from, to): (usize, usize),
|
||||
merge: bool,
|
||||
color: u8,
|
||||
pers: u8,
|
||||
) {
|
||||
if from == to {
|
||||
return;
|
||||
}
|
||||
let from_2 = from * 2;
|
||||
let to_2 = to * 2;
|
||||
if from < to {
|
||||
for column in (from_2 + 1)..to_2 {
|
||||
if merge && column == to_2 - 1 {
|
||||
grid.set(column, index, ARR_R, color, pers);
|
||||
} else {
|
||||
let (curr, _, old_pers) = grid.get_tuple(column, index);
|
||||
let (new_col, new_pers) = if pers < old_pers {
|
||||
(Some(color), Some(pers))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
match curr {
|
||||
DOT | CIRCLE => {}
|
||||
VER => grid.set_opt(column, index, Some(CROSS), None, None),
|
||||
HOR | CROSS | HOR_U | HOR_D => {
|
||||
grid.set_opt(column, index, None, new_col, new_pers)
|
||||
}
|
||||
L_U | R_U => grid.set_opt(column, index, Some(HOR_U), new_col, new_pers),
|
||||
L_D | R_D => grid.set_opt(column, index, Some(HOR_D), new_col, new_pers),
|
||||
_ => {
|
||||
grid.set_opt(column, index, Some(HOR), new_col, new_pers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (left, _, old_pers) = grid.get_tuple(from_2, index);
|
||||
let (new_col, new_pers) = if pers < old_pers {
|
||||
(Some(color), Some(pers))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
match left {
|
||||
DOT | CIRCLE => {}
|
||||
VER => grid.set_opt(from_2, index, Some(VER_R), new_col, new_pers),
|
||||
VER_L => grid.set_opt(from_2, index, Some(CROSS), None, None),
|
||||
VER_R => {}
|
||||
HOR | L_U => grid.set_opt(from_2, index, Some(HOR_U), new_col, new_pers),
|
||||
_ => {
|
||||
grid.set_opt(from_2, index, Some(R_D), new_col, new_pers);
|
||||
}
|
||||
}
|
||||
|
||||
let (right, _, old_pers) = grid.get_tuple(to_2, index);
|
||||
let (new_col, new_pers) = if pers < old_pers {
|
||||
(Some(color), Some(pers))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
match right {
|
||||
DOT | CIRCLE => {}
|
||||
VER => grid.set_opt(to_2, index, Some(VER_L), None, None),
|
||||
VER_L | HOR_U => grid.set_opt(to_2, index, None, new_col, new_pers),
|
||||
HOR | R_U => grid.set_opt(to_2, index, Some(HOR_U), new_col, new_pers),
|
||||
_ => {
|
||||
grid.set_opt(to_2, index, Some(L_U), new_col, new_pers);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for column in (to_2 + 1)..from_2 {
|
||||
if merge && column == to_2 + 1 {
|
||||
grid.set(column, index, ARR_L, color, pers);
|
||||
} else {
|
||||
let (curr, _, old_pers) = grid.get_tuple(column, index);
|
||||
let (new_col, new_pers) = if pers < old_pers {
|
||||
(Some(color), Some(pers))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
match curr {
|
||||
DOT | CIRCLE => {}
|
||||
VER => grid.set_opt(column, index, Some(CROSS), None, None),
|
||||
HOR | CROSS | HOR_U | HOR_D => {
|
||||
grid.set_opt(column, index, None, new_col, new_pers)
|
||||
}
|
||||
L_U | R_U => grid.set_opt(column, index, Some(HOR_U), new_col, new_pers),
|
||||
L_D | R_D => grid.set_opt(column, index, Some(HOR_D), new_col, new_pers),
|
||||
_ => {
|
||||
grid.set_opt(column, index, Some(HOR), new_col, new_pers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (left, _, old_pers) = grid.get_tuple(to_2, index);
|
||||
let (new_col, new_pers) = if pers < old_pers {
|
||||
(Some(color), Some(pers))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
match left {
|
||||
DOT | CIRCLE => {}
|
||||
VER => grid.set_opt(to_2, index, Some(VER_R), None, None),
|
||||
VER_R => grid.set_opt(to_2, index, None, new_col, new_pers),
|
||||
HOR | L_U => grid.set_opt(to_2, index, Some(HOR_U), new_col, new_pers),
|
||||
_ => {
|
||||
grid.set_opt(to_2, index, Some(R_U), new_col, new_pers);
|
||||
}
|
||||
}
|
||||
|
||||
let (right, _, old_pers) = grid.get_tuple(from_2, index);
|
||||
let (new_col, new_pers) = if pers < old_pers {
|
||||
(Some(color), Some(pers))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
match right {
|
||||
DOT | CIRCLE => {}
|
||||
VER => grid.set_opt(from_2, index, Some(VER_L), new_col, new_pers),
|
||||
VER_R => grid.set_opt(from_2, index, Some(CROSS), None, None),
|
||||
VER_L => grid.set_opt(from_2, index, None, new_col, new_pers),
|
||||
HOR | R_D => grid.set_opt(from_2, index, Some(HOR_D), new_col, new_pers),
|
||||
_ => {
|
||||
grid.set_opt(from_2, index, Some(L_D), new_col, new_pers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates required additional rows
|
||||
fn get_inserts(graph: &GitGraph, compact: bool) -> HashMap<usize, Vec<Vec<Occ>>> {
|
||||
let mut inserts: HashMap<usize, Vec<Vec<Occ>>> = HashMap::new();
|
||||
|
||||
for (idx, info) in graph.commits.iter().enumerate() {
|
||||
let column = graph.all_branches[info.branch_trace.unwrap()]
|
||||
.visual
|
||||
.column
|
||||
.unwrap();
|
||||
|
||||
inserts.insert(idx, vec![vec![Occ::Commit(idx, column)]]);
|
||||
}
|
||||
|
||||
for (idx, info) in graph.commits.iter().enumerate() {
|
||||
if let Some(trace) = info.branch_trace {
|
||||
let branch = &graph.all_branches[trace];
|
||||
let column = branch.visual.column.unwrap();
|
||||
|
||||
for p in 0..2 {
|
||||
if let Some(par_oid) = info.parents[p] {
|
||||
if let Some(par_idx) = graph.indices.get(&par_oid) {
|
||||
let par_info = &graph.commits[*par_idx];
|
||||
let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
|
||||
let par_column = par_branch.visual.column.unwrap();
|
||||
let column_range = sorted(column, par_column);
|
||||
|
||||
if column != par_column {
|
||||
let split_index = super::get_deviate_index(graph, idx, *par_idx);
|
||||
match inserts.entry(split_index) {
|
||||
Occupied(mut entry) => {
|
||||
let mut insert_at = entry.get().len();
|
||||
for (insert_idx, sub_entry) in entry.get().iter().enumerate() {
|
||||
let mut occ = false;
|
||||
for other_range in sub_entry {
|
||||
if other_range.overlaps(&column_range) {
|
||||
match other_range {
|
||||
Occ::Commit(target_index, _) => {
|
||||
if !compact
|
||||
|| !info.is_merge
|
||||
|| idx != *target_index
|
||||
|| p == 0
|
||||
{
|
||||
occ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Occ::Range(o_idx, o_par_idx, _, _) => {
|
||||
if idx != *o_idx && par_idx != o_par_idx {
|
||||
occ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !occ {
|
||||
insert_at = insert_idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let vec = entry.get_mut();
|
||||
if insert_at == vec.len() {
|
||||
vec.push(vec![Occ::Range(
|
||||
idx,
|
||||
*par_idx,
|
||||
column_range.0,
|
||||
column_range.1,
|
||||
)]);
|
||||
} else {
|
||||
vec[insert_at].push(Occ::Range(
|
||||
idx,
|
||||
*par_idx,
|
||||
column_range.0,
|
||||
column_range.1,
|
||||
));
|
||||
}
|
||||
}
|
||||
Vacant(entry) => {
|
||||
entry.insert(vec![vec![Occ::Range(
|
||||
idx,
|
||||
*par_idx,
|
||||
column_range.0,
|
||||
column_range.1,
|
||||
)]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inserts
|
||||
}
|
||||
|
||||
/// Creates the complete graph visualization, incl. formatter commits.
|
||||
fn print_graph(
|
||||
characters: &Characters,
|
||||
grid: &Grid,
|
||||
text_lines: Vec<Option<String>>,
|
||||
color: bool,
|
||||
) -> (Vec<String>, Vec<String>) {
|
||||
let mut g_lines = vec![];
|
||||
let mut t_lines = vec![];
|
||||
|
||||
for (row, line) in grid.data.chunks(grid.width).zip(text_lines.into_iter()) {
|
||||
let mut g_out = String::new();
|
||||
let mut t_out = String::new();
|
||||
|
||||
if color {
|
||||
for arr in row {
|
||||
if arr[0] == SPACE {
|
||||
write!(g_out, "{}", characters.chars[arr[0] as usize])
|
||||
} else {
|
||||
write!(
|
||||
g_out,
|
||||
"{}",
|
||||
Paint::fixed(arr[1], characters.chars[arr[0] as usize])
|
||||
)
|
||||
}
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
let str = row
|
||||
.iter()
|
||||
.map(|arr| characters.chars[arr[0] as usize])
|
||||
.collect::<String>();
|
||||
write!(g_out, "{}", str).unwrap();
|
||||
}
|
||||
|
||||
if let Some(line) = line {
|
||||
write!(t_out, "{}", line).unwrap();
|
||||
}
|
||||
|
||||
g_lines.push(g_out);
|
||||
t_lines.push(t_out);
|
||||
}
|
||||
|
||||
(g_lines, t_lines)
|
||||
}
|
||||
|
||||
/// Format a commit.
|
||||
fn format(
|
||||
format: &CommitFormat,
|
||||
graph: &GitGraph,
|
||||
info: &CommitInfo,
|
||||
head: Option<&HeadInfo>,
|
||||
color: bool,
|
||||
wrapping: &Option<Options>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let commit = graph
|
||||
.repository
|
||||
.find_commit(info.oid)
|
||||
.map_err(|err| err.message().to_string())?;
|
||||
|
||||
let branch_str = format_branches(graph, info, head, color);
|
||||
|
||||
let hash_color = if color { Some(HASH_COLOR) } else { None };
|
||||
|
||||
crate::print::format::format(&commit, branch_str, wrapping, hash_color, format)
|
||||
}
|
||||
|
||||
/// Format branches and tags.
|
||||
pub fn format_branches(
|
||||
graph: &GitGraph,
|
||||
info: &CommitInfo,
|
||||
head: Option<&HeadInfo>,
|
||||
color: bool,
|
||||
) -> String {
|
||||
let curr_color = info
|
||||
.branch_trace
|
||||
.map(|branch_idx| &graph.all_branches[branch_idx].visual.term_color);
|
||||
|
||||
let mut branch_str = String::new();
|
||||
|
||||
let head_str = "HEAD ->";
|
||||
if let Some(head) = head {
|
||||
if !head.is_branch {
|
||||
if color {
|
||||
write!(branch_str, " {}", Paint::fixed(HEAD_COLOR, head_str))
|
||||
} else {
|
||||
write!(branch_str, " {}", head_str)
|
||||
}
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if !info.branches.is_empty() {
|
||||
write!(branch_str, " (").unwrap();
|
||||
|
||||
let branches = info.branches.iter().sorted_by_key(|br| {
|
||||
if let Some(head) = head {
|
||||
head.name != graph.all_branches[**br].name
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
for (idx, branch_index) in branches.enumerate() {
|
||||
let branch = &graph.all_branches[*branch_index];
|
||||
let branch_color = branch.visual.term_color;
|
||||
|
||||
if let Some(head) = head {
|
||||
if idx == 0 && head.is_branch {
|
||||
if color {
|
||||
write!(branch_str, "{} ", Paint::fixed(14, head_str))
|
||||
} else {
|
||||
write!(branch_str, "{} ", head_str)
|
||||
}
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if color {
|
||||
write!(branch_str, "{}", Paint::fixed(branch_color, &branch.name))
|
||||
} else {
|
||||
write!(branch_str, "{}", &branch.name)
|
||||
}
|
||||
.unwrap();
|
||||
|
||||
if idx < info.branches.len() - 1 {
|
||||
write!(branch_str, ", ").unwrap();
|
||||
}
|
||||
}
|
||||
write!(branch_str, ")").unwrap();
|
||||
}
|
||||
|
||||
if !info.tags.is_empty() {
|
||||
write!(branch_str, " [").unwrap();
|
||||
for (idx, tag_index) in info.tags.iter().enumerate() {
|
||||
let tag = &graph.all_branches[*tag_index];
|
||||
let tag_color = curr_color.unwrap_or(&tag.visual.term_color);
|
||||
|
||||
if color {
|
||||
write!(branch_str, "{}", Paint::fixed(*tag_color, &tag.name[5..]))
|
||||
} else {
|
||||
write!(branch_str, "{}", &tag.name[5..])
|
||||
}
|
||||
.unwrap();
|
||||
|
||||
if idx < info.tags.len() - 1 {
|
||||
write!(branch_str, ", ").unwrap();
|
||||
}
|
||||
}
|
||||
write!(branch_str, "]").unwrap();
|
||||
}
|
||||
|
||||
branch_str
|
||||
}
|
||||
|
||||
/// Occupied row ranges
|
||||
enum Occ {
|
||||
Commit(usize, usize),
|
||||
Range(usize, usize, usize, usize),
|
||||
}
|
||||
|
||||
impl Occ {
|
||||
fn overlaps(&self, (start, end): &(usize, usize)) -> bool {
|
||||
match self {
|
||||
Occ::Commit(_, col) => start <= col && end >= col,
|
||||
Occ::Range(_, _, s, e) => s <= end && e >= start,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sorts two numbers in ascending order
|
||||
fn sorted(v1: usize, v2: usize) -> (usize, usize) {
|
||||
if v2 > v1 {
|
||||
(v1, v2)
|
||||
} else {
|
||||
(v2, v1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Two-dimensional grid with 3 layers, used to produce the graph representation.
|
||||
#[allow(dead_code)]
|
||||
struct Grid {
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: Vec<[u8; 3]>,
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
pub fn new(width: usize, height: usize, initial: [u8; 3]) -> Self {
|
||||
Grid {
|
||||
width,
|
||||
height,
|
||||
data: vec![initial; width * height],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reverse(&mut self) {
|
||||
self.data.reverse();
|
||||
}
|
||||
pub fn index(&self, x: usize, y: usize) -> usize {
|
||||
y * self.width + x
|
||||
}
|
||||
pub fn get_tuple(&self, x: usize, y: usize) -> (u8, u8, u8) {
|
||||
let v = self.data[self.index(x, y)];
|
||||
(v[0], v[1], v[2])
|
||||
}
|
||||
pub fn set(&mut self, x: usize, y: usize, character: u8, color: u8, pers: u8) {
|
||||
let idx = self.index(x, y);
|
||||
self.data[idx] = [character, color, pers];
|
||||
}
|
||||
pub fn set_opt(
|
||||
&mut self,
|
||||
x: usize,
|
||||
y: usize,
|
||||
character: Option<u8>,
|
||||
color: Option<u8>,
|
||||
pers: Option<u8>,
|
||||
) {
|
||||
let idx = self.index(x, y);
|
||||
let arr = &mut self.data[idx];
|
||||
if let Some(character) = character {
|
||||
arr[0] = character;
|
||||
}
|
||||
if let Some(color) = color {
|
||||
arr[1] = color;
|
||||
}
|
||||
if let Some(pers) = pers {
|
||||
arr[2] = pers;
|
||||
}
|
||||
}
|
||||
}
|
353
src/settings.rs
Normal file
353
src/settings.rs
Normal file
|
@ -0,0 +1,353 @@
|
|||
//! Graph generation settings.
|
||||
|
||||
use crate::print::format::CommitFormat;
|
||||
use regex::{Error, Regex};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Repository settings for the branching model.
|
||||
/// Used to read repo's git-graph.toml
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RepoSettings {
|
||||
/// The repository's branching model
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
/// Ordering policy for branches in visual columns.
|
||||
pub enum BranchOrder {
|
||||
/// Recommended! Shortest branches are inserted left-most.
|
||||
///
|
||||
/// For branches with equal length, branches ending last are inserted first.
|
||||
/// Reverse (arg = false): Branches ending first are inserted first.
|
||||
ShortestFirst(bool),
|
||||
/// Longest branches are inserted left-most.
|
||||
///
|
||||
/// For branches with equal length, branches ending last are inserted first.
|
||||
/// Reverse (arg = false): Branches ending first are inserted first.
|
||||
LongestFirst(bool),
|
||||
}
|
||||
|
||||
/// Top-level settings
|
||||
pub struct Settings {
|
||||
/// Reverse the order of commits
|
||||
pub reverse_commit_order: bool,
|
||||
/// Debug printing and drawing
|
||||
pub debug: bool,
|
||||
/// Compact text-based graph
|
||||
pub compact: bool,
|
||||
/// Colored text-based graph
|
||||
pub colored: bool,
|
||||
/// Include remote branches?
|
||||
pub include_remote: bool,
|
||||
/// Formatting for commits
|
||||
pub format: CommitFormat,
|
||||
/// Text wrapping options
|
||||
pub wrapping: Option<(Option<usize>, Option<usize>, Option<usize>)>,
|
||||
/// Characters to use for text-based graph
|
||||
pub characters: Characters,
|
||||
/// Branch column sorting algorithm
|
||||
pub branch_order: BranchOrder,
|
||||
/// Settings for branches
|
||||
pub branches: BranchSettings,
|
||||
/// Regex patterns for finding branch names in merge commit summaries
|
||||
pub merge_patterns: MergePatterns,
|
||||
}
|
||||
|
||||
/// Helper for reading BranchSettings, required due to RegEx.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BranchSettingsDef {
|
||||
/// Branch persistence
|
||||
pub persistence: Vec<String>,
|
||||
/// Branch ordering
|
||||
pub order: Vec<String>,
|
||||
/// Branch colors
|
||||
pub terminal_colors: ColorsDef,
|
||||
/// Branch colors for SVG output
|
||||
pub svg_colors: ColorsDef,
|
||||
}
|
||||
|
||||
/// Helper for reading branch colors, required due to RegEx.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ColorsDef {
|
||||
matches: Vec<(String, Vec<String>)>,
|
||||
unknown: Vec<String>,
|
||||
}
|
||||
|
||||
impl BranchSettingsDef {
|
||||
/// The Git-Flow model.
|
||||
pub fn git_flow() -> Self {
|
||||
BranchSettingsDef {
|
||||
persistence: vec![
|
||||
r"^(master|main)$".to_string(),
|
||||
r"^(develop|dev)$".to_string(),
|
||||
r"^feature.*$".to_string(),
|
||||
r"^release.*$".to_string(),
|
||||
r"^hotfix.*$".to_string(),
|
||||
r"^bugfix.*$".to_string(),
|
||||
],
|
||||
order: vec![
|
||||
r"^(master|main)$".to_string(),
|
||||
r"^(hotfix|release).*$".to_string(),
|
||||
r"^(develop|dev)$".to_string(),
|
||||
],
|
||||
terminal_colors: ColorsDef {
|
||||
matches: vec![
|
||||
(
|
||||
r"^(master|main)$".to_string(),
|
||||
vec!["bright_blue".to_string()],
|
||||
),
|
||||
(
|
||||
r"^(develop|dev)$".to_string(),
|
||||
vec!["bright_yellow".to_string()],
|
||||
),
|
||||
(
|
||||
r"^(feature|fork/).*$".to_string(),
|
||||
vec!["bright_magenta".to_string(), "bright_cyan".to_string()],
|
||||
),
|
||||
(r"^release.*$".to_string(), vec!["bright_green".to_string()]),
|
||||
(
|
||||
r"^(bugfix|hotfix).*$".to_string(),
|
||||
vec!["bright_red".to_string()],
|
||||
),
|
||||
(r"^tags/.*$".to_string(), vec!["bright_green".to_string()]),
|
||||
],
|
||||
unknown: vec!["white".to_string()],
|
||||
},
|
||||
|
||||
svg_colors: ColorsDef {
|
||||
matches: vec![
|
||||
(r"^(master|main)$".to_string(), vec!["blue".to_string()]),
|
||||
(r"^(develop|dev)$".to_string(), vec!["orange".to_string()]),
|
||||
(
|
||||
r"^(feature|fork/).*$".to_string(),
|
||||
vec!["purple".to_string(), "turquoise".to_string()],
|
||||
),
|
||||
(r"^release.*$".to_string(), vec!["green".to_string()]),
|
||||
(r"^(bugfix|hotfix).*$".to_string(), vec!["red".to_string()]),
|
||||
(r"^tags/.*$".to_string(), vec!["green".to_string()]),
|
||||
],
|
||||
unknown: vec!["gray".to_string()],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple feature-based model.
|
||||
pub fn simple() -> Self {
|
||||
BranchSettingsDef {
|
||||
persistence: vec![r"^(master|main)$".to_string()],
|
||||
order: vec![r"^tags/.*$".to_string(), r"^(master|main)$".to_string()],
|
||||
terminal_colors: ColorsDef {
|
||||
matches: vec![
|
||||
(
|
||||
r"^(master|main)$".to_string(),
|
||||
vec!["bright_blue".to_string()],
|
||||
),
|
||||
(r"^tags/.*$".to_string(), vec!["bright_green".to_string()]),
|
||||
],
|
||||
unknown: vec![
|
||||
"bright_yellow".to_string(),
|
||||
"bright_green".to_string(),
|
||||
"bright_red".to_string(),
|
||||
"bright_magenta".to_string(),
|
||||
"bright_cyan".to_string(),
|
||||
],
|
||||
},
|
||||
|
||||
svg_colors: ColorsDef {
|
||||
matches: vec![
|
||||
(r"^(master|main)$".to_string(), vec!["blue".to_string()]),
|
||||
(r"^tags/.*$".to_string(), vec!["green".to_string()]),
|
||||
],
|
||||
unknown: vec![
|
||||
"orange".to_string(),
|
||||
"green".to_string(),
|
||||
"red".to_string(),
|
||||
"purple".to_string(),
|
||||
"turquoise".to_string(),
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Very simple model without any defined branch roles.
|
||||
pub fn none() -> Self {
|
||||
BranchSettingsDef {
|
||||
persistence: vec![],
|
||||
order: vec![],
|
||||
terminal_colors: ColorsDef {
|
||||
matches: vec![],
|
||||
unknown: vec![
|
||||
"bright_blue".to_string(),
|
||||
"bright_yellow".to_string(),
|
||||
"bright_green".to_string(),
|
||||
"bright_red".to_string(),
|
||||
"bright_magenta".to_string(),
|
||||
"bright_cyan".to_string(),
|
||||
],
|
||||
},
|
||||
|
||||
svg_colors: ColorsDef {
|
||||
matches: vec![],
|
||||
unknown: vec![
|
||||
"blue".to_string(),
|
||||
"orange".to_string(),
|
||||
"green".to_string(),
|
||||
"red".to_string(),
|
||||
"purple".to_string(),
|
||||
"turquoise".to_string(),
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings defining branching models
|
||||
pub struct BranchSettings {
|
||||
/// Branch persistence
|
||||
pub persistence: Vec<Regex>,
|
||||
/// Branch ordering
|
||||
pub order: Vec<Regex>,
|
||||
/// Branch colors
|
||||
pub terminal_colors: Vec<(Regex, Vec<String>)>,
|
||||
/// Colors for branches not matching any of `colors`
|
||||
pub terminal_colors_unknown: Vec<String>,
|
||||
/// Branch colors for SVG output
|
||||
pub svg_colors: Vec<(Regex, Vec<String>)>,
|
||||
/// Colors for branches not matching any of `colors` for SVG output
|
||||
pub svg_colors_unknown: Vec<String>,
|
||||
}
|
||||
|
||||
impl BranchSettings {
|
||||
pub fn from(def: BranchSettingsDef) -> Result<Self, Error> {
|
||||
let persistence = def
|
||||
.persistence
|
||||
.iter()
|
||||
.map(|str| Regex::new(str))
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
|
||||
let order = def
|
||||
.order
|
||||
.iter()
|
||||
.map(|str| Regex::new(str))
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
|
||||
let terminal_colors = def
|
||||
.terminal_colors
|
||||
.matches
|
||||
.into_iter()
|
||||
.map(|(str, vec)| Regex::new(&str).map(|re| (re, vec)))
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
|
||||
let terminal_colors_unknown = def.terminal_colors.unknown;
|
||||
|
||||
let svg_colors = def
|
||||
.svg_colors
|
||||
.matches
|
||||
.into_iter()
|
||||
.map(|(str, vec)| Regex::new(&str).map(|re| (re, vec)))
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
|
||||
let svg_colors_unknown = def.svg_colors.unknown;
|
||||
|
||||
Ok(BranchSettings {
|
||||
persistence,
|
||||
order,
|
||||
terminal_colors,
|
||||
terminal_colors_unknown,
|
||||
svg_colors,
|
||||
svg_colors_unknown,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// RegEx patterns for extracting branch names from merge commit summaries.
|
||||
pub struct MergePatterns {
|
||||
/// The patterns. Evaluated in the given order.
|
||||
pub patterns: Vec<Regex>,
|
||||
}
|
||||
|
||||
impl Default for MergePatterns {
|
||||
fn default() -> Self {
|
||||
MergePatterns {
|
||||
patterns: vec![
|
||||
// GitLab pull request
|
||||
Regex::new(r"^Merge branch '(.+)' into '.+'$").unwrap(),
|
||||
// Git default
|
||||
Regex::new(r"^Merge branch '(.+)' into .+$").unwrap(),
|
||||
// Git default into main branch
|
||||
Regex::new(r"^Merge branch '(.+)'$").unwrap(),
|
||||
// GitHub pull request
|
||||
Regex::new(r"^Merge pull request #[0-9]+ from .[^/]+/(.+)$").unwrap(),
|
||||
// GitHub pull request (from fork?)
|
||||
Regex::new(r"^Merge branch '(.+)' of .+$").unwrap(),
|
||||
// BitBucket pull request
|
||||
Regex::new(r"^Merged in (.+) \(pull request #[0-9]+\)$").unwrap(),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The characters used for drawing text-based graphs.
|
||||
pub struct Characters {
|
||||
pub chars: Vec<char>,
|
||||
}
|
||||
|
||||
impl FromStr for Characters {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(str: &str) -> Result<Self, Self::Err> {
|
||||
match str {
|
||||
"normal" | "thin" | "n" | "t" => Ok(Characters::thin()),
|
||||
"round" | "r" => Ok(Characters::round()),
|
||||
"bold" | "b" => Ok(Characters::bold()),
|
||||
"double" | "d" => Ok(Characters::double()),
|
||||
"ascii" | "a" => Ok(Characters::ascii()),
|
||||
_ => Err(format!("Unknown characters/style '{}'. Must be one of [normal|thin|round|bold|double|ascii]", str)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Characters {
|
||||
/// Default/thin graphs
|
||||
pub fn thin() -> Self {
|
||||
Characters {
|
||||
chars: " ●○│─┼└┌┐┘┤├┴┬<>".chars().collect(),
|
||||
}
|
||||
}
|
||||
/// Graphs with rounded corners
|
||||
pub fn round() -> Self {
|
||||
Characters {
|
||||
chars: " ●○│─┼╰╭╮╯┤├┴┬<>".chars().collect(),
|
||||
}
|
||||
}
|
||||
/// Bold/fat graphs
|
||||
pub fn bold() -> Self {
|
||||
Characters {
|
||||
chars: " ●○┃━╋┗┏┓┛┫┣┻┳<>".chars().collect(),
|
||||
}
|
||||
}
|
||||
/// Double-lined graphs
|
||||
pub fn double() -> Self {
|
||||
Characters {
|
||||
chars: " ●○║═╬╚╔╗╝╣╠╩╦<>".chars().collect(),
|
||||
}
|
||||
}
|
||||
/// ASCII-only graphs
|
||||
pub fn ascii() -> Self {
|
||||
Characters {
|
||||
chars: " *o|-+'..'||++<>".chars().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reverse(self) -> Self {
|
||||
let mut chars = self.chars;
|
||||
|
||||
chars.swap(6, 8);
|
||||
chars.swap(7, 9);
|
||||
chars.swap(10, 11);
|
||||
chars.swap(12, 13);
|
||||
chars.swap(14, 15);
|
||||
|
||||
Characters { chars }
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue