diff --git a/CHANGELOG.md b/CHANGELOG.md index 0052b63..523d2fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +### Added + +- track::Builder for incrementally adding commits. +- TrackMap::new() constructor + ### Changed - (BREAKING) Builder::with_settings now take Rc as argument. @@ -26,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Support for octo-merges (more than 2 parents) + Fix git relative worktrees extension, by updating dependencies + to include libgit2 v1.9.4 ## [0.7.3] - 2026-05-01 diff --git a/Cargo.lock b/Cargo.lock index 67715a5..2c1e74f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,27 +33,27 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "cc" -version = "1.2.60" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", @@ -69,9 +69,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -146,9 +146,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -166,9 +166,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "equivalent" @@ -201,6 +201,30 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -236,6 +260,7 @@ dependencies = [ "git2", "itertools", "lazy_static", + "log", "regex", "serde", "serde_derive", @@ -246,9 +271,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hermit-abi" @@ -378,9 +403,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -417,11 +442,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -433,15 +459,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libgit2-sys" -version = "0.18.3+1.9.2" +version = "0.18.5+1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" dependencies = [ "cc", "libc", @@ -451,9 +477,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.28" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "libc", @@ -490,21 +516,21 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -556,6 +582,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkg-config" version = "0.3.33" @@ -606,9 +638,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -629,9 +661,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rustc_version" @@ -713,9 +745,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -748,6 +780,12 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -831,7 +869,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.1", + "winnow 1.0.3", ] [[package]] @@ -848,9 +886,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -890,18 +928,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" dependencies = [ "cfg-if", "once_cell", @@ -912,9 +950,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -922,9 +960,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" dependencies = [ "bumpalo", "proc-macro2", @@ -935,9 +973,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" dependencies = [ "unicode-ident", ] @@ -1040,15 +1078,15 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" @@ -1064,9 +1102,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -1087,9 +1125,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] diff --git a/Cargo.toml b/Cargo.toml index 3541c21..34c8586 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,3 +34,4 @@ atty = "0.2" crossterm = {version = "0.29", optional = false} chrono = {version = "0.4", optional = false} textwrap = {version = "0.16", default-features = false, optional = false, features = ["unicode-width"]} +log = "0.4" diff --git a/src/backend/git2.rs b/src/backend/git2.rs index e0adb33..fefa30b 100644 --- a/src/backend/git2.rs +++ b/src/backend/git2.rs @@ -10,15 +10,17 @@ use git2::Oid; use git2::Repository; use crate::settings::Settings; -use crate::track::parse_merge_summary; +use crate::track::create_merge_branches; use crate::track::Binx; use crate::track::BranchInfo as AbstractBranchInfo; use crate::track::BranchInfoType; +use crate::track::Builder as AbstractBuilder; use crate::track::CommitInfo as AbstractCommitInfo; use crate::track::TrackMap as AbstractTrackMap; use crate::track::FORK; pub type BranchInfo = AbstractBranchInfo; +pub type Builder = AbstractBuilder; pub type CommitInfo = AbstractCommitInfo; pub type TrackMap = AbstractTrackMap; @@ -166,6 +168,8 @@ pub fn assign_branches( // New policy: Do not keep any branch that has no commits assigned. // When a commit has multiple labels (branches and/or tags) // you must use module label.rs instead. + // TODO Keep at most one branch per commit, now that labels can + // deal with multi-branch per commit. if any_assigned { branch_idx += 1; Some(Binx::new(branch_idx - 1)) @@ -349,40 +353,19 @@ fn extract_merge_branches( if info.parents.len() < 2 { continue; } - let child_oid = info.oid; + let child_oid = &info.oid; let commit = repository - .find_commit(child_oid) + .find_commit(*child_oid) .map_err(|err| err.message().to_string())?; - // Parse the branch names from the merge summary using configured patterns. - // use get(parent_index - 1) because the primary parent is NOT in this list - let par_branch_names = commit - .summary() - .map(|summary| parse_merge_summary(summary, &settings.merge_patterns)) - .unwrap_or(vec![]); - - // Iterate over branches merged into this branch (Skip primary parent) - for parent_index in 1..info.parents.len() { - let parent_oid = info.parents[parent_index]; - let par_branch_name = par_branch_names - .get(parent_index - 1) - .unwrap_or(&"unknown".to_string()) - .clone(); - - // Determine persistence and order for the derived branch. - let persistence = branch_order(&par_branch_name, &settings.branches.persistence) as u8; - - // Create and add the BranchInfo for the derived merge branch. - let branch_info = BranchInfo::new( - parent_oid, // Target is the parent of the merge. - Some(child_oid), // The merge commit itself. - par_branch_name, - persistence, - BranchInfoType::Derived, // This is a derived merge branch. - Some(idx + 1), // End index typically points to the commit after the merge. - ); - merge_branches.push(branch_info); - } + merge_branches.extend(create_merge_branches( + &settings.merge_patterns, + &settings.branches.persistence, + child_oid, + commit.summary().unwrap_or(""), + &info.parents, + idx + 1, // End index typically points to the commit + )); } Ok(merge_branches) } diff --git a/src/track.rs b/src/track.rs index e63d89b..cdc8daf 100644 --- a/src/track.rs +++ b/src/track.rs @@ -4,15 +4,20 @@ It is expensive to compute because changes in one end may affect the other end. Fortunately it can be computed incrementally. */ +use std::cell::RefCell; use std::collections::HashMap; +use std::hash::Hash; +use std::rc::Rc; use regex::Regex; use crate::define_u32_index; -use crate::settings::MergePatterns; +pub use crate::settings::MergePatterns; const ORIGIN: &str = "origin/"; pub const FORK: &str = "fork/"; +const MAX_PERSISTENCE: u8 = 255; +const AUTO_PERSISTENCE: u8 = 254; define_u32_index!( /** Index into [TrackMap].all_branches. @@ -24,6 +29,9 @@ define_u32_index!( pub struct Binx; ); +/// Index into [TrackMap].commits +pub type Cinx = usize; + /** Group commits into tracks. A track is a sequence of commits where every commit has a parent inside the track, except the oldest @@ -40,6 +48,51 @@ where /// All detected branches and tags, including merged and deleted pub all_branches: Vec>, } +impl Default for TrackMap { + fn default() -> Self { + Self::new() + } +} +impl TrackMap { + /// Create an empty TrackMap + pub fn new() -> Self { + Self { + commits: vec![], + indices: HashMap::new(), + all_branches: vec![], + } + } + /// Append a commit and return the index it got. + /// Does not deal with branch information. + pub fn add_commit(&mut self, commit: CommitInfo) -> usize { + let commit_index = self.commits.len(); + self.indices.insert(commit.oid.clone(), commit_index); + self.commits.push(commit); + // Make sure every child has a branch + commit_index + } + /// Get at reference to the commit with this Oid + pub fn commit_at(&self, commit_id: &Oid) -> Option<&CommitInfo> { + self.indices + .get(commit_id) + .map(|&commit_index| &self.commits[commit_index]) + } + /// Get a reference to the branch associated with the commit + pub fn branch_at(&self, commit_id: &Oid) -> Option<&BranchInfo> { + self.commit_at(commit_id) + .and_then(|commit_info| commit_info.branch_trace) + .map(|branch_index| &self.all_branches[branch_index]) + } + /// Push a branch onto tracks.all_branches and return its index. + /// + /// *Note*: You must update the target commit to point + /// at this branch. + fn store_branch(&mut self, branch: BranchInfo) -> Binx { + let branch_id = self.all_branches.len(); + self.all_branches.push(branch); + Binx::new(branch_id) + } +} /// Gleisbau branch come in several variants, that indicate their origin #[derive(PartialEq)] @@ -104,7 +157,9 @@ impl BranchInfo { pub struct CommitInfo { /// Commit object identifier from git2 pub oid: Oid, - /// Parents of commit. Filled in first pass + /// Parents of commit. Filled in first pass. + /// + /// *Note*: May point to commits not yet in TrackMap pub parents: Vec, /// Children of commit. Filled in second pass pub children: Vec, @@ -118,10 +173,404 @@ impl CommitInfo { } } +/** Data about a missing parent. + + Mising parents occur when we walk towards the boundary of what is known. + + - [add_child](MissingParent::add_child) + Store relations to parents not yet seen. + - [set_branch](MissingParent::set_branch) + Store candidate new branch for a not yet seen commit. + +*/ +struct MissingParent { + known_children: Vec, + candidate_branch: Option>, +} +impl Default for MissingParent { + fn default() -> Self { + Self::new() + } +} +impl MissingParent { + pub fn new() -> Self { + Self { + known_children: vec![], + candidate_branch: None, + } + } + /// Store relation to missing parent from a known child. The child should + /// have the missing parent as primary parent. + /// + /// Later, when calling [Self::set_commit_branch], this child is one of the + /// candidates for extending its branch to include the parent. + pub fn add_child(&mut self, child_inx: Cinx) { + self.known_children.push(child_inx); + } + /// Set the candidate branch to create here. Does nothing if one already + /// is set and it has at least as good persistence as the new one. + /// + /// This should be called if a commit has merged the missing parent (the + /// parent is not the primary parent), or if the user has decided a branch + /// should start here. Remember to set the branch merge_target to indicate if + /// the child has merged the parent. + pub fn set_branch(&mut self, branch: BranchInfo) { + let old_branch = &self + .candidate_branch + .as_ref() + // Keep old candidate if it has lower pers order = more persistene + .filter(|br| br.persistence <= branch.persistence); + if old_branch.is_none() { + self.candidate_branch = Some(branch); + } + } + /// Set branch on no-longer-missing parent commit. + /// It considers the current candidate branch and the branches of + /// all known children. If candidate branch is used, a new branch starts. + /// If a child branch is used, it is extended. + pub fn set_commit_branch(self, tracks: &mut TrackMap, commit_inx: Cinx) { + // + // Find the most persistent branch that wants this commit + // + + // Persistence of the best branch so far + let mut best_pers = self + .candidate_branch + .as_ref() + .map(|br| br.persistence) + .unwrap_or(MAX_PERSISTENCE); // the least possible persistent + // Child to extend, or None if the candidate is best + let mut best_extend = None; + + // Check if any child branch is more persistent + for child_inx in self.known_children { + let child = &mut tracks.commits[child_inx]; + if child.branch_trace.is_none() { + panic!("Known child does not yet have a branch"); + } + let better_child_branch = child + .branch_trace + .map(|branch_index| &tracks.all_branches[branch_index]) + .filter(|br| br.persistence < best_pers); + if let Some(br) = better_child_branch { + best_pers = br.persistence; + best_extend = Some(child_inx); + } + } + + // + // Set commit branch + // + + let branch_trace; + if let Some(best_child_inx) = best_extend { + // Extend the best child + let child = &mut tracks.commits[best_child_inx]; + let branch_index = child + .branch_trace + .expect("All commits must have a branch - including child of unknown parent"); + let child_branch = &mut tracks.all_branches[branch_index]; + child_branch.range.1 = Some(commit_inx); + branch_trace = Some(branch_index) + } else if let Some(branch_info) = self.candidate_branch { + // Create candidate branch + let branch_index = tracks.store_branch(branch_info); + branch_trace = Some(branch_index); + } else { + // No candidates, so we must be at a head. + // Create a new track/branch starting at this commit + + let target = tracks.commits[commit_inx].oid.clone(); + let next_branch_index = tracks.all_branches.len(); + let name = format!("anonhead-{}", next_branch_index); + let persistence = AUTO_PERSISTENCE; + let end_index = Some(commit_inx); + + let new_branch = BranchInfo::new( + target, // Target is the commit to add. + None, // no merge target + name, + persistence, + BranchInfoType::Local, // Default value + end_index, + ); + + // Add new branch and use it for the commit to add + let branch_index = tracks.store_branch(new_branch); + branch_trace = Some(branch_index); + } + + // Update commit + tracks.commits[commit_inx].branch_trace = branch_trace; + } +} + +pub type PersistencePatterns = Vec; + +/** + Add repository commit information to a [TrackMap]. + + This struct is ment to be temporary. Once the repository has been walked + it can be discarded. It holds information about commit ids that have not + yet been processed. Once they are available it will update the previously + seen commits in TrackMap with the missing relations. + + The curent implementation assumes that commits are only added + during a full repository walk from newest to oldest commit. + This means that children are always added before their parents. + This assumption reduces the amount of memory needed for building. +*/ +pub struct Builder { + /// Track structure to update + tracks: Rc>>, + + /// Merge patterns to use when deriving a branch name from a merge + merge_patterns: Rc, + + /// Branch persistence from branch name pattern + persistence: PersistencePatterns, + + /// For each missing parent, store information + /// that should be added when it is found. + missing_parents: HashMap>, +} +impl Builder { + /// Create a builder for the specified TrackMap + pub fn new(target: Rc>>) -> Self { + Self { + tracks: target.clone(), + merge_patterns: MergePatterns::default().into(), + persistence: vec![], + missing_parents: HashMap::new(), + } + } + + /// Set the regex patterns that are used to derive + /// branch names from merge commit message. + pub fn with_merge_patterns(mut self, merge_patterns: MergePatterns) -> Self { + self.merge_patterns = merge_patterns.into(); + self + } + /// Set a sequence of branch name regex that determine the + /// persistence order of branches. + pub fn with_persistence_patterns(mut self, persistence: PersistencePatterns) -> Self { + self.persistence = persistence; + self + } + + #[allow(clippy::doc_overindented_list_items)] + /// Add a commit to the TrackMap. + /// When a missing parent is added, create the missing relations. + /// + /// # Arguments + /// - id : Child Oid + /// - message: Child commit message. If child is a merge this may give + /// a name to a merged branch. + /// - parents: List of parent Oid. + pub fn add_commit(&mut self, id: Oid, message: String, parents: Vec) { + let merge_patterns = self.merge_patterns.clone(); + + // The add_commit function has the following steps: + // + // 1. Compute and register commit + // 1.1. Add CommitInfo to tracks + // 1.2. Compute branch for commit + // + // 2. Record parents of commit + // for each unknown parent, + // 2.1. if first parent - store as continuation + // 2.2. if merge-parent - store as derived branch + + // + // 1. The commit + // + + // 1.1. Add CommitInfo to tracks + + let ci = CommitInfo { + oid: id.clone(), + parents: parents.clone(), + // TODO why the child pointer?? Can it be completely removed? + children: vec![], + branch_trace: None, // We will set this in a moment + }; + self.tracks.borrow_mut().add_commit(ci); + + // 1.2. Compute branch_trace for commit + + let commit_index = *self + .tracks + .borrow() + .indices + .get(&id) + .expect("Commit must be known in TrackMap"); + self.missing_parents + .remove(&id) + .unwrap_or_default() + .set_commit_branch(&mut *self.tracks.borrow_mut(), commit_index); + + // + // 2. Parents + // + + // 2.1 Primary parent + + if !parents.is_empty() { + // Primary parent continues the commit at this commit + // Store that information in MissingParent so it can be + // used when the parent commit is processed. + let child_inx = *self + .tracks + .borrow() + .indices + .get(&id) + .expect("Commit must be known in TrackMap"); + let parent_oid = parents[0].clone(); + self.missing_parents + .entry(parent_oid) + .or_default() + .add_child(child_inx); + } + + // 2.2. Non-Primary parents + + // Run through all merged parents and add a derived branch + // if it is not already part of a branch. + // Normally every merged parent would start a branch. + // This does not happen if the merged parent has already + // been claimed by another branch. + let add_branch_if_missing = |child_oid: Oid, parent_oid: Oid, par_branch_name: String| { + // Determine persistence and order for the derived branch. + let par_persistence = branch_order(&par_branch_name, &self.persistence) as u8; + + // Abort if a branch is already here, with at least same persistence. + // Note: This never happens on first walk of TrackMap + if let Some(existing_branch) = self.tracks.borrow().branch_at(&parent_oid) { + if existing_branch.persistence <= par_persistence { + return; + } + } + + let Some(&end_index) = self.tracks.borrow().indices.get(&parent_oid) else { + // Parent is outside self.tracks, + // and we build TrackMap in one batch + // so it is safe to ignore. + // BUG Incrementally building TrackMap will + // fail if this is ignored. + // TODO Add to known child unknown parent + // and store branch info there as well + return; + }; + + // Create and remember the BranchInfo for the derived merge branch. + let new_branch = BranchInfo::new( + parent_oid.clone(), // Target is the parent of the merge. + Some(child_oid.clone()), // The merge commit itself. + par_branch_name, + par_persistence, + BranchInfoType::Derived, + Some(end_index), + ); + + // TODO This code assumes that we only do one walk to fill + // TrackMap. If run a second time, the commit is already there + // and MissingParent is probably not the right way to do things. + self.missing_parents + .entry(parent_oid) + .or_default() + .set_branch(new_branch); + }; + + // Derive merge branches from merged-in parents + // and store them for later usage. + // TODO find a better name for this function. + // Perhaps derive_merge_branches + // or process_merged_parents + handle_merge_branches( + &merge_patterns, + &id, // Child Oid + &message, + &parents, + add_branch_if_missing, + ); + } +} + // // Generic functions not tied to a particular struct // +pub fn create_merge_branches( + merge_patterns: &MergePatterns, + persistence_patterns: &[Regex], + child_oid: &Oid, + message: &str, + parents: &[Oid], + end_index: usize, +) -> Vec> { + let mut merge_branches = vec![]; + + // Parse the branch names from the merge summary using configured patterns. + // use get(parent_index - 1) because the primary parent is NOT in this list + let par_branch_names = parse_merge_summary(message, merge_patterns); + + // Iterate over branches merged into this branch (Skip primary parent) + #[allow(clippy::needless_range_loop)] + for parent_index in 1..parents.len() { + let parent_oid = parents[parent_index].clone(); + let par_branch_name = par_branch_names + .get(parent_index - 1) + .unwrap_or(&"unknown".to_string()) + .clone(); + + // Determine persistence and order for the derived branch. + let persistence = branch_order(&par_branch_name, persistence_patterns) as u8; + + // Create and add the BranchInfo for the derived merge branch. + let branch_info = BranchInfo::new( + parent_oid, // Branch target is the parent of the merge. + Some(child_oid.clone()), // The merge commit is the merge_target + par_branch_name, + persistence, + BranchInfoType::Derived, + Some(end_index), + ); + merge_branches.push(branch_info); + } + + merge_branches +} + +/// Visit all merged parents of a commit. +/// The visitor is provided the assumed branch name +/// derived from the merge message. +/// +/// [TrackMap] uses this to derive new branches that targets the merged commit. +pub fn handle_merge_branches( + merge_patterns: &MergePatterns, + child_oid: &Oid, + message: &str, + parents: &[Oid], + mut branch_visitor: impl FnMut(Oid, Oid, String), +) { + // Extract the branch names from the merge summary using configured patterns. + let par_branch_names = parse_merge_summary(message, merge_patterns); + + // Visit all branches merged into this branch. + // parent 0 is not visited because it is the target branch. + #[allow(clippy::needless_range_loop)] + for parent_index in 1..parents.len() { + let child_oid = child_oid.clone(); + let parent_oid = parents[parent_index].clone(); + let par_branch_name = par_branch_names + .get(parent_index - 1) + .unwrap_or(&"unknown".to_string()) + .clone(); + + branch_visitor(child_oid, parent_oid, par_branch_name); + } +} + /// Finds the index for a branch name from a slice of prefixes pub fn branch_order(name: &str, order: &[Regex]) -> usize { order