Jump to…
snowinitial commitqoxwzsukwmkx1mo
Matt W1//! The indexing pipeline (spec §4).
Matt W2//!
Matt W3//! ```text
Matt W4//! worker: IndexPush
Matt W5//! 1. walk new commits reachable from updated refs
Matt W6//! 2. extract change ids
Matt W7//! 3. upsert changes and revisions
Matt W8//! 4. recompute stack edges for affected changes
Matt W9//! 5. detect conflict state
Matt W10//! 6. rebase comment anchors onto the new revision
Matt W11//! 7. recompute change states vs target bookmarks
Matt W12//! 8. emit timeline events
Matt W13//! 9. NOTIFY so open pages can refresh
Matt W14//! ```
Matt W15//!
Matt W16//! The indexer must be **idempotent and re-runnable** — running it twice over
Matt W17//! the same repository must produce the same rows, because every change to
Matt W18//! indexing logic means reindexing live repositories.
Matt W19//!
Matt W20//! This module holds the pure decision logic. The database and storage calls
Matt W21//! live in `df-worker`, so the interesting parts are testable without either.
Matt W22
Matt W23use std::collections::{HashMap, HashSet};
Matt W24
Matt W25use crate::ChangeId;
Matt W26
Matt W27/// A commit as the indexer sees it, independent of any Git library.
Matt W28#[derive(Debug, Clone, PartialEq, Eq)]
Matt W29pub struct IndexedCommit {
Matt W30 pub rev: String,
Matt W31 pub change_id: Option<ChangeId>,
Matt W32 pub parents: Vec<String>,
Matt W33 pub author_name: String,
Matt W34 pub author_email: String,
Matt W35 pub authored_at: chrono::DateTime<chrono::Utc>,
Matt W36 pub message: String,
Matt W37 pub conflicted: bool,
Matt W38 pub conflict_sides: Vec<String>,
Matt W39 pub conflict_bases: Vec<String>,
Matt W40}
Matt W41
Matt W42impl IndexedCommit {
Matt W43 pub fn summary(&self) -> &str {
Matt W44 self.message.lines().next().unwrap_or("").trim()
Matt W45 }
Matt W46}
Matt W47
Matt W48/// A stack edge: `parent` sits immediately below `child`.
Matt W49#[derive(Debug, Clone, PartialEq, Eq, Hash)]
Matt W50pub struct StackEdge {
Matt W51 pub parent: String,
Matt W52 pub child: String,
Matt W53}
Matt W54
Matt W55/// Compute stack edges over a set of commits.
Matt W56///
Matt W57/// Spec §4: "A stack is a chain of changes where each is a parent of the next
Matt W58/// and none has yet merged into the target bookmark."
Matt W59///
Matt W60/// Edges are between *changes*, not revisions — that is what makes a stack
Matt W61/// survive a rebase. `merged` names changes already on the target bookmark;
Matt W62/// they terminate a stack rather than extending it.
Matt W63///
Matt W64/// Walks parent links transitively through commits whose change is already
Matt W65/// merged, so a stack is not severed by an intervening landed change.
Matt W66pub fn stack_edges(
Matt W67 commits: &[IndexedCommit],
Matt W68 merged: &HashSet<String>,
Matt W69) -> HashSet<StackEdge> {
Matt W70 // rev -> change id, for the commits in scope.
Matt W71 let by_rev: HashMap<&str, &IndexedCommit> =
Matt W72 commits.iter().map(|c| (c.rev.as_str(), c)).collect();
Matt W73
Matt W74 let change_of = |rev: &str| -> Option<String> {
Matt W75 by_rev
Matt W76 .get(rev)
Matt W77 .and_then(|c| c.change_id.as_ref())
Matt W78 .map(|c| c.as_str().to_owned())
Matt W79 };
Matt W80
Matt W81 let mut edges = HashSet::new();
Matt W82
Matt W83 for c in commits {
Matt W84 let Some(child) = c.change_id.as_ref().map(|c| c.as_str().to_owned()) else {
Matt W85 continue;
Matt W86 };
Matt W87 if merged.contains(&child) {
Matt W88 continue;
Matt W89 }
Matt W90
Matt W91 // Walk up until we find an unmerged ancestor change, or run out.
Matt W92 let mut frontier: Vec<String> = c.parents.clone();
Matt W93 let mut seen: HashSet<String> = HashSet::new();
Matt W94
Matt W95 while let Some(rev) = frontier.pop() {
Matt W96 if !seen.insert(rev.clone()) {
Matt W97 continue;
Matt W98 }
Matt W99 let Some(parent_change) = change_of(&rev) else {
Matt W100 // Outside the indexed set: stop this branch.
Matt W101 continue;
Matt W102 };
Matt W103
Matt W104 if parent_change == child {
Matt W105 // Same change appearing twice in the walk; not an edge.
Matt W106 continue;
Matt W107 }
Matt W108
Matt W109 if merged.contains(&parent_change) {
Matt W110 // Landed: look further up rather than ending the stack here.
Matt W111 if let Some(p) = by_rev.get(rev.as_str()) {
Matt W112 frontier.extend(p.parents.iter().cloned());
Matt W113 }
Matt W114 continue;
Matt W115 }
Matt W116
Matt W117 edges.insert(StackEdge {
Matt W118 parent: parent_change,
Matt W119 child: child.clone(),
Matt W120 });
Matt W121 }
Matt W122 }
Matt W123
Matt W124 edges
Matt W125}
Matt W126
Matt W127/// Decide the new state of a change.
Matt W128///
Matt W129/// Spec §4 step 7, and the draft decision: `draft` is set by the author in the
Matt W130/// UI and must never be overwritten by indexing.
Matt W131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Matt W132pub enum ChangeState {
Matt W133 Draft,
Matt W134 Open,
Matt W135 Merged,
Matt W136 Abandoned,
Matt W137}
Matt W138
Matt W139pub fn next_state(current: ChangeState, on_target_bookmark: bool) -> ChangeState {
Matt W140 match current {
Matt W141 // The author's explicit choice wins over anything inferred.
Matt W142 ChangeState::Draft => ChangeState::Draft,
Matt W143 // Abandoned is also explicit; a re-push does not silently reopen it.
Matt W144 ChangeState::Abandoned => ChangeState::Abandoned,
Matt W145 _ if on_target_bookmark => ChangeState::Merged,
Matt W146 // A change that was merged and is no longer reachable from the target
Matt W147 // has been reverted or the bookmark was rewound; reopen it.
Matt W148 ChangeState::Merged => ChangeState::Open,
Matt W149 ChangeState::Open => ChangeState::Open,
Matt W150 }
Matt W151}
Matt W152
Matt W153/// Where a comment anchor ended up after a new revision landed.
Matt W154///
Matt W155/// Spec §5: "This is the mechanism that makes stable change identity actually
Matt W156/// pay off for reviewers, and it is the single most valuable piece of logic in
Matt W157/// the product."
Matt W158#[derive(Debug, Clone, PartialEq, Eq)]
Matt W159pub enum AnchorOutcome {
Matt W160 /// Line is unchanged; keep `current`, update the anchor revision.
Matt W161 Unchanged,
Matt W162 /// Line moved; keep `current`, update the line number.
Matt W163 Moved { new_line: u32 },
Matt W164 /// The line's content changed; mark `outdated`, retain the original text.
Matt W165 Outdated,
Matt W166 /// The file or hunk is gone; mark `orphaned` and surface in the timeline.
Matt W167 Orphaned,
Matt W168}
Matt W169
Matt W170/// One line of the new version of a file, for anchor rebasing.
Matt W171#[derive(Debug, Clone)]
Matt W172pub struct NewLine {
Matt W173 pub number: u32,
Matt W174 pub content: String,
Matt W175 /// The line number this came from in the old version, when it is a
Matt W176 /// carried-over (context) line.
Matt W177 pub from_old: Option<u32>,
Matt W178}
Matt W179
Matt W180/// Re-anchor a comment onto a new revision.
Matt W181///
Matt W182/// `old_line` is where the comment sat; `anchor_context` is the text of that
Matt W183/// line when the comment was written — the thing we match on, because line
Matt W184/// numbers move but content is what the reviewer was talking about.
Matt W185pub fn rebase_anchor(
Matt W186 old_line: u32,
Matt W187 anchor_context: Option<&str>,
Matt W188 new_lines: &[NewLine],
Matt W189 file_still_exists: bool,
Matt W190) -> AnchorOutcome {
Matt W191 if !file_still_exists {
Matt W192 return AnchorOutcome::Orphaned;
Matt W193 }
Matt W194
Matt W195 // 1. The line survived at a known position.
Matt W196 if let Some(l) = new_lines.iter().find(|l| l.from_old == Some(old_line)) {
Matt W197 // Its content may still have changed (whitespace, a rewrite).
Matt W198 if let Some(ctx) = anchor_context {
Matt W199 if l.content != ctx {
Matt W200 return AnchorOutcome::Outdated;
Matt W201 }
Matt W202 }
Matt W203 return if l.number == old_line {
Matt W204 AnchorOutcome::Unchanged
Matt W205 } else {
Matt W206 AnchorOutcome::Moved { new_line: l.number }
Matt W207 };
Matt W208 }
Matt W209
Matt W210 // 2. The mapping is gone, but identical text still exists — the line was
Matt W211 // moved rather than edited. Only trust this when the content is
Matt W212 // distinctive: matching on a blank line or a lone `}` would anchor the
Matt W213 // comment somewhere arbitrary, which is worse than admitting it is
Matt W214 // outdated.
Matt W215 if let Some(ctx) = anchor_context.filter(|c| is_distinctive(c)) {
Matt W216 let matches: Vec<&NewLine> = new_lines.iter().filter(|l| l.content == ctx).collect();
Matt W217 if matches.len() == 1 {
Matt W218 return AnchorOutcome::Moved {
Matt W219 new_line: matches[0].number,
Matt W220 };
Matt W221 }
Matt W222 }
Matt W223
Matt W224 // 3. The line is gone from the file.
Matt W225 AnchorOutcome::Outdated
Matt W226}
Matt W227
Matt W228/// Whether a line's content is specific enough to re-anchor on.
Matt W229fn is_distinctive(s: &str) -> bool {
Matt W230 let t = s.trim();
Matt W231 // Structural punctuation and blank lines repeat throughout a file.
Matt W232 t.len() >= 4 && t.chars().any(|c| c.is_alphanumeric())
Matt W233}
Matt W234
Matt W235/// Build the old→new line mapping for a file across a rewrite.
Matt W236///
Matt W237/// [`rebase_anchor`] needs to know, for each line of the new file, which line of
Matt W238/// the old file it came from. A diff's hunks only carry that for the lines near
Matt W239/// a change, and a comment can sit anywhere — so the mapping is computed over
Matt W240/// the whole file rather than read out of a hunk.
Matt W241///
Matt W242/// Uses `similar`'s line diff, the same algorithm `df-store` renders diffs with,
Matt W243/// so the mapping a comment is rebased through agrees with the diff a reviewer
Matt W244/// is looking at.
Matt W245pub fn line_map(old: &str, new: &str) -> Vec<NewLine> {
Matt W246 use similar::{ChangeTag, TextDiff};
Matt W247
Matt W248 let diff = TextDiff::from_lines(old, new);
Matt W249 let mut out = Vec::new();
Matt W250
Matt W251 for change in diff.iter_all_changes() {
Matt W252 // Only lines that exist in the new file get an entry; a deleted line is
Matt W253 // absent from the mapping, which is exactly what makes `rebase_anchor`
Matt W254 // fall through to its content search.
Matt W255 let Some(new_index) = change.new_index() else {
Matt W256 continue;
Matt W257 };
Matt W258 out.push(NewLine {
Matt W259 number: new_index as u32 + 1,
Matt W260 content: change.value().trim_end_matches('\n').to_owned(),
Matt W261 from_old: match change.tag() {
Matt W262 // An inserted line came from nowhere.
Matt W263 ChangeTag::Insert => None,
Matt W264 _ => change.old_index().map(|i| i as u32 + 1),
Matt W265 },
Matt W266 });
Matt W267 }
Matt W268
Matt W269 out
Matt W270}
Matt W271
Matt W272#[cfg(test)]
Matt W273mod tests {
Matt W274 use super::*;
Matt W275
Matt W276 fn commit(rev: &str, change: &str, parents: &[&str]) -> IndexedCommit {
Matt W277 IndexedCommit {
Matt W278 rev: rev.into(),
Matt W279 change_id: ChangeId::parse(change),
Matt W280 parents: parents.iter().map(|s| s.to_string()).collect(),
Matt W281 author_name: "A".into(),
Matt W282 author_email: "a@b.c".into(),
Matt W283 authored_at: chrono::Utc::now(),
Matt W284 message: "msg".into(),
Matt W285 conflicted: false,
Matt W286 conflict_sides: vec![],
Matt W287 conflict_bases: vec![],
Matt W288 }
Matt W289 }
Matt W290
Matt W291 /// 32 chars in the reverse-hex alphabet.
Matt W292 fn cid(seed: char) -> String {
Matt W293 std::iter::repeat(seed).take(32).collect()
Matt W294 }
Matt W295
Matt W296 // ─── stacks ──────────────────────────────────────────────────────────────
Matt W297
Matt W298 #[test]
Matt W299 fn a_linear_chain_produces_a_chain_of_edges() {
Matt W300 let (a, b, c) = (cid('k'), cid('l'), cid('m'));
Matt W301 let commits = vec![
Matt W302 commit("r1", &a, &[]),
Matt W303 commit("r2", &b, &["r1"]),
Matt W304 commit("r3", &c, &["r2"]),
Matt W305 ];
Matt W306 let edges = stack_edges(&commits, &HashSet::new());
Matt W307
Matt W308 assert!(edges.contains(&StackEdge { parent: a.clone(), child: b.clone() }));
Matt W309 assert!(edges.contains(&StackEdge { parent: b, child: c }));
Matt W310 assert_eq!(edges.len(), 2);
Matt W311 }
Matt W312
Matt W313 #[test]
Matt W314 fn merged_changes_do_not_appear_in_stacks() {
Matt W315 let (a, b) = (cid('k'), cid('l'));
Matt W316 let commits = vec![commit("r1", &a, &[]), commit("r2", &b, &["r1"])];
Matt W317
Matt W318 let mut merged = HashSet::new();
Matt W319 merged.insert(a);
Matt W320
Matt W321 let edges = stack_edges(&commits, &merged);
Matt W322 assert!(
Matt W323 edges.is_empty(),
Matt W324 "a landed parent must not anchor a stack: {edges:?}"
Matt W325 );
Matt W326 }
Matt W327
Matt W328 #[test]
Matt W329 fn a_stack_is_not_severed_by_a_landed_change_in_the_middle() {
Matt W330 // a (merged) <- b (merged) <- c: c should still connect upward to
Matt W331 // nothing, but a <- b <- c with only b merged must yield a -> c.
Matt W332 let (a, b, c) = (cid('k'), cid('l'), cid('m'));
Matt W333 let commits = vec![
Matt W334 commit("r1", &a, &[]),
Matt W335 commit("r2", &b, &["r1"]),
Matt W336 commit("r3", &c, &["r2"]),
Matt W337 ];
Matt W338 let mut merged = HashSet::new();
Matt W339 merged.insert(b.clone());
Matt W340
Matt W341 let edges = stack_edges(&commits, &merged);
Matt W342 assert!(
Matt W343 edges.contains(&StackEdge { parent: a, child: c }),
Matt W344 "walk must pass through the landed change: {edges:?}"
Matt W345 );
Matt W346 }
Matt W347
Matt W348 #[test]
Matt W349 fn commits_without_a_change_id_do_not_produce_edges() {
Matt W350 let a = cid('k');
Matt W351 let mut plain = commit("r1", &a, &[]);
Matt W352 plain.change_id = None;
Matt W353 let commits = vec![plain, commit("r2", &cid('l'), &["r1"])];
Matt W354
Matt W355 let edges = stack_edges(&commits, &HashSet::new());
Matt W356 assert!(edges.is_empty(), "got {edges:?}");
Matt W357 }
Matt W358
Matt W359 #[test]
Matt W360 fn is_idempotent_over_repeated_runs() {
Matt W361 // The indexer must be re-runnable (spec §4).
Matt W362 let commits = vec![
Matt W363 commit("r1", &cid('k'), &[]),
Matt W364 commit("r2", &cid('l'), &["r1"]),
Matt W365 ];
Matt W366 let first = stack_edges(&commits, &HashSet::new());
Matt W367 let second = stack_edges(&commits, &HashSet::new());
Matt W368 assert_eq!(first, second);
Matt W369 }
Matt W370
Matt W371 #[test]
Matt W372 fn a_cycle_does_not_hang_the_walk() {
Matt W373 // Impossible in Git, but the indexer must not be the thing that hangs
Matt W374 // if the graph is ever malformed.
Matt W375 let (a, b) = (cid('k'), cid('l'));
Matt W376 let commits = vec![
Matt W377 commit("r1", &a, &["r2"]),
Matt W378 commit("r2", &b, &["r1"]),
Matt W379 ];
Matt W380 let _ = stack_edges(&commits, &HashSet::new());
Matt W381 }
Matt W382
Matt W383 #[test]
Matt W384 fn a_merge_commit_links_to_both_parents() {
Matt W385 let (a, b, m) = (cid('k'), cid('l'), cid('m'));
Matt W386 let commits = vec![
Matt W387 commit("r1", &a, &[]),
Matt W388 commit("r2", &b, &[]),
Matt W389 commit("r3", &m, &["r1", "r2"]),
Matt W390 ];
Matt W391 let edges = stack_edges(&commits, &HashSet::new());
Matt W392 assert!(edges.contains(&StackEdge { parent: a, child: m.clone() }));
Matt W393 assert!(edges.contains(&StackEdge { parent: b, child: m }));
Matt W394 }
Matt W395
Matt W396 // ─── state ───────────────────────────────────────────────────────────────
Matt W397
Matt W398 #[test]
Matt W399 fn indexing_never_overwrites_draft() {
Matt W400 // The decided behaviour: draft is the author's, set in the UI.
Matt W401 assert_eq!(next_state(ChangeState::Draft, true), ChangeState::Draft);
Matt W402 assert_eq!(next_state(ChangeState::Draft, false), ChangeState::Draft);
Matt W403 }
Matt W404
Matt W405 #[test]
Matt W406 fn indexing_never_reopens_an_abandoned_change() {
Matt W407 assert_eq!(
Matt W408 next_state(ChangeState::Abandoned, false),
Matt W409 ChangeState::Abandoned
Matt W410 );
Matt W411 assert_eq!(
Matt W412 next_state(ChangeState::Abandoned, true),
Matt W413 ChangeState::Abandoned
Matt W414 );
Matt W415 }
Matt W416
Matt W417 #[test]
Matt W418 fn a_change_on_the_target_bookmark_becomes_merged() {
Matt W419 assert_eq!(next_state(ChangeState::Open, true), ChangeState::Merged);
Matt W420 }
Matt W421
Matt W422 #[test]
Matt W423 fn a_rewound_bookmark_reopens_a_merged_change() {
Matt W424 assert_eq!(next_state(ChangeState::Merged, false), ChangeState::Open);
Matt W425 }
Matt W426
Matt W427 // ─── anchor rebasing (spec §5) ───────────────────────────────────────────
Matt W428
Matt W429 fn lines(spec: &[(u32, &str, Option<u32>)]) -> Vec<NewLine> {
Matt W430 spec.iter()
Matt W431 .map(|(n, c, f)| NewLine {
Matt W432 number: *n,
Matt W433 content: (*c).to_string(),
Matt W434 from_old: *f,
Matt W435 })
Matt W436 .collect()
Matt W437 }
Matt W438
Matt W439 #[test]
Matt W440 fn an_unchanged_line_stays_current() {
Matt W441 let new = lines(&[(1, "let x = 1;", Some(1))]);
Matt W442 assert_eq!(
Matt W443 rebase_anchor(1, Some("let x = 1;"), &new, true),
Matt W444 AnchorOutcome::Unchanged
Matt W445 );
Matt W446 }
Matt W447
Matt W448 #[test]
Matt W449 fn a_line_pushed_down_by_an_insertion_moves() {
Matt W450 // Someone added a line above; the comment follows its line.
Matt W451 let new = lines(&[
Matt W452 (1, "// new comment", None),
Matt W453 (2, "let x = 1;", Some(1)),
Matt W454 ]);
Matt W455 assert_eq!(
Matt W456 rebase_anchor(1, Some("let x = 1;"), &new, true),
Matt W457 AnchorOutcome::Moved { new_line: 2 }
Matt W458 );
Matt W459 }
Matt W460
Matt W461 #[test]
Matt W462 fn an_edited_line_becomes_outdated() {
Matt W463 let new = lines(&[(1, "let x = 2;", Some(1))]);
Matt W464 assert_eq!(
Matt W465 rebase_anchor(1, Some("let x = 1;"), &new, true),
Matt W466 AnchorOutcome::Outdated
Matt W467 );
Matt W468 }
Matt W469
Matt W470 #[test]
Matt W471 fn a_deleted_file_orphans_the_comment() {
Matt W472 assert_eq!(
Matt W473 rebase_anchor(1, Some("anything"), &[], false),
Matt W474 AnchorOutcome::Orphaned
Matt W475 );
Matt W476 }
Matt W477
Matt W478 #[test]
Matt W479 fn a_deleted_line_becomes_outdated_not_orphaned() {
Matt W480 // The file still exists, so the comment belongs in the diff view marked
Matt W481 // outdated rather than being exiled to the timeline.
Matt W482 let new = lines(&[(1, "something else entirely", None)]);
Matt W483 assert_eq!(
Matt W484 rebase_anchor(5, Some("let x = 1;"), &new, true),
Matt W485 AnchorOutcome::Outdated
Matt W486 );
Matt W487 }
Matt W488
Matt W489 #[test]
Matt W490 fn a_moved_line_is_found_by_its_distinctive_content() {
Matt W491 // A rebase reordered the file; the mapping is gone but the text is
Matt W492 // unique, so the comment follows it.
Matt W493 let new = lines(&[
Matt W494 (1, "unrelated", None),
Matt W495 (2, "fn interesting_function() {", None),
Matt W496 ]);
Matt W497 assert_eq!(
Matt W498 rebase_anchor(50, Some("fn interesting_function() {"), &new, true),
Matt W499 AnchorOutcome::Moved { new_line: 2 }
Matt W500 );
Matt W501 }
Matt W502
Matt W503 #[test]
Matt W504 fn ambiguous_content_does_not_move_the_anchor() {
Matt W505 // Two identical lines: guessing would put the comment in the wrong
Matt W506 // place, which is worse than marking it outdated.
Matt W507 let new = lines(&[
Matt W508 (1, " return None;", None),
Matt W509 (2, " return None;", None),
Matt W510 ]);
Matt W511 assert_eq!(
Matt W512 rebase_anchor(9, Some(" return None;"), &new, true),
Matt W513 AnchorOutcome::Outdated
Matt W514 );
Matt W515 }
Matt W516
Matt W517 #[test]
Matt W518 fn structural_lines_are_not_used_to_re_anchor() {
Matt W519 // Matching on `}` or a blank line would anchor almost anywhere.
Matt W520 let new = lines(&[(1, "}", None)]);
Matt W521 assert_eq!(
Matt W522 rebase_anchor(9, Some("}"), &new, true),
Matt W523 AnchorOutcome::Outdated
Matt W524 );
Matt W525 assert_eq!(
Matt W526 rebase_anchor(9, Some(" "), &new, true),
Matt W527 AnchorOutcome::Outdated
Matt W528 );
Matt W529 }
Matt W530
Matt W531 #[test]
Matt W532 fn whitespace_only_reindentation_marks_outdated_not_moved() {
Matt W533 // The line is the same code, but its text changed. Marking it outdated
Matt W534 // keeps the original text visible to the reviewer (spec §5 step 4).
Matt W535 let new = lines(&[(1, " let x = 1;", Some(1))]);
Matt W536 assert_eq!(
Matt W537 rebase_anchor(1, Some("let x = 1;"), &new, true),
Matt W538 AnchorOutcome::Outdated
Matt W539 );
Matt W540 }
Matt W541
Matt W542 #[test]
Matt W543 fn a_comment_with_no_stored_context_follows_the_line_mapping() {
Matt W544 // Older comments predate anchor_context; they still rebase by position.
Matt W545 let new = lines(&[(7, "whatever", Some(3))]);
Matt W546 assert_eq!(
Matt W547 rebase_anchor(3, None, &new, true),
Matt W548 AnchorOutcome::Moved { new_line: 7 }
Matt W549 );
Matt W550 }
Matt W551
Matt W552 #[test]
Matt W553 fn survives_five_successive_rewrites() {
Matt W554 // Spec §5: "a change that is rewritten five times".
Matt W555 let mut line = 10u32;
Matt W556 let context = "fn the_function_under_review() {";
Matt W557
Matt W558 for shift in 1..=5u32 {
Matt W559 let new = lines(&[(line + shift, context, Some(line))]);
Matt W560 match rebase_anchor(line, Some(context), &new, true) {
Matt W561 AnchorOutcome::Moved { new_line } => line = new_line,
Matt W562 other => panic!("rewrite {shift} lost the anchor: {other:?}"),
Matt W563 }
Matt W564 }
Matt W565 assert_eq!(line, 25, "anchor should have tracked every shift");
Matt W566 }
Matt W567
Matt W568 // ─── line mapping ────────────────────────────────────────────────────────
Matt W569
Matt W570 #[test]
Matt W571 fn line_map_carries_unchanged_lines_through() {
Matt W572 let m = line_map("a\nb\nc\n", "a\nb\nc\n");
Matt W573 assert_eq!(m.len(), 3);
Matt W574 for (i, l) in m.iter().enumerate() {
Matt W575 assert_eq!(l.number, i as u32 + 1);
Matt W576 assert_eq!(l.from_old, Some(i as u32 + 1), "identical files map 1:1");
Matt W577 }
Matt W578 }
Matt W579
Matt W580 #[test]
Matt W581 fn an_inserted_line_shifts_the_ones_below_it() {
Matt W582 let m = line_map("a\nb\n", "new\na\nb\n");
Matt W583 assert_eq!(m[0].from_old, None, "the inserted line came from nowhere");
Matt W584 assert_eq!(m[1].from_old, Some(1));
Matt W585 assert_eq!(m[1].number, 2, "`a` moved from line 1 to line 2");
Matt W586 }
Matt W587
Matt W588 #[test]
Matt W589 fn a_deleted_line_is_absent_from_the_mapping() {
Matt W590 let m = line_map("a\nb\nc\n", "a\nc\n");
Matt W591 assert!(
Matt W592 !m.iter().any(|l| l.from_old == Some(2)),
Matt W593 "the deleted line must not appear: {m:?}"
Matt W594 );
Matt W595 assert_eq!(m.iter().find(|l| l.content == "c").unwrap().from_old, Some(3));
Matt W596 }
Matt W597
Matt W598 /// The end-to-end property this exists for: a comment on a line survives an
Matt W599 /// insertion above it.
Matt W600 #[test]
Matt W601 fn a_comment_follows_its_line_through_a_real_rewrite() {
Matt W602 let old = "fn a() {}\nfn the_reviewed_function() {}\nfn c() {}\n";
Matt W603 let new = "use std::io;\n\nfn a() {}\nfn the_reviewed_function() {}\nfn c() {}\n";
Matt W604 let m = line_map(old, new);
Matt W605 assert_eq!(
Matt W606 rebase_anchor(2, Some("fn the_reviewed_function() {}"), &m, true),
Matt W607 AnchorOutcome::Moved { new_line: 4 }
Matt W608 );
Matt W609 }
Matt W610
Matt W611 #[test]
Matt W612 fn a_file_emptied_by_a_rewrite_outdates_rather_than_panicking() {
Matt W613 let m = line_map("a\nb\n", "");
Matt W614 assert_eq!(rebase_anchor(1, Some("a"), &m, true), AnchorOutcome::Outdated);
Matt W615 }
Matt W616}

616 lines · Rust