use crate::error::Result;
use crate::repo::JjWorkspace;
use crate::types::{Bookmark, BookmarkSegment, BranchStack, ChangeGraph, LogEntry};
use std::collections::{HashMap, HashSet};
use tracing::debug;
struct TraversalResult {
segments: Vec<RawSegment>,
already_seen_change_id: Option<String>,
excluded_bookmark_count: usize,
newly_tainted_change_ids: Vec<String>,
}
struct RawSegment {
bookmark_names: Vec<String>,
changes: Vec<LogEntry>,
}
#[allow(clippy::too_many_lines)]
pub fn build_change_graph(workspace: &JjWorkspace) -> Result<ChangeGraph> {
debug!("Discovering user bookmarks...");
let all_bookmarks = workspace.local_bookmarks()?;
debug!(
"Found {} bookmarks: {:?}",
all_bookmarks.len(),
all_bookmarks.iter().map(|b| &b.name).collect::<Vec<_>>()
);
let bookmarks_by_name: HashMap<String, Bookmark> = all_bookmarks
.iter()
.map(|b| (b.name.clone(), b.clone()))
.collect();
let mut fully_collected_bookmarks: HashSet<String> = HashSet::new();
let mut bookmark_to_change_id: HashMap<String, String> = HashMap::new();
let mut bookmarked_change_adjacency_list: HashMap<String, String> = HashMap::new();
let mut bookmarked_change_id_to_segment: HashMap<String, Vec<LogEntry>> = HashMap::new();
let mut stack_roots: HashSet<String> = HashSet::new();
let mut tainted_change_ids: HashSet<String> = HashSet::new();
let mut total_excluded_bookmark_count = 0;
for bookmark in &all_bookmarks {
if fully_collected_bookmarks.contains(&bookmark.name) {
debug!("Skipping already processed bookmark: {}", bookmark.name);
continue;
}
debug!("Processing bookmark: {}", bookmark.name);
let result = traverse_and_discover_segments(
workspace,
bookmark,
&fully_collected_bookmarks,
&tainted_change_ids,
)?;
if result.excluded_bookmark_count > 0 {
tainted_change_ids.extend(result.newly_tainted_change_ids);
total_excluded_bookmark_count += result.excluded_bookmark_count;
debug!(
" Excluded {} due to merge commit in history",
bookmark.name
);
continue;
}
for segment in &result.segments {
if segment.changes.is_empty() {
continue;
}
let first_change_id = segment.changes[0].change_id.clone();
bookmarked_change_id_to_segment
.insert(first_change_id.clone(), segment.changes.clone());
for bm_name in &segment.bookmark_names {
bookmark_to_change_id.insert(bm_name.clone(), first_change_id.clone());
fully_collected_bookmarks.insert(bm_name.clone());
}
debug!(
" Found segment for [{}]: {} changes",
segment.bookmark_names.join(", "),
segment.changes.len()
);
}
for i in 0..result.segments.len().saturating_sub(1) {
let child_segment = &result.segments[i];
let parent_segment = &result.segments[i + 1];
if child_segment.changes.is_empty() || parent_segment.changes.is_empty() {
continue;
}
let child_id = child_segment.changes[0].change_id.clone();
let parent_id = parent_segment.changes[0].change_id.clone();
bookmarked_change_adjacency_list.insert(child_id.clone(), parent_id.clone());
debug!(
" Stacking: [{}] -> [{}]",
child_segment.bookmark_names.join(", "),
parent_segment.bookmark_names.join(", ")
);
}
if let Some(ref already_seen_id) = result.already_seen_change_id {
if let Some(root_segment) = result.segments.last() {
if !root_segment.changes.is_empty() {
let root_id = root_segment.changes[0].change_id.clone();
bookmarked_change_adjacency_list.insert(root_id, already_seen_id.clone());
}
}
} else if let Some(root_segment) = result.segments.last() {
if !root_segment.changes.is_empty() {
let root_id = root_segment.changes[0].change_id.clone();
stack_roots.insert(root_id);
for bm_name in &root_segment.bookmark_names {
debug!(" Root bookmark identified: {}", bm_name);
}
}
}
debug!(
" Processed {} - found {} segments",
bookmark.name,
result.segments.len()
);
}
let change_ids_with_children: HashSet<String> =
bookmarked_change_adjacency_list.values().cloned().collect();
let stack_leafs: HashSet<String> = bookmarked_change_id_to_segment
.keys()
.filter(|id| !change_ids_with_children.contains(*id))
.cloned()
.collect();
let stacks = group_segments_into_stacks(
&bookmarks_by_name,
&stack_leafs,
&bookmarked_change_adjacency_list,
&bookmarked_change_id_to_segment,
);
Ok(ChangeGraph {
bookmarks: bookmarks_by_name,
bookmark_to_change_id,
bookmarked_change_adjacency_list,
bookmarked_change_id_to_segment,
stack_leafs,
stack_roots,
stacks,
excluded_bookmark_count: total_excluded_bookmark_count,
})
}
fn traverse_and_discover_segments(
workspace: &JjWorkspace,
bookmark: &Bookmark,
fully_collected_bookmarks: &HashSet<String>,
tainted_change_ids: &HashSet<String>,
) -> Result<TraversalResult> {
let mut segments: Vec<RawSegment> = Vec::new();
let mut current_segment: Option<RawSegment> = None;
let mut already_seen_change_id: Option<String> = None;
let mut seen_change_ids: Vec<String> = Vec::new();
let revset = format!("trunk()..{}", bookmark.commit_id);
let changes = workspace.resolve_revset(&revset)?;
for change in &changes {
seen_change_ids.push(change.change_id.clone());
if change.parents.len() > 1 || tainted_change_ids.contains(&change.change_id) {
debug!(
"Found {} in bookmark {} - excluding bookmark and descendants",
if change.parents.len() > 1 {
"merge commit"
} else {
"tainted change"
},
bookmark.name
);
return Ok(TraversalResult {
segments: Vec::new(),
already_seen_change_id: None,
excluded_bookmark_count: 1,
newly_tainted_change_ids: seen_change_ids,
});
}
}
for change in &changes {
if !change.local_bookmarks.is_empty() {
if let Some(seg) = current_segment.take() {
segments.push(seg);
}
if change
.local_bookmarks
.iter()
.any(|b| fully_collected_bookmarks.contains(b))
{
debug!(" Found fully-collected bookmark at {}", change.commit_id);
already_seen_change_id = Some(change.change_id.clone());
break;
}
current_segment = Some(RawSegment {
bookmark_names: change.local_bookmarks.clone(),
changes: Vec::new(),
});
debug!(
" Starting new segment for bookmarks: {} at commit {}",
change.local_bookmarks.join(", "),
change.commit_id
);
}
if let Some(ref mut seg) = current_segment {
seg.changes.push(change.clone());
}
}
if let Some(seg) = current_segment {
segments.push(seg);
}
Ok(TraversalResult {
segments,
already_seen_change_id,
excluded_bookmark_count: 0,
newly_tainted_change_ids: Vec::new(),
})
}
fn group_segments_into_stacks(
bookmarks: &HashMap<String, Bookmark>,
stack_leafs: &HashSet<String>,
adjacency_list: &HashMap<String, String>,
change_id_to_segment: &HashMap<String, Vec<LogEntry>>,
) -> Vec<BranchStack> {
let mut stacks = Vec::new();
for leaf_change_id in stack_leafs {
let stack_change_ids = build_path_to_root(leaf_change_id, adjacency_list);
let segments = build_segments(&stack_change_ids, bookmarks, change_id_to_segment);
stacks.push(BranchStack { segments });
}
stacks
}
fn build_path_to_root(
leaf_change_id: &str,
adjacency_list: &HashMap<String, String>,
) -> Vec<String> {
let mut path = vec![leaf_change_id.to_string()];
let mut current = leaf_change_id.to_string();
while let Some(parent) = adjacency_list.get(¤t) {
path.push(parent.clone());
current = parent.clone();
}
path.reverse();
path
}
fn build_segments(
stack_change_ids: &[String],
bookmarks: &HashMap<String, Bookmark>,
change_id_to_segment: &HashMap<String, Vec<LogEntry>>,
) -> Vec<BookmarkSegment> {
let mut segments = Vec::new();
for change_id in stack_change_ids {
if let Some(changes) = change_id_to_segment.get(change_id) {
if changes.is_empty() {
continue;
}
let bookmark_list: Vec<Bookmark> = changes[0]
.local_bookmarks
.iter()
.filter_map(|name| bookmarks.get(name).cloned())
.collect();
segments.push(BookmarkSegment {
bookmarks: bookmark_list,
changes: changes.clone(),
});
}
}
segments
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_path_to_root_single() {
let adjacency: HashMap<String, String> = HashMap::new();
let path = build_path_to_root("leaf", &adjacency);
assert_eq!(path, vec!["leaf"]);
}
#[test]
fn test_build_path_to_root_chain() {
let mut adjacency: HashMap<String, String> = HashMap::new();
adjacency.insert("c".to_string(), "b".to_string());
adjacency.insert("b".to_string(), "a".to_string());
let path = build_path_to_root("c", &adjacency);
assert_eq!(path, vec!["a", "b", "c"]);
}
#[test]
fn test_build_segments_empty() {
let bookmarks: HashMap<String, Bookmark> = HashMap::new();
let change_id_to_segment: HashMap<String, Vec<LogEntry>> = HashMap::new();
let segments = build_segments(&["id1".to_string()], &bookmarks, &change_id_to_segment);
assert!(segments.is_empty());
}
}