git_graph/
settings.rs

1//! Graph generation settings.
2//!
3//! The settings control how a branching graph is layed out.
4//! They are used in the [print][super::print] module when generating
5//! a visualization and persisted to disk in the [config][super::config] module.
6//!
7//! These are the main structs
8//! * [Settings] The main settings object, which contains:
9//!   * [CommitFormat] Format of the commit summary text to the right of the graph.
10//!   * [Characters] The symbols to use when rendering a graph as text.
11//!   * [BranchSettings] Control how a graph is formatted.
12//!   * [BranchOrder] Determines the left-to-right order of branches.
13//!   * [MergePatterns] Regex that extract branch names from a merge commit.
14
15use crate::print::format::CommitFormat;
16use regex::{Error, Regex};
17use serde_derive::{Deserialize, Serialize};
18use std::str::FromStr;
19
20/// Repository settings for the branching model.
21/// Used to read repo's git-graph.toml
22#[derive(Serialize, Deserialize)]
23pub struct RepoSettings {
24    /// The repository's branching model
25    pub model: String,
26}
27
28/// Ordering policy for branches in visual columns.
29pub enum BranchOrder {
30    /// Recommended! Shortest branches are inserted left-most.
31    ///
32    /// For branches with equal length, branches ending last are inserted first.
33    /// Reverse (arg = false): Branches ending first are inserted first.
34    ShortestFirst(bool),
35    /// Longest branches are inserted left-most.
36    ///
37    /// For branches with equal length, branches ending last are inserted first.
38    /// Reverse (arg = false): Branches ending first are inserted first.
39    LongestFirst(bool),
40}
41
42/// Top-level settings
43pub struct Settings {
44    /// Reverse the order of commits
45    pub reverse_commit_order: bool,
46    /// Debug printing and drawing
47    pub debug: bool,
48    /// Compact text-based graph
49    pub compact: bool,
50    /// Colored text-based graph
51    pub colored: bool,
52    /// Include remote branches?
53    pub include_remote: bool,
54    /// Formatting for commits
55    pub format: CommitFormat,
56    /// Text wrapping options
57    pub wrapping: Option<(Option<usize>, Option<usize>, Option<usize>)>,
58    /// Characters to use for text-based graph
59    pub characters: Characters,
60    /// Branch column sorting algorithm
61    pub branch_order: BranchOrder,
62    /// Settings for branches
63    pub branches: BranchSettings,
64    /// Regex patterns for finding branch names in merge commit summaries
65    pub merge_patterns: MergePatterns,
66}
67
68/// Helper for reading BranchSettings, required due to RegEx.
69#[derive(Serialize, Deserialize)]
70pub struct BranchSettingsDef {
71    /// Branch persistence
72    pub persistence: Vec<String>,
73    /// Branch ordering
74    pub order: Vec<String>,
75    /// Branch colors
76    pub terminal_colors: ColorsDef,
77    /// Branch colors for SVG output
78    pub svg_colors: ColorsDef,
79}
80
81/// Helper for reading branch colors, required due to RegEx.
82#[derive(Serialize, Deserialize)]
83pub struct ColorsDef {
84    matches: Vec<(String, Vec<String>)>,
85    unknown: Vec<String>,
86}
87
88impl BranchSettingsDef {
89    /// The Git-Flow model.
90    pub fn git_flow() -> Self {
91        BranchSettingsDef {
92            persistence: vec![
93                r"^(master|main|trunk)$".to_string(),
94                r"^(develop|dev)$".to_string(),
95                r"^feature.*$".to_string(),
96                r"^release.*$".to_string(),
97                r"^hotfix.*$".to_string(),
98                r"^bugfix.*$".to_string(),
99            ],
100            order: vec![
101                r"^(master|main|trunk)$".to_string(),
102                r"^(hotfix|release).*$".to_string(),
103                r"^(develop|dev)$".to_string(),
104            ],
105            terminal_colors: ColorsDef {
106                matches: vec![
107                    (
108                        r"^(master|main|trunk)$".to_string(),
109                        vec!["bright_blue".to_string()],
110                    ),
111                    (
112                        r"^(develop|dev)$".to_string(),
113                        vec!["bright_yellow".to_string()],
114                    ),
115                    (
116                        r"^(feature|fork/).*$".to_string(),
117                        vec!["bright_magenta".to_string(), "bright_cyan".to_string()],
118                    ),
119                    (r"^release.*$".to_string(), vec!["bright_green".to_string()]),
120                    (
121                        r"^(bugfix|hotfix).*$".to_string(),
122                        vec!["bright_red".to_string()],
123                    ),
124                    (r"^tags/.*$".to_string(), vec!["bright_green".to_string()]),
125                ],
126                unknown: vec!["white".to_string()],
127            },
128
129            svg_colors: ColorsDef {
130                matches: vec![
131                    (
132                        r"^(master|main|trunk)$".to_string(),
133                        vec!["blue".to_string()],
134                    ),
135                    (r"^(develop|dev)$".to_string(), vec!["orange".to_string()]),
136                    (
137                        r"^(feature|fork/).*$".to_string(),
138                        vec!["purple".to_string(), "turquoise".to_string()],
139                    ),
140                    (r"^release.*$".to_string(), vec!["green".to_string()]),
141                    (r"^(bugfix|hotfix).*$".to_string(), vec!["red".to_string()]),
142                    (r"^tags/.*$".to_string(), vec!["green".to_string()]),
143                ],
144                unknown: vec!["gray".to_string()],
145            },
146        }
147    }
148
149    /// Simple feature-based model.
150    pub fn simple() -> Self {
151        BranchSettingsDef {
152            persistence: vec![r"^(master|main|trunk)$".to_string()],
153            order: vec![
154                r"^tags/.*$".to_string(),
155                r"^(master|main|trunk)$".to_string(),
156            ],
157            terminal_colors: ColorsDef {
158                matches: vec![
159                    (
160                        r"^(master|main|trunk)$".to_string(),
161                        vec!["bright_blue".to_string()],
162                    ),
163                    (r"^tags/.*$".to_string(), vec!["bright_green".to_string()]),
164                ],
165                unknown: vec![
166                    "bright_yellow".to_string(),
167                    "bright_green".to_string(),
168                    "bright_red".to_string(),
169                    "bright_magenta".to_string(),
170                    "bright_cyan".to_string(),
171                ],
172            },
173
174            svg_colors: ColorsDef {
175                matches: vec![
176                    (
177                        r"^(master|main|trunk)$".to_string(),
178                        vec!["blue".to_string()],
179                    ),
180                    (r"^tags/.*$".to_string(), vec!["green".to_string()]),
181                ],
182                unknown: vec![
183                    "orange".to_string(),
184                    "green".to_string(),
185                    "red".to_string(),
186                    "purple".to_string(),
187                    "turquoise".to_string(),
188                ],
189            },
190        }
191    }
192
193    /// Very simple model without any defined branch roles.
194    pub fn none() -> Self {
195        BranchSettingsDef {
196            persistence: vec![],
197            order: vec![],
198            terminal_colors: ColorsDef {
199                matches: vec![],
200                unknown: vec![
201                    "bright_blue".to_string(),
202                    "bright_yellow".to_string(),
203                    "bright_green".to_string(),
204                    "bright_red".to_string(),
205                    "bright_magenta".to_string(),
206                    "bright_cyan".to_string(),
207                ],
208            },
209
210            svg_colors: ColorsDef {
211                matches: vec![],
212                unknown: vec![
213                    "blue".to_string(),
214                    "orange".to_string(),
215                    "green".to_string(),
216                    "red".to_string(),
217                    "purple".to_string(),
218                    "turquoise".to_string(),
219                ],
220            },
221        }
222    }
223}
224
225/// Settings defining branching models
226pub struct BranchSettings {
227    /// Branch persistence
228    pub persistence: Vec<Regex>,
229    /// Branch ordering
230    pub order: Vec<Regex>,
231    /// Branch colors
232    pub terminal_colors: Vec<(Regex, Vec<String>)>,
233    /// Colors for branches not matching any of `colors`
234    pub terminal_colors_unknown: Vec<String>,
235    /// Branch colors for SVG output
236    pub svg_colors: Vec<(Regex, Vec<String>)>,
237    /// Colors for branches not matching any of `colors` for SVG output
238    pub svg_colors_unknown: Vec<String>,
239}
240
241impl BranchSettings {
242    pub fn from(def: BranchSettingsDef) -> Result<Self, Error> {
243        let persistence = def
244            .persistence
245            .iter()
246            .map(|str| Regex::new(str))
247            .collect::<Result<Vec<_>, Error>>()?;
248
249        let order = def
250            .order
251            .iter()
252            .map(|str| Regex::new(str))
253            .collect::<Result<Vec<_>, Error>>()?;
254
255        let terminal_colors = def
256            .terminal_colors
257            .matches
258            .into_iter()
259            .map(|(str, vec)| Regex::new(&str).map(|re| (re, vec)))
260            .collect::<Result<Vec<_>, Error>>()?;
261
262        let terminal_colors_unknown = def.terminal_colors.unknown;
263
264        let svg_colors = def
265            .svg_colors
266            .matches
267            .into_iter()
268            .map(|(str, vec)| Regex::new(&str).map(|re| (re, vec)))
269            .collect::<Result<Vec<_>, Error>>()?;
270
271        let svg_colors_unknown = def.svg_colors.unknown;
272
273        Ok(BranchSettings {
274            persistence,
275            order,
276            terminal_colors,
277            terminal_colors_unknown,
278            svg_colors,
279            svg_colors_unknown,
280        })
281    }
282}
283
284/// RegEx patterns for extracting branch names from merge commit summaries.
285pub struct MergePatterns {
286    /// The patterns. Evaluated in the given order.
287    pub patterns: Vec<Regex>,
288}
289
290impl Default for MergePatterns {
291    fn default() -> Self {
292        MergePatterns {
293            patterns: vec![
294                // GitLab pull request
295                Regex::new(r"^Merge branch '(.+)' into '.+'$").unwrap(),
296                // Git default
297                Regex::new(r"^Merge branch '(.+)' into .+$").unwrap(),
298                // Git default into main branch
299                Regex::new(r"^Merge branch '(.+)'$").unwrap(),
300                // GitHub pull request
301                Regex::new(r"^Merge pull request #[0-9]+ from .[^/]+/(.+)$").unwrap(),
302                // GitHub pull request (from fork?)
303                Regex::new(r"^Merge branch '(.+)' of .+$").unwrap(),
304                // BitBucket pull request
305                Regex::new(r"^Merged in (.+) \(pull request #[0-9]+\)$").unwrap(),
306            ],
307        }
308    }
309}
310
311/// The characters used for drawing text-based graphs.
312pub struct Characters {
313    pub chars: Vec<char>,
314}
315
316impl FromStr for Characters {
317    type Err = String;
318
319    fn from_str(str: &str) -> Result<Self, Self::Err> {
320        match str {
321            "normal" | "thin" | "n" | "t" => Ok(Characters::thin()),
322            "round" | "r" => Ok(Characters::round()),
323            "bold" | "b" => Ok(Characters::bold()),
324            "double" | "d" => Ok(Characters::double()),
325            "ascii" | "a" => Ok(Characters::ascii()),
326            _ => Err(format!("Unknown characters/style '{}'. Must be one of [normal|thin|round|bold|double|ascii]", str)),
327        }
328    }
329}
330
331impl Characters {
332    /// Default/thin graphs
333    pub fn thin() -> Self {
334        Characters {
335            chars: " ●○│─┼└┌┐┘┤├┴┬<>".chars().collect(),
336        }
337    }
338    /// Graphs with rounded corners
339    pub fn round() -> Self {
340        Characters {
341            chars: " ●○│─┼╰╭╮╯┤├┴┬<>".chars().collect(),
342        }
343    }
344    /// Bold/fat graphs
345    pub fn bold() -> Self {
346        Characters {
347            chars: " ●○┃━╋┗┏┓┛┫┣┻┳<>".chars().collect(),
348        }
349    }
350    /// Double-lined graphs
351    pub fn double() -> Self {
352        Characters {
353            chars: " ●○║═╬╚╔╗╝╣╠╩╦<>".chars().collect(),
354        }
355    }
356    /// ASCII-only graphs
357    pub fn ascii() -> Self {
358        Characters {
359            chars: " *o|-+'..'||++<>".chars().collect(),
360        }
361    }
362
363    pub fn reverse(self) -> Self {
364        let mut chars = self.chars;
365
366        chars.swap(6, 8);
367        chars.swap(7, 9);
368        chars.swap(10, 11);
369        chars.swap(12, 13);
370        chars.swap(14, 15);
371
372        Characters { chars }
373    }
374}