blame_rs/
blame.rs

1use crate::types::{
2    BlameError, BlameLine, BlameOptions, BlameResult, BlameRevision, DiffAlgorithm,
3};
4use similar::{Algorithm, ChangeTag, TextDiff};
5use std::rc::Rc;
6
7#[derive(Debug)]
8struct LineOrigin<'a, T> {
9    content: &'a str,
10    metadata: Rc<T>,
11}
12
13impl<'a, T> Clone for LineOrigin<'a, T> {
14    fn clone(&self) -> Self {
15        Self {
16            content: self.content,
17            metadata: Rc::clone(&self.metadata),
18        }
19    }
20}
21
22/// Performs a blame operation on a sequence of revisions to determine the origin of each line.
23///
24/// This function takes a slice of `BlameRevision` objects ordered chronologically (oldest to newest)
25/// and computes which revision each line in the final version originated from.
26///
27/// # Arguments
28///
29/// * `revisions` - A slice of revisions ordered chronologically (oldest first, newest last)
30///
31/// # Returns
32///
33/// Returns a `BlameResult` containing each line of the final revision along with metadata
34/// about which revision introduced that line.
35///
36/// # Errors
37///
38/// Returns `BlameError::EmptyRevisions` if the revisions slice is empty.
39///
40/// # Example
41///
42/// ```ignore
43/// use blame_rs::{blame, BlameRevision};
44/// use std::rc::Rc;
45///
46/// #[derive(Debug)]
47/// struct CommitInfo {
48///     hash: String,
49///     author: String,
50/// }
51///
52/// let revisions = vec![
53///     BlameRevision {
54///         content: "line 1\nline 2",
55///         metadata: Rc::new(CommitInfo { hash: "abc123".into(), author: "Alice".into() }),
56///     },
57///     BlameRevision {
58///         content: "line 1\nline 2\nline 3",
59///         metadata: Rc::new(CommitInfo { hash: "def456".into(), author: "Bob".into() }),
60///     },
61/// ];
62///
63/// let result = blame(&revisions)?;
64/// ```
65pub fn blame<'a, T>(revisions: &'a [BlameRevision<'a, T>]) -> Result<BlameResult<'a, T>, BlameError> {
66    blame_with_options(revisions, BlameOptions::default())
67}
68
69/// Performs a blame operation with custom options.
70///
71/// # Arguments
72///
73/// * `revisions` - A slice of revisions ordered chronologically (oldest first, newest last)
74/// * `options` - Configuration options for the blame operation
75///
76/// # Returns
77///
78/// Returns a `BlameResult` containing each line of the final revision along with metadata
79/// about which revision introduced that line.
80///
81/// # Errors
82///
83/// Returns `BlameError::EmptyRevisions` if the revisions slice is empty.
84///
85/// # Example
86///
87/// ```ignore
88/// use blame_rs::{blame_with_options, BlameOptions, BlameRevision, DiffAlgorithm};
89///
90/// let options = BlameOptions {
91///     algorithm: DiffAlgorithm::Patience,
92/// };
93///
94/// let result = blame_with_options(&revisions, options)?;
95/// ```
96pub fn blame_with_options<'a, T>(
97    revisions: &'a [BlameRevision<'a, T>],
98    options: BlameOptions,
99) -> Result<BlameResult<'a, T>, BlameError> {
100    if revisions.is_empty() {
101        return Err(BlameError::EmptyRevisions);
102    }
103
104    let similar_algorithm = match options.algorithm {
105        DiffAlgorithm::Myers => Algorithm::Myers,
106        DiffAlgorithm::Patience => Algorithm::Patience,
107    };
108
109    let first_revision = &revisions[0];
110
111    let init_diff = TextDiff::configure()
112        .algorithm(similar_algorithm)
113        .diff_lines("", first_revision.content);
114
115    // Pre-allocate capacity based on estimated line count
116    let estimated_lines = first_revision.content.lines().count();
117    let mut line_origins: Vec<LineOrigin<'a, T>> = Vec::with_capacity(estimated_lines);
118
119    // Create shared reference to first revision's metadata
120    let first_metadata = Rc::clone(&first_revision.metadata);
121
122    for change in init_diff.iter_all_changes() {
123        if change.tag() == ChangeTag::Insert {
124            line_origins.push(LineOrigin {
125                content: change.value(),
126                metadata: Rc::clone(&first_metadata),
127            });
128        }
129    }
130
131    // Forward iteration: track each line's origin through revisions
132    for i in 0..revisions.len() - 1 {
133        let old_content = revisions[i].content;
134        let new_content = revisions[i + 1].content;
135
136        // Create shared reference to this revision's metadata
137        let shared_metadata = Rc::clone(&revisions[i + 1].metadata);
138
139        let diff = TextDiff::configure()
140            .algorithm(similar_algorithm)
141            .diff_lines(old_content, new_content);
142
143        // Pre-allocate based on new content's line count
144        let estimated_new_lines = new_content.lines().count();
145        let mut new_line_origins: Vec<LineOrigin<'a, T>> = Vec::with_capacity(estimated_new_lines);
146
147        for change in diff.iter_all_changes() {
148            match change.tag() {
149                ChangeTag::Equal => {
150                    let old_line_num = change
151                        .old_index()
152                        .expect("Equal change must have old_index");
153                    let origin = line_origins
154                        .get(old_line_num)
155                        .expect("old_index should be within line_origins bounds");
156                    new_line_origins.push(origin.clone());
157                }
158                ChangeTag::Insert => {
159                    new_line_origins.push(LineOrigin {
160                        content: change.value(),
161                        metadata: Rc::clone(&shared_metadata),
162                    });
163                }
164                ChangeTag::Delete => {}
165            }
166        }
167
168        line_origins = new_line_origins;
169    }
170
171    let blame_lines: Vec<BlameLine<'a, T>> = line_origins
172        .into_iter()
173        .enumerate()
174        .map(|(idx, origin)| BlameLine {
175            line_number: idx,
176            content: origin.content,
177            revision_metadata: origin.metadata,
178        })
179        .collect();
180
181    Ok(BlameResult::new(blame_lines))
182}