├── .gitignore ├── .github └── workflows │ ├── crates-io.yml │ ├── tests.yml │ └── release.yml ├── Justfile ├── src ├── lib.rs ├── print │ ├── colors.rs │ ├── mod.rs │ ├── svg.rs │ └── format.rs ├── config.rs ├── settings.rs ├── main.rs └── graph.rs ├── LICENSE ├── Cargo.toml ├── docs ├── branch_assignment.md └── manual.md ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.iml 3 | /.idea/ 4 | -------------------------------------------------------------------------------- /.github/workflows/crates-io.yml: -------------------------------------------------------------------------------- 1 | name: Crates.io 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | publish: 12 | name: Publish to crates.io 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Install Rust 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | override: true 22 | - name: Publish binaries 23 | uses: katyo/publish-crates@v1 24 | with: 25 | registry-token: ${{ secrets.CRATES_IO_TOKEN }} 26 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | _default: 2 | @just --choose 3 | 4 | # Shows a list of all available recipes 5 | help: 6 | @just --list 7 | 8 | green := '\033[0;32m' 9 | red := '\033[0;31m' 10 | reset := '\033[0m' 11 | 12 | # Checks if all requirements to work on this project are installed 13 | check-requirements: 14 | @command -v cargo &>/dev/null && echo -e "{{ green }}✓{{ reset}} cargo installed" || echo -e "{{ red }}✖{{ reset }} cargo missing" 15 | 16 | # Runs the same linters as the pipeline with fix option 17 | lint: 18 | cargo fmt --all # Is in fix mode by default 19 | cargo clippy --all --all-targets --allow-dirty --fix 20 | 21 | # Runs the same test as the pipeline but locally 22 | test: 23 | cargo test --all 24 | 25 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! git-graph shows clear git graphs arranged for your branching model. 2 | //! 3 | //! It provides both a library and a command line tool. 4 | //! 5 | //! The main steps are: 6 | //! 1. Read branching model configuration (See [config] and [settings]) 7 | //! 2. Lay out the graph structure according to the branching model (See [graph]) 8 | //! 3. Render the layout to text or SVG (See [mod@print]) 9 | 10 | // Configure clippy to look for complex functions 11 | #![warn(clippy::cognitive_complexity)] 12 | #![warn(clippy::too_many_lines)] 13 | 14 | use git2::Repository; 15 | use std::path::Path; 16 | 17 | pub mod config; 18 | pub mod graph; 19 | pub mod print; 20 | pub mod settings; 21 | 22 | pub fn get_repo>( 23 | path: P, 24 | skip_repo_owner_validation: bool, 25 | ) -> Result { 26 | if skip_repo_owner_validation { 27 | unsafe { git2::opts::set_verify_owner_validation(false)? } 28 | } 29 | Repository::discover(path) 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Martin Lange 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "git-graph" 3 | version = "0.7.0" 4 | authors = ["Martin Lange "] 5 | description = "Command line tool to show clear git graphs arranged for your branching model" 6 | repository = "https://github.com/mlange-42/git-graph.git" 7 | keywords = ["git", "graph"] 8 | license = "MIT" 9 | readme = "README.md" 10 | edition = "2021" 11 | rust-version = "1.83.0" 12 | 13 | [profile.release] 14 | opt-level = 3 15 | lto = true 16 | codegen-units = 1 17 | debug = false 18 | debug-assertions = false 19 | overflow-checks = false 20 | 21 | [dependencies] 22 | git2 = {version = "0.20", default-features = false, optional = false} 23 | regex = {version = "1.7", default-features = false, optional = false, features = ["std"]} 24 | serde = "1.0" 25 | serde_derive = {version = "1.0", default-features = false, optional = false} 26 | toml = "0.9" 27 | itertools = "0.14" 28 | svg = "0.18" 29 | clap = {version = "4.0", optional = false, features = ["cargo"]} 30 | lazy_static = "1.4" 31 | yansi = "1.0" 32 | atty = "0.2" 33 | platform-dirs = "0.3" 34 | crossterm = {version = "0.29", optional = false} 35 | chrono = {version = "0.4", optional = false} 36 | textwrap = {version = "0.16", default-features = false, optional = false, features = ["unicode-width"]} 37 | -------------------------------------------------------------------------------- /src/print/colors.rs: -------------------------------------------------------------------------------- 1 | //! ANSI terminal color handling. 2 | 3 | use lazy_static::lazy_static; 4 | use std::collections::HashMap; 5 | 6 | /// Converts a color name to the index in the 256-color palette. 7 | pub fn to_terminal_color(color: &str) -> Result { 8 | match NAMED_COLORS.get(color) { 9 | None => match color.parse::() { 10 | Ok(col) => Ok(col), 11 | Err(_) => Err(format!("Color {} not found", color)), 12 | }, 13 | Some(rgb) => Ok(*rgb), 14 | } 15 | } 16 | 17 | macro_rules! hashmap { 18 | ($( $key: expr => $val: expr ),*) => {{ 19 | let mut map = ::std::collections::HashMap::new(); 20 | $( map.insert($key, $val); )* 21 | map 22 | }} 23 | } 24 | 25 | lazy_static! { 26 | /// Named ANSI colors 27 | pub static ref NAMED_COLORS: HashMap<&'static str, u8> = hashmap![ 28 | "black" => 0, 29 | "red" => 1, 30 | "green" => 2, 31 | "yellow" => 3, 32 | "blue" => 4, 33 | "magenta" => 5, 34 | "cyan" => 6, 35 | "white" => 7, 36 | "bright_black" => 8, 37 | "bright_red" => 9, 38 | "bright_green" => 10, 39 | "bright_yellow" => 11, 40 | "bright_blue" => 12, 41 | "bright_magenta" => 13, 42 | "bright_cyan" => 14, 43 | "bright_white" => 15 44 | ]; 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | test: 13 | name: Test Suite 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout sources 17 | uses: actions/checkout@v2 18 | 19 | - name: Install stable toolchain 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | override: true 25 | 26 | - name: Run cargo test 27 | uses: actions-rs/cargo@v1 28 | with: 29 | command: test 30 | args: --all 31 | 32 | lints: 33 | name: Lints 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout sources 37 | uses: actions/checkout@v2 38 | 39 | - name: Install stable toolchain 40 | uses: actions-rs/toolchain@v1 41 | with: 42 | profile: minimal 43 | toolchain: stable 44 | override: true 45 | components: rustfmt, clippy 46 | 47 | - name: Run cargo fmt 48 | uses: actions-rs/cargo@v1 49 | with: 50 | command: fmt 51 | args: --all -- --check 52 | 53 | - name: Run cargo clippy 54 | uses: actions-rs/cargo@v1 55 | with: 56 | command: clippy 57 | args: --all --all-targets -- --deny warnings 58 | -------------------------------------------------------------------------------- /src/print/mod.rs: -------------------------------------------------------------------------------- 1 | //! Create visual representations of git graphs. 2 | 3 | use crate::graph::GitGraph; 4 | use std::cmp::max; 5 | 6 | pub mod colors; 7 | pub mod format; 8 | pub mod svg; 9 | pub mod unicode; 10 | 11 | /// Find the index at which a between-branch connection 12 | /// has to deviate from the current branch's column. 13 | /// 14 | /// Returns the last index on the current column. 15 | fn get_deviate_index(graph: &GitGraph, index: usize, par_index: usize) -> usize { 16 | let info = &graph.commits[index]; 17 | 18 | let par_info = &graph.commits[par_index]; 19 | let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()]; 20 | 21 | let mut min_split_idx = index; 22 | for sibling_oid in &par_info.children { 23 | if let Some(&sibling_index) = graph.indices.get(sibling_oid) { 24 | if let Some(sibling) = graph.commits.get(sibling_index) { 25 | if let Some(sibling_trace) = sibling.branch_trace { 26 | let sibling_branch = &graph.all_branches[sibling_trace]; 27 | if sibling_oid != &info.oid 28 | && sibling_branch.visual.column == par_branch.visual.column 29 | && sibling_index > min_split_idx 30 | { 31 | min_split_idx = sibling_index; 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | // TODO: in cases where no crossings occur, the rule for merge commits can also be applied to normal commits 39 | // See also branch::trace_branch() 40 | if info.is_merge { 41 | max(index, min_split_idx) 42 | } else { 43 | (par_index as i32 - 1) as usize 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | push: 7 | branches: [master] 8 | pull_request: 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | REPO: git-graph 13 | 14 | jobs: 15 | release: 16 | name: Release for ${{ matrix.os }} 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | include: 21 | - os: ubuntu-latest 22 | bin_extension: "" 23 | os_name: "linux-amd64" 24 | - os: windows-latest 25 | bin_extension: ".exe" 26 | os_name: "windows-amd64" 27 | - os: macos-latest 28 | bin_extension: "" 29 | os_name: "macos-amd64" 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | 34 | - name: Get tag or commit 35 | run: | 36 | if [[ "${GITHUB_REF}" == refs/tags/* ]]; then 37 | echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 38 | else 39 | SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) 40 | echo "RELEASE_VERSION=${SHORT_SHA}" >> $GITHUB_ENV 41 | fi 42 | shell: bash 43 | 44 | - name: Build 45 | run: | 46 | cargo build --release 47 | 48 | - name: Compress 49 | run: | 50 | cp -f target/release/$REPO${{ matrix.bin_extension }} . 51 | tar -czf release.tar.gz $REPO${{ matrix.bin_extension }} 52 | shell: bash 53 | 54 | - name: Archive build artifacts 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: ${{ env.REPO }}-${{ env.RELEASE_VERSION }}-${{ matrix.os_name }} 58 | path: release.tar.gz 59 | 60 | - name: Upload binaries to release 61 | if: github.event_name == 'release' 62 | uses: svenstaro/upload-release-action@v2 63 | with: 64 | repo_token: ${{ secrets.GITHUB_TOKEN }} 65 | file: release.tar.gz 66 | asset_name: ${{ env.REPO }}-${{ env.RELEASE_VERSION }}-${{ matrix.os_name }}.tar.gz 67 | tag: ${{ github.ref }} 68 | -------------------------------------------------------------------------------- /docs/branch_assignment.md: -------------------------------------------------------------------------------- 1 | 2 | # Overview 3 | 4 | To generate a graph, [GitGraph::new()] will read the repository 5 | and assign every commit to a single branch. 6 | 7 | It takes the following steps to generate the graph 8 | 9 | - Identify branches 10 | - Sort branches by persistence 11 | - Trace branches to commits 12 | - Filtering and indexing 13 | 14 | ## Identify branches 15 | Local and remote git-branches and tags are used as candidates for branches. 16 | A branch can be identified by a merge commit, even though no current git-branch 17 | refers to it. 18 | 19 | ## Sort branches by persistence 20 | Each branch is assigned a persistence which can be configured by settings. 21 | Think of persistence as z-order where lower values take preceedence. 22 | **TODO** Merge branch 23 | 24 | ## Trace branches to commits 25 | The branches now get to pick their commits, in order of persistence. Each 26 | branch starts with a head, and follow the primary parent while it is 27 | available. It stops when the parent is a commit already assigned to a branch. 28 | **TODO** Duplicate branch names 29 | **TODO** Handle visual artifacts on merge 30 | 31 | ## Filtering and indexing 32 | Commits that have not been assigned a branch is filtered out. 33 | An *index_map* is created to map from original commit index, to filtered 34 | commit index. 35 | **TODO** what? why? Would it not be better to track from child/heads instead of every single commit in repo? 36 | 37 | 38 | 39 | 40 | # Branch sorting 41 | The goal of this algorithm is to assign a column number to each tracked branch so that they can be visualized linearly without overlapping in the graph. It uses a shortest-first scheduling strategy (optionally longest-first and with forward/backward start sorting). 42 | 43 | ## Initialization 44 | - occupied: A vector of vectors of vectors of tuples. 45 | The outer vector is indexed by the branch's order_group (determined by branch_order based on the settings.branches.order). 46 | Each inner vector represents a column within that order group, 47 | and the tuples (start, end) store the range of commits occupied by a branch in that column. 48 | 49 | ## Preparing Branches for Sorting 50 | - It creates branches_sort, a vector of tuples containing the branch index, its start commit index (range.0), its end commit index (range.1), its source order group, and its target order group. 51 | - It filters out branches that don't have a defined range (meaning they weren't associated with any commits). 52 | ## Sorting Branches 53 | - The branches_sort vector is sorted based on a key that prioritizes: 54 | 1. The maximum of the source and target order groups. This likely aims to keep related branches (e.g., those involved in merges) closer together. 55 | 2. The length of the branch's lifespan (end - start commit index), either shortest-first or longest-first based on the shortest_first setting. 56 | 3. The starting commit index, either forward or backward based on the forward setting. 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-graph 2 | 3 | [![Tests](https://github.com/mlange-42/git-graph/actions/workflows/tests.yml/badge.svg)](https://github.com/mlange-42/git-graph/actions/workflows/tests.yml) 4 | [![GitHub](https://img.shields.io/badge/github-repo-blue?logo=github)](https://github.com/mlange-42/git-graph) 5 | [![Crate](https://img.shields.io/crates/v/git-graph.svg)](https://crates.io/crates/git-graph) 6 | [![MIT license](https://img.shields.io/github/license/mlange-42/git-graph)](https://github.com/mlange-42/git-graph/blob/master/LICENSE) 7 | 8 | A command line tool to visualize Git history graphs in a comprehensible way, following different branching models. 9 | 10 | 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. 11 | 12 | > GitFlow was chosen for its complexity, while any other branching model is supported, including user-defined ones. 13 | 14 | ![Graph comparison between tools](https://user-images.githubusercontent.com/44003176/103466403-36a81780-4d45-11eb-90cc-167d210d7a52.png) 15 | 16 | Decide for yourself which graph is the most comprehensible. :sunglasses: 17 | 18 | If you want an **interactive Git terminal application**, see [**git-igitt**](https://github.com/mlange-42/git-igitt), which is based on git-graph. 19 | 20 | ## Features 21 | 22 | * View structured graphs directly in the terminal 23 | * Pre-defined and custom branching models and coloring 24 | * Different styles, including ASCII-only (i.e. no "special characters") 25 | * Custom commit formatting, like with `git log --format="..."` 26 | 27 | ## Installation 28 | 29 | **Pre-compiled binaries** 30 | 31 | 1. Download the [latest binaries](https://github.com/mlange-42/git-graph/releases) for your platform 32 | 2. Unzip somewhere 33 | 3. *Optional:* add directory `git-graph` to your `PATH` environmental variable 34 | 35 | **Using `cargo`** 36 | 37 | In case you have [Rust](https://www.rust-lang.org/) installed, you can install with `cargo`: 38 | 39 | ``` 40 | cargo install git-graph 41 | ``` 42 | 43 | **From `homebrew`** 44 | 45 | If you use the [homebrew](https://brew.sh/) package manager: 46 | 47 | ``` 48 | brew install git-graph 49 | ``` 50 | 51 | ## Usage 52 | 53 | **For detailed information, see the [manual](docs/manual.md)**. 54 | 55 | For basic usage, run the following command inside a Git repository's folder: 56 | 57 | ``` 58 | git-graph 59 | ``` 60 | 61 | > Note: git-graph needs to be on the PATH, or you need use the full path to git-graph: 62 | > 63 | > ``` 64 | > C:/path/to/git-graph/git-graph 65 | > ``` 66 | 67 | **Branching models** 68 | 69 | Run git-graph with a specific model, e.g. `simple`: 70 | 71 | ``` 72 | git-graph --model simple 73 | ``` 74 | 75 | Alternatively, set the model for the current repository permanently: 76 | 77 | ``` 78 | git-graph model simple 79 | ``` 80 | 81 | **Get help** 82 | 83 | For the full CLI help describing all options, use: 84 | 85 | ``` 86 | git-graph -h 87 | git-graph --help 88 | ``` 89 | 90 | For **styles** and commit **formatting**, see the [manual](docs/manual.md). 91 | 92 | ## Custom branching models 93 | 94 | Branching models are configured using the files in `APP_DATA/git-graph/models`. 95 | 96 | * Windows: `C:\Users\\AppData\Roaming\git-graph` 97 | * Linux: `~/.config/git-graph` 98 | * OSX: `~/Library/Application Support/git-graph` 99 | 100 | 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: 101 | 102 | ``` 103 | git-graph --model my-model 104 | ``` 105 | 106 | **For details on how to create your own branching models see the manual, section [Custom branching models](docs/manual.md#custom-branching-models).** 107 | 108 | ## Limitations 109 | 110 | * Summaries of merge commits (i.e. 1st line of message) should not be modified! git-graph needs them to categorize merged branches. 111 | * Supports only the primary remote repository `origin`. 112 | * Does currently not support "octopus merges" (i.e. no more than 2 parents) 113 | * On Windows PowerShell, piping to file output does not work properly (changes encoding), so you may want to use the default Windows console instead 114 | 115 | ## Contributing 116 | 117 | Please report any issues and feature requests in the [issue tracker](https://github.com/mlange-42/git-graph/issues). 118 | 119 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 120 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Branching model configurations. 2 | //! 3 | //! In this module you will find functions to read and write branching model 4 | //! configurations on disk. 5 | //! 6 | //! The [branching models][BranchSettingsDef] themselves are defined in 7 | //! module [settings][super::settings] 8 | 9 | use crate::settings::{BranchSettingsDef, RepoSettings}; 10 | use git2::Repository; 11 | use std::ffi::OsStr; 12 | use std::path::{Path, PathBuf}; 13 | 14 | /// Creates the directory `APP_DATA/git-graph/models` if it does not exist, 15 | /// and writes the files for built-in branching models there. 16 | pub fn create_config + AsRef>(app_model_path: &P) -> Result<(), String> { 17 | let path: &Path = app_model_path.as_ref(); 18 | if !path.exists() { 19 | std::fs::create_dir_all(app_model_path).map_err(|err| err.to_string())?; 20 | 21 | let models = [ 22 | (BranchSettingsDef::git_flow(), "git-flow.toml"), 23 | (BranchSettingsDef::simple(), "simple.toml"), 24 | (BranchSettingsDef::none(), "none.toml"), 25 | ]; 26 | for (model, file) in &models { 27 | let mut path = PathBuf::from(&app_model_path); 28 | path.push(file); 29 | let str = toml::to_string_pretty(&model).map_err(|err| err.to_string())?; 30 | std::fs::write(&path, str).map_err(|err| err.to_string())?; 31 | } 32 | } 33 | 34 | Ok(()) 35 | } 36 | 37 | /// Get models available in `APP_DATA/git-graph/models`. 38 | pub fn get_available_models>(app_model_path: &P) -> Result, String> { 39 | let models = std::fs::read_dir(app_model_path) 40 | .map_err(|err| err.to_string())? 41 | .filter_map(|e| match e { 42 | Ok(e) => { 43 | if let (Some(name), Some(ext)) = (e.path().file_name(), e.path().extension()) { 44 | if ext == "toml" { 45 | name.to_str() 46 | .map(|name| (name[..(name.len() - 5)]).to_string()) 47 | } else { 48 | None 49 | } 50 | } else { 51 | None 52 | } 53 | } 54 | Err(_) => None, 55 | }) 56 | .collect::>(); 57 | 58 | Ok(models) 59 | } 60 | 61 | /// Get the currently set branching model for a repo. 62 | pub fn get_model_name(repository: &Repository, file_name: &str) -> Result, String> { 63 | let mut config_path = PathBuf::from(repository.path()); 64 | config_path.push(file_name); 65 | 66 | if config_path.exists() { 67 | let repo_config: RepoSettings = 68 | toml::from_str(&std::fs::read_to_string(config_path).map_err(|err| err.to_string())?) 69 | .map_err(|err| err.to_string())?; 70 | 71 | Ok(Some(repo_config.model)) 72 | } else { 73 | Ok(None) 74 | } 75 | } 76 | 77 | /// Try to get the branch settings for a given model. 78 | /// If no model name is given, returns the branch settings set for the repo, or the default otherwise. 79 | pub fn get_model + AsRef>( 80 | repository: &Repository, 81 | model: Option<&str>, 82 | repo_config_file: &str, 83 | app_model_path: &P, 84 | ) -> Result { 85 | match model { 86 | Some(model) => read_model(model, app_model_path), 87 | None => { 88 | let mut config_path = PathBuf::from(repository.path()); 89 | config_path.push(repo_config_file); 90 | 91 | if config_path.exists() { 92 | let repo_config: RepoSettings = toml::from_str( 93 | &std::fs::read_to_string(config_path).map_err(|err| err.to_string())?, 94 | ) 95 | .map_err(|err| err.to_string())?; 96 | 97 | read_model(&repo_config.model, app_model_path) 98 | } else { 99 | Ok(read_model("git-flow", app_model_path) 100 | .unwrap_or_else(|_| BranchSettingsDef::git_flow())) 101 | } 102 | } 103 | } 104 | } 105 | 106 | /// Read a branching model file. 107 | fn read_model + AsRef>( 108 | model: &str, 109 | app_model_path: &P, 110 | ) -> Result { 111 | let mut model_file = PathBuf::from(&app_model_path); 112 | model_file.push(format!("{}.toml", model)); 113 | 114 | if model_file.exists() { 115 | toml::from_str::( 116 | &std::fs::read_to_string(model_file).map_err(|err| err.to_string())?, 117 | ) 118 | .map_err(|err| err.to_string()) 119 | } else { 120 | let models = get_available_models(&app_model_path)?; 121 | let path: &Path = app_model_path.as_ref(); 122 | Err(format!( 123 | "ERROR: No branching model named '{}' found in {}\n Available models are: {}", 124 | model, 125 | path.display(), 126 | itertools::join(models, ", ") 127 | )) 128 | } 129 | } 130 | /// Permanently sets the branching model for a repository 131 | pub fn set_model>( 132 | repository: &Repository, 133 | model: &str, 134 | repo_config_file: &str, 135 | app_model_path: &P, 136 | ) -> Result<(), String> { 137 | let models = get_available_models(&app_model_path)?; 138 | 139 | if !models.contains(&model.to_string()) { 140 | return Err(format!( 141 | "ERROR: No branching model named '{}' found in {}\n Available models are: {}", 142 | model, 143 | app_model_path.as_ref().display(), 144 | itertools::join(models, ", ") 145 | )); 146 | } 147 | 148 | let mut config_path = PathBuf::from(repository.path()); 149 | config_path.push(repo_config_file); 150 | 151 | let config = RepoSettings { 152 | model: model.to_string(), 153 | }; 154 | 155 | let str = toml::to_string_pretty(&config).map_err(|err| err.to_string())?; 156 | std::fs::write(&config_path, str).map_err(|err| err.to_string())?; 157 | 158 | Ok(()) 159 | } 160 | -------------------------------------------------------------------------------- /src/print/svg.rs: -------------------------------------------------------------------------------- 1 | //! Create graphs in SVG format (Scalable Vector Graphics). 2 | 3 | use crate::graph::GitGraph; 4 | use crate::settings::Settings; 5 | use svg::node::element::path::Data; 6 | use svg::node::element::{Circle, Line, Path}; 7 | use svg::Document; 8 | 9 | /// Creates a SVG visual representation of a graph. 10 | pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result { 11 | let mut document = Document::new(); 12 | 13 | let max_idx = graph.commits.len(); 14 | let mut max_column = 0; 15 | 16 | if settings.debug { 17 | for branch in &graph.all_branches { 18 | if let (Some(start), Some(end)) = branch.range { 19 | document = document.add(bold_line( 20 | start, 21 | branch.visual.column.unwrap(), 22 | end, 23 | branch.visual.column.unwrap(), 24 | "cyan", 25 | )); 26 | } 27 | } 28 | } 29 | 30 | for (idx, info) in graph.commits.iter().enumerate() { 31 | if let Some(trace) = info.branch_trace { 32 | let branch = &graph.all_branches[trace]; 33 | let branch_color = &branch.visual.svg_color; 34 | 35 | if branch.visual.column.unwrap() > max_column { 36 | max_column = branch.visual.column.unwrap(); 37 | } 38 | 39 | for p in 0..2 { 40 | let parent = info.parents[p]; 41 | let Some(par_oid) = parent else { 42 | continue; 43 | }; 44 | let Some(par_idx) = graph.indices.get(&par_oid) else { 45 | // Parent is outside scope of graph.indices 46 | // so draw a vertical line to the bottom 47 | let idx_bottom = max_idx; 48 | document = document.add(line( 49 | idx, 50 | branch.visual.column.unwrap(), 51 | idx_bottom, 52 | branch.visual.column.unwrap(), 53 | branch_color, 54 | )); 55 | continue; 56 | }; 57 | let par_info = &graph.commits[*par_idx]; 58 | let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()]; 59 | 60 | let color = if info.is_merge { 61 | &par_branch.visual.svg_color 62 | } else { 63 | branch_color 64 | }; 65 | 66 | if branch.visual.column == par_branch.visual.column { 67 | document = document.add(line( 68 | idx, 69 | branch.visual.column.unwrap(), 70 | *par_idx, 71 | par_branch.visual.column.unwrap(), 72 | color, 73 | )); 74 | } else { 75 | let split_index = super::get_deviate_index(graph, idx, *par_idx); 76 | document = document.add(path( 77 | idx, 78 | branch.visual.column.unwrap(), 79 | *par_idx, 80 | par_branch.visual.column.unwrap(), 81 | split_index, 82 | color, 83 | )); 84 | } 85 | } 86 | 87 | document = document.add(commit_dot( 88 | idx, 89 | branch.visual.column.unwrap(), 90 | branch_color, 91 | !info.is_merge, 92 | )); 93 | } 94 | } 95 | let (x_max, y_max) = commit_coord(max_idx + 1, max_column + 1); 96 | document = document 97 | .set("viewBox", (0, 0, x_max, y_max)) 98 | .set("width", x_max) 99 | .set("height", y_max); 100 | 101 | let mut out: Vec = vec![]; 102 | svg::write(&mut out, &document).map_err(|err| err.to_string())?; 103 | Ok(String::from_utf8(out).unwrap_or_else(|_| "Invalid UTF8 character.".to_string())) 104 | } 105 | 106 | fn commit_dot(index: usize, column: usize, color: &str, filled: bool) -> Circle { 107 | let (x, y) = commit_coord(index, column); 108 | Circle::new() 109 | .set("cx", x) 110 | .set("cy", y) 111 | .set("r", 4) 112 | .set("fill", if filled { color } else { "white" }) 113 | .set("stroke", color) 114 | .set("stroke-width", 1) 115 | } 116 | 117 | fn line(index1: usize, column1: usize, index2: usize, column2: usize, color: &str) -> Line { 118 | let (x1, y1) = commit_coord(index1, column1); 119 | let (x2, y2) = commit_coord(index2, column2); 120 | Line::new() 121 | .set("x1", x1) 122 | .set("y1", y1) 123 | .set("x2", x2) 124 | .set("y2", y2) 125 | .set("stroke", color) 126 | .set("stroke-width", 1) 127 | } 128 | 129 | fn bold_line(index1: usize, column1: usize, index2: usize, column2: usize, color: &str) -> Line { 130 | let (x1, y1) = commit_coord(index1, column1); 131 | let (x2, y2) = commit_coord(index2, column2); 132 | Line::new() 133 | .set("x1", x1) 134 | .set("y1", y1) 135 | .set("x2", x2) 136 | .set("y2", y2) 137 | .set("stroke", color) 138 | .set("stroke-width", 5) 139 | } 140 | 141 | fn path( 142 | index1: usize, 143 | column1: usize, 144 | index2: usize, 145 | column2: usize, 146 | split_idx: usize, 147 | color: &str, 148 | ) -> Path { 149 | let c0 = commit_coord(index1, column1); 150 | 151 | let c1 = commit_coord(split_idx, column1); 152 | let c2 = commit_coord(split_idx + 1, column2); 153 | 154 | let c3 = commit_coord(index2, column2); 155 | 156 | let m = (0.5 * (c1.0 + c2.0), 0.5 * (c1.1 + c2.1)); 157 | 158 | let data = Data::new() 159 | .move_to(c0) 160 | .line_to(c1) 161 | .quadratic_curve_to((c1.0, m.1, m.0, m.1)) 162 | .quadratic_curve_to((c2.0, m.1, c2.0, c2.1)) 163 | .line_to(c3); 164 | 165 | Path::new() 166 | .set("d", data) 167 | .set("fill", "none") 168 | .set("stroke", color) 169 | .set("stroke-width", 1) 170 | } 171 | 172 | fn commit_coord(index: usize, column: usize) -> (f32, f32) { 173 | (15.0 * (column as f32 + 1.0), 15.0 * (index as f32 + 1.0)) 174 | } 175 | -------------------------------------------------------------------------------- /docs/manual.md: -------------------------------------------------------------------------------- 1 | # git-graph manual 2 | 3 | **Content** 4 | 5 | * [Overview](#overview) 6 | * [Options](#options) 7 | * [Formatting](#formatting) 8 | * [Custom branching models](#custom-branching-models) 9 | 10 | ## Overview 11 | 12 | The most basic usage is to simply call git-graph from inside a Git repository: 13 | 14 | ``` 15 | git-graph 16 | ``` 17 | 18 | This works also deeper down the directory tree, so no need to be in the repository's root folder. 19 | 20 | Alternatively, the path to the repository to visualize can be specified with option `--path`: 21 | 22 | ``` 23 | git-graph --path "path/to/repo" 24 | ``` 25 | 26 | **Branching models** 27 | 28 | The above call assumes the GitFlow branching model (the default). Different branching models can be used with the option `--model` or `-m`: 29 | 30 | ``` 31 | git-graph --model simple 32 | ``` 33 | 34 | To *permanently* set the branching model for a repository, use subcommand `model`, like 35 | 36 | ``` 37 | git-graph model simple 38 | ``` 39 | 40 | Use the subcommand without argument to view the currently set branching model of a repository: 41 | 42 | ``` 43 | git-graph model 44 | ``` 45 | 46 | To view all available branching models, use option `--list` or `-l` of the subcommand: 47 | 48 | ``` 49 | git-graph model --list 50 | ``` 51 | 52 | For **defining your own models**, see section [Custom branching models](#custom-branching-models). 53 | 54 | **Styles** 55 | 56 | 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`: 57 | 58 | ``` 59 | git-graph --style round 60 | ``` 61 | 62 | ![styles](https://user-images.githubusercontent.com/44003176/103467621-357ce780-4d51-11eb-8ff9-dd7be8b40f84.png) 63 | 64 | Style `ascii` can be used for devices and media that do not support Unicode/UTF-8 characters. 65 | 66 | **Formatting** 67 | 68 | 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). 69 | 70 | For a complete list of all available options, see the next section [Options](#options). 71 | 72 | ## Options 73 | 74 | All options are explained in the CLI help. View it with `git-graph -h`: 75 | 76 | ``` 77 | Structured Git graphs for your branching model. 78 | https://github.com/mlange-42/git-graph 79 | 80 | EXAMPES: 81 | git-graph -> Show graph 82 | git-graph --style round -> Show graph in a different style 83 | git-graph --model -> Show graph using a certain 84 | git-graph model --list -> List available branching models 85 | git-graph model -> Show repo's current branching models 86 | git-graph model -> Permanently set model for this repo 87 | 88 | USAGE: 89 | git-graph [FLAGS] [OPTIONS] [SUBCOMMAND] 90 | 91 | FLAGS: 92 | -d, --debug Additional debug output and graphics. 93 | -h, --help Prints help information 94 | -l, --local Show only local branches, no remotes. 95 | --no-color Print without colors. Missing color support should be detected 96 | automatically (e.g. when piping to a file). 97 | Overrides option '--color' 98 | --no-pager Use no pager (print everything at once without prompt). 99 | -S, --sparse Print a less compact graph: merge lines point to target lines 100 | rather than merge commits. 101 | --svg Render graph as SVG instead of text-based. 102 | -V, --version Prints version information 103 | 104 | OPTIONS: 105 | --color Specify when colors should be used. One of [auto|always|never]. 106 | Default: auto. 107 | -f, --format Commit format. One of [oneline|short|medium|full|""]. 108 | (First character can be used as abbreviation, e.g. '-f m') 109 | Default: oneline. 110 | For placeholders supported in "", consult 'git-graph --help' 111 | -n, --max-count Maximum number of commits 112 | -m, --model Branching model. Available presets are [simple|git-flow|none]. 113 | Default: git-flow. 114 | Permanently set the model for a repository with 115 | > git-graph model 116 | -p, --path Open repository from this path or above. Default '.' 117 | -s, --style