a73x

702a1e6f

Run cargo fmt over the whole tree

a73x   2026-09-05 17:13

Commit message
Run cargo fmt over the whole tree

Formatting only, no behaviour change. The tree had drifted from rustfmt in
about fifty files, so any patch that formatted its own files dragged
unrelated hunks along. One commit that takes the hit for everything.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

src/abbrev.rs
Old New
@@ -181,10 +181,7 @@ impl Abbrev {
181 /// Ids are ASCII hex, so bytes and characters coincide and slicing at a byte 181 /// Ids are ASCII hex, so bytes and characters coincide and slicing at a byte
182 /// index is always on a character boundary. 182 /// index is always on a character boundary.
183 fn common_prefix_len(a: &str, b: &str) -> usize { 183 fn common_prefix_len(a: &str, b: &str) -> usize {
184 a.bytes() 184 a.bytes().zip(b.bytes()).take_while(|(x, y)| x == y).count()
185 .zip(b.bytes())
186 .take_while(|(x, y)| x == y)
187 .count()
188 } 185 }
189 186
190 /// Read `collab.abbrev`, or `None` to use the automatic width. 187 /// Read `collab.abbrev`, or `None` to use the automatic width.
@@ -320,7 +317,14 @@ mod tests {
320 #[test] 317 #[test]
321 fn every_printed_prefix_matches_exactly_one_id() { 318 fn every_printed_prefix_matches_exactly_one_id() {
322 let all = ids(&[ 319 let all = ids(&[
323 "aaaaaaaa1", "aaaaaaaa2", "aaaaaaab", "bbbbbbbb", "bbbbbbbc", "c", "d", "e", 320 "aaaaaaaa1",
321 "aaaaaaaa2",
322 "aaaaaaab",
323 "bbbbbbbb",
324 "bbbbbbbc",
325 "c",
326 "d",
327 "e",
324 ]); 328 ]);
325 let a = Abbrev::new(all.clone()); 329 let a = Abbrev::new(all.clone());
326 for id in &all { 330 for id in &all {
src/body.rs
Old New
@@ -150,7 +150,11 @@ mod tests {
150 fn both_options_together_are_rejected() { 150 fn both_options_together_are_rejected() {
151 let args = BodyArgs::new(Some("a"), Some("b")); 151 let args = BodyArgs::new(Some("a"), Some("b"));
152 let err = format!("{}", resolve_required(&args, "", "comment").unwrap_err()); 152 let err = format!("{}", resolve_required(&args, "", "comment").unwrap_err());
153 assert!(err.contains("--body") && err.contains("--body-file"), "{}", err); 153 assert!(
154 err.contains("--body") && err.contains("--body-file"),
155 "{}",
156 err
157 );
154 } 158 }
155 159
156 #[test] 160 #[test]
@@ -201,5 +205,4 @@ mod tests {
201 let args = BodyArgs::new(Some(""), None); 205 let args = BodyArgs::new(Some(""), None);
202 assert_eq!(resolve_optional(&args).unwrap(), Some(String::new())); 206 assert_eq!(resolve_optional(&args).unwrap(), Some(String::new()));
203 } 207 }
204
205 } 208 }
src/build_provenance.rs
Old New
@@ -165,10 +165,7 @@ fn in_our_own_checkout() -> bool {
165 let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") else { 165 let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") else {
166 return false; 166 return false;
167 }; 167 };
168 match ( 168 match (fs::canonicalize(&toplevel), fs::canonicalize(&manifest_dir)) {
169 fs::canonicalize(&toplevel),
170 fs::canonicalize(&manifest_dir),
171 ) {
172 (Ok(a), Ok(b)) => a == b, 169 (Ok(a), Ok(b)) => a == b,
173 _ => false, 170 _ => false,
174 } 171 }
src/cli.rs
Old New
@@ -360,7 +360,6 @@ pub enum HookCmd {
360 /// argument for the flag. 360 /// argument for the flag.
361 const BODY_FILE_HELP: &str = "Read the body from a file, or from stdin with '-'"; 361 const BODY_FILE_HELP: &str = "Read the body from a file, or from stdin with '-'";
362 362
363
364 // --------------------------------------------------------------------------- 363 // ---------------------------------------------------------------------------
365 // `--json` is per-command rather than a global flag, and the four `wants_json` 364 // `--json` is per-command rather than a global flag, and the four `wants_json`
366 // implementations below are the price of that. 365 // implementations below are the price of that.
src/dag.rs
Old New
@@ -229,13 +229,13 @@ pub fn reconcile(
229 return Ok((local_oid, ReconcileOutcome::AlreadyCurrent)); 229 return Ok((local_oid, ReconcileOutcome::AlreadyCurrent));
230 } 230 }
231 231
232 let merge_base = repo.merge_base(local_oid, remote_oid).map_err(|e| { 232 let merge_base =
233 Error::DisjointHistories { 233 repo.merge_base(local_oid, remote_oid)
234 local_ref: local_ref.to_string(), 234 .map_err(|e| Error::DisjointHistories {
235 remote_ref: remote_ref.to_string(), 235 local_ref: local_ref.to_string(),
236 detail: e.message().to_string(), 236 remote_ref: remote_ref.to_string(),
237 } 237 detail: e.message().to_string(),
238 })?; 238 })?;
239 239
240 if merge_base == remote_oid { 240 if merge_base == remote_oid {
241 // Remote is ancestor of local — local is ahead 241 // Remote is ancestor of local — local is ahead
@@ -350,7 +350,9 @@ fn commit_message(action: &Action) -> String {
350 Action::IssueComment { .. } => "issue: comment".to_string(), 350 Action::IssueComment { .. } => "issue: comment".to_string(),
351 Action::IssueClose { .. } => "issue: close".to_string(), 351 Action::IssueClose { .. } => "issue: close".to_string(),
352 Action::IssueReopen => "issue: reopen".to_string(), 352 Action::IssueReopen => "issue: reopen".to_string(),
353 Action::IssueCommitLink { commit } => format!("issue: commit link {}", &commit[..commit.len().min(7)]), 353 Action::IssueCommitLink { commit } => {
354 format!("issue: commit link {}", &commit[..commit.len().min(7)])
355 }
354 Action::PatchCreate { title, .. } => format!("patch: create \"{}\"", title), 356 Action::PatchCreate { title, .. } => format!("patch: create \"{}\"", title),
355 Action::PatchLabel { ref label } => format!("patch: label \"{}\"", label), 357 Action::PatchLabel { ref label } => format!("patch: label \"{}\"", label),
356 Action::PatchUnlabel { ref label } => format!("patch: unlabel \"{}\"", label), 358 Action::PatchUnlabel { ref label } => format!("patch: unlabel \"{}\"", label),
@@ -361,7 +363,10 @@ fn commit_message(action: &Action) -> String {
361 Action::PatchInlineComment { ref file, line, .. } => { 363 Action::PatchInlineComment { ref file, line, .. } => {
362 format!("patch: inline comment on {}:{}", file, line) 364 format!("patch: inline comment on {}:{}", file, line)
363 } 365 }
364 Action::PatchCommentResolve { ref comment, revision } => match revision { 366 Action::PatchCommentResolve {
367 ref comment,
368 revision,
369 } => match revision {
365 Some(n) => format!("patch: resolve comment {:.8} at r{}", comment, n), 370 Some(n) => format!("patch: resolve comment {:.8} at r{}", comment, n),
366 None => format!("patch: resolve comment {:.8}", comment), 371 None => format!("patch: resolve comment {:.8}", comment),
367 }, 372 },
src/editor.rs
Old New
@@ -319,7 +319,10 @@ mod tests {
319 let out = compose_with("", &script).unwrap(); 319 let out = compose_with("", &script).unwrap();
320 assert_eq!(out.trim_end_matches('\n'), content.trim_end_matches('\n')); 320 assert_eq!(out.trim_end_matches('\n'), content.trim_end_matches('\n'));
321 assert!(out.starts_with("# Heading"), "no stripping of leading #"); 321 assert!(out.starts_with("# Heading"), "no stripping of leading #");
322 assert!(out.contains('\t') && out.contains('\r'), "no whitespace tidying"); 322 assert!(
323 out.contains('\t') && out.contains('\r'),
324 "no whitespace tidying"
325 );
323 assert!(out.contains('\u{301}'), "non-ASCII preserved"); 326 assert!(out.contains('\u{301}'), "non-ASCII preserved");
324 } 327 }
325 328
@@ -335,9 +338,8 @@ mod tests {
335 /// that from silently returning the unedited seed. 338 /// that from silently returning the unedited seed.
336 #[test] 339 #[test]
337 fn test_compose_survives_an_editor_that_saves_by_rename() { 340 fn test_compose_survives_an_editor_that_saves_by_rename() {
338 let script = fake_editor( 341 let script =
339 "#!/bin/sh\nprintf 'renamed in\\n' > \"$1.new\"\nmv \"$1.new\" \"$1\"\n", 342 fake_editor("#!/bin/sh\nprintf 'renamed in\\n' > \"$1.new\"\nmv \"$1.new\" \"$1\"\n");
340 );
341 let out = compose_with("seed", &script).unwrap(); 343 let out = compose_with("seed", &script).unwrap();
342 assert_eq!(out, "renamed in\n"); 344 assert_eq!(out, "renamed in\n");
343 } 345 }
src/error.rs
Old New
@@ -20,7 +20,10 @@ pub enum Error {
20 #[error("verification error: {0}")] 20 #[error("verification error: {0}")]
21 Verification(String), 21 Verification(String),
22 22
23 #[error("no signing key found — run `{} init-key` to generate one", crate::BINARY_NAME)] 23 #[error(
24 "no signing key found — run `{} init-key` to generate one",
25 crate::BINARY_NAME
26 )]
24 KeyNotFound, 27 KeyNotFound,
25 28
26 #[error("untrusted key: {0}")] 29 #[error("untrusted key: {0}")]
src/event.rs
Old New
@@ -49,9 +49,7 @@ pub enum Action {
49 #[serde(rename = "issue.reopen")] 49 #[serde(rename = "issue.reopen")]
50 IssueReopen, 50 IssueReopen,
51 #[serde(rename = "issue.commit_link")] 51 #[serde(rename = "issue.commit_link")]
52 IssueCommitLink { 52 IssueCommitLink { commit: String },
53 commit: String,
54 },
55 /// A patch, and the commit and tree revision 1 stands at. 53 /// A patch, and the commit and tree revision 1 stands at.
56 /// 54 ///
57 /// The variant name `PatchCreate` and the `head_commit` spelling of the 55 /// The variant name `PatchCreate` and the `head_commit` spelling of the
src/hooks.rs
Old New
@@ -89,7 +89,10 @@ pub fn hook_script(exe: &Path) -> String {
89 89
90 /// The line someone with their own `commit-msg` hook adds to it by hand. 90 /// The line someone with their own `commit-msg` hook adds to it by hand.
91 pub fn shim_line() -> String { 91 pub fn shim_line() -> String {
92 format!("git-collab {} \"$1\" >/dev/null 2>&1 || true", SHIM_SUBCOMMAND) 92 format!(
93 "git-collab {} \"$1\" >/dev/null 2>&1 || true",
94 SHIM_SUBCOMMAND
95 )
93 } 96 }
94 97
95 // --------------------------------------------------------------------------- 98 // ---------------------------------------------------------------------------
@@ -321,7 +324,10 @@ pub fn report_install(outcome: &InstallOutcome) {
321 #[derive(Debug, Clone)] 324 #[derive(Debug, Clone)]
322 pub enum Target { 325 pub enum Target {
323 /// Exactly one open patch records this branch: the only case that stamps. 326 /// Exactly one open patch records this branch: the only case that stamps.
324 One { branch: String, patch: Box<PatchState> }, 327 One {
328 branch: String,
329 patch: Box<PatchState>,
330 },
325 /// No open patch records this branch. 331 /// No open patch records this branch.
326 NoPatch { branch: String }, 332 NoPatch { branch: String },
327 /// Several do. Ambiguous, so nothing is stamped — see [`target`]. 333 /// Several do. Ambiguous, so nothing is stamped — see [`target`].
@@ -411,10 +417,7 @@ pub fn run_commit_msg(repo: &Repository, file: &Path) {
411 /// author's nor ours; a rename either happened or did not. 417 /// author's nor ours; a rename either happened or did not.
412 fn replace_atomically(file: &Path, contents: &str) -> std::io::Result<()> { 418 fn replace_atomically(file: &Path, contents: &str) -> std::io::Result<()> {
413 let dir = file.parent().unwrap_or_else(|| Path::new(".")); 419 let dir = file.parent().unwrap_or_else(|| Path::new("."));
414 let tmp = dir.join(format!( 420 let tmp = dir.join(format!(".git-collab-commit-msg.{}", std::process::id()));
415 ".git-collab-commit-msg.{}",
416 std::process::id()
417 ));
418 std::fs::write(&tmp, contents)?; 421 std::fs::write(&tmp, contents)?;
419 if let Err(e) = std::fs::rename(&tmp, file) { 422 if let Err(e) = std::fs::rename(&tmp, file) {
420 let _ = std::fs::remove_file(&tmp); 423 let _ = std::fs::remove_file(&tmp);
@@ -595,7 +598,10 @@ pub fn status(repo: &Repository) -> Result<(), Error> {
595 println!("HEAD is not on a branch, so nothing would be stamped") 598 println!("HEAD is not on a branch, so nothing would be stamped")
596 } 599 }
597 Target::Unreadable(e) => { 600 Target::Unreadable(e) => {
598 println!("Patches could not be read ({}), so nothing would be stamped", e) 601 println!(
602 "Patches could not be read ({}), so nothing would be stamped",
603 e
604 )
599 } 605 }
600 } 606 }
601 Ok(()) 607 Ok(())
@@ -678,7 +684,8 @@ mod tests {
678 #[test] 684 #[test]
679 fn a_patch_trailer_below_the_scissors_does_not_count_as_present() { 685 fn a_patch_trailer_below_the_scissors_does_not_count_as_present() {
680 // It is part of a diff, not of the message, and git throws it away. 686 // It is part of a diff, not of the message, and git throws it away.
681 let input = "subject\n# ------------------------ >8 ------------------------\n+Patch: notmine\n"; 687 let input =
688 "subject\n# ------------------------ >8 ------------------------\n+Patch: notmine\n";
682 assert!(stamp(input, '#', "abc123").is_some()); 689 assert!(stamp(input, '#', "abc123").is_some());
683 } 690 }
684 691
src/issue.rs
Old New
@@ -73,7 +73,15 @@ pub fn list_to_writer(
73 labels: &[String], 73 labels: &[String],
74 writer: &mut dyn std::io::Write, 74 writer: &mut dyn std::io::Write,
75 ) -> Result<(), crate::error::Error> { 75 ) -> Result<(), crate::error::Error> {
76 let entries = list(repo, show_closed, show_archived, limit, offset, sort, labels)?; 76 let entries = list(
77 repo,
78 show_closed,
79 show_archived,
80 limit,
81 offset,
82 sort,
83 labels,
84 )?;
77 if entries.is_empty() { 85 if entries.is_empty() {
78 writeln!(writer, "No issues found.").ok(); 86 writeln!(writer, "No issues found.").ok();
79 return Ok(()); 87 return Ok(());
src/lib.rs
Old New
@@ -328,7 +328,11 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> {
328 if !i.linked_commits.is_empty() { 328 if !i.linked_commits.is_empty() {
329 println!("\n--- Linked Commits ---"); 329 println!("\n--- Linked Commits ---");
330 for lc in &i.linked_commits { 330 for lc in &i.linked_commits {
331 let short_sha = if lc.commit.len() >= 7 { &lc.commit[..7] } else { &lc.commit }; 331 let short_sha = if lc.commit.len() >= 7 {
332 &lc.commit[..7]
333 } else {
334 &lc.commit
335 };
332 let (subject, commit_author) = match git2::Oid::from_str(&lc.commit) 336 let (subject, commit_author) = match git2::Oid::from_str(&lc.commit)
333 .ok() 337 .ok()
334 .and_then(|oid| repo.find_commit(oid).ok()) 338 .and_then(|oid| repo.find_commit(oid).ok())
@@ -338,11 +342,8 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> {
338 .summary() 342 .summary()
339 .map(|s| truncate_summary(s, 60)) 343 .map(|s| truncate_summary(s, 60))
340 .unwrap_or_default(); 344 .unwrap_or_default();
341 let author = commit 345 let author =
342 .author() 346 commit.author().name().unwrap_or("unknown").to_string();
343 .name()
344 .unwrap_or("unknown")
345 .to_string();
346 (Some(subject), Some(author)) 347 (Some(subject), Some(author))
347 } 348 }
348 None => (None, None), 349 None => (None, None),
@@ -361,10 +362,7 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> {
361 _ => { 362 _ => {
362 println!( 363 println!(
363 "· linked {} (commit {} not in local repo) (linked by {}, {})", 364 "· linked {} (commit {} not in local repo) (linked by {}, {})",
364 short_sha, 365 short_sha, short_sha, lc.event_author.name, lc.event_timestamp,
365 short_sha,
366 lc.event_author.name,
367 lc.event_timestamp,
368 ); 366 );
369 } 367 }
370 } 368 }
@@ -834,10 +832,8 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> {
834 // same rule inline comments already follow: an absent 832 // same rule inline comments already follow: an absent
835 // anchor is shown as absent, never as `(r?)` or as a 833 // anchor is shown as absent, never as `(r?)` or as a
836 // revision it was not cast against. 834 // revision it was not cast against.
837 let rev_label = r 835 let rev_label =
838 .revision 836 r.revision.map(|n| format!(" (r{})", n)).unwrap_or_default();
839 .map(|n| format!(" (r{})", n))
840 .unwrap_or_default();
841 println!( 837 println!(
842 "\n{} ({}) - {}{}{} [{:.8}]:\n{}", 838 "\n{} ({}) - {}{}{} [{:.8}]:\n{}",
843 r.author.name, 839 r.author.name,
@@ -886,7 +882,11 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> {
886 // "blocking" would relabel every comment ever written 882 // "blocking" would relabel every comment ever written
887 // as a demand, which is the reading the flag exists to 883 // as a demand, which is the reading the flag exists to
888 // stop being the only one available. 884 // stop being the only one available.
889 let blocking = if c.non_blocking { " [non-blocking]" } else { "" }; 885 let blocking = if c.non_blocking {
886 " [non-blocking]"
887 } else {
888 ""
889 };
890 println!( 890 println!(
891 "\n{} on {}:{} ({}{}){}{} [{:.8}]:\n {}", 891 "\n{} on {}:{} ({}{}){}{} [{:.8}]:\n {}",
892 c.author.name, 892 c.author.name,
@@ -918,7 +918,11 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> {
918 let stale = match (r.revision, latest_revision) { 918 let stale = match (r.revision, latest_revision) {
919 (Some(res), Some(head)) if res < head => { 919 (Some(res), Some(head)) if res < head => {
920 format!(" — {} revision{} landed since", head - res, { 920 format!(" — {} revision{} landed since", head - res, {
921 if head - res == 1 { "" } else { "s" } 921 if head - res == 1 {
922 ""
923 } else {
924 "s"
925 }
922 }) 926 })
923 } 927 }
924 _ => String::new(), 928 _ => String::new(),
@@ -996,7 +1000,8 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> {
996 stat, 1000 stat,
997 paths, 1001 paths,
998 }; 1002 };
999 let diff = patch::diff(repo, &id, revision, between_pair, answers.as_deref(), &opts)?; 1003 let diff =
1004 patch::diff(repo, &id, revision, between_pair, answers.as_deref(), &opts)?;
1000 if diff.is_empty() { 1005 if diff.is_empty() {
1001 // An empty diff means two different things, and saying 1006 // An empty diff means two different things, and saying
1002 // "commits may be identical" for both is how a `--path` 1007 // "commits may be identical" for both is how a `--path`
@@ -1209,11 +1214,7 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> {
1209 || "Revision description updated.".to_string(), 1214 || "Revision description updated.".to_string(),
1210 ) 1215 )
1211 } 1216 }
1212 PatchCmd::Log { 1217 PatchCmd::Log { id, timeline, json } => {
1213 id,
1214 timeline,
1215 json,
1216 } => {
1217 if timeline { 1218 if timeline {
1218 let (_, entries) = timeline::build(repo, &id)?; 1219 let (_, entries) = timeline::build(repo, &id)?;
1219 if json { 1220 if json {
@@ -1270,8 +1271,7 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> {
1270 } => { 1271 } => {
1271 let merged = patch::merge(repo, &id, commit.as_deref(), !no_close)?; 1272 let merged = patch::merge(repo, &id, commit.as_deref(), !no_close)?;
1272 let closed = merged.close == merge_scan::CloseOutcome::Closed; 1273 let closed = merged.close == merge_scan::CloseOutcome::Closed;
1273 let already = 1274 let already = matches!(merged.outcome, merge_scan::MergeOutcome::AlreadyMerged);
1274 matches!(merged.outcome, merge_scan::MergeOutcome::AlreadyMerged);
1275 report( 1275 report(
1276 json, 1276 json,
1277 || { 1277 || {
@@ -1632,7 +1632,11 @@ fn search(repo: &Repository, query: &str) -> Result<(), error::Error> {
1632 if issue.body.to_lowercase().contains(&q) { 1632 if issue.body.to_lowercase().contains(&q) {
1633 matches.push("body"); 1633 matches.push("body");
1634 } 1634 }
1635 if issue.comments.iter().any(|c| c.body.to_lowercase().contains(&q)) { 1635 if issue
1636 .comments
1637 .iter()
1638 .any(|c| c.body.to_lowercase().contains(&q))
1639 {
1636 matches.push("comment"); 1640 matches.push("comment");
1637 } 1641 }
1638 if !matches.is_empty() { 1642 if !matches.is_empty() {
@@ -1656,13 +1660,25 @@ fn search(repo: &Repository, query: &str) -> Result<(), error::Error> {
1656 if patch.body.to_lowercase().contains(&q) { 1660 if patch.body.to_lowercase().contains(&q) {
1657 matches.push("body"); 1661 matches.push("body");
1658 } 1662 }
1659 if patch.comments.iter().any(|c| c.body.to_lowercase().contains(&q)) { 1663 if patch
1664 .comments
1665 .iter()
1666 .any(|c| c.body.to_lowercase().contains(&q))
1667 {
1660 matches.push("comment"); 1668 matches.push("comment");
1661 } 1669 }
1662 if patch.reviews.iter().any(|r| r.body.to_lowercase().contains(&q)) { 1670 if patch
1671 .reviews
1672 .iter()
1673 .any(|r| r.body.to_lowercase().contains(&q))
1674 {
1663 matches.push("review"); 1675 matches.push("review");
1664 } 1676 }
1665 if patch.inline_comments.iter().any(|ic| ic.body.to_lowercase().contains(&q)) { 1677 if patch
1678 .inline_comments
1679 .iter()
1680 .any(|ic| ic.body.to_lowercase().contains(&q))
1681 {
1666 matches.push("inline comment"); 1682 matches.push("inline comment");
1667 } 1683 }
1668 if !matches.is_empty() { 1684 if !matches.is_empty() {
src/merge_scan.rs
Old New
@@ -412,7 +412,10 @@ fn collect_trailers(
412 let message = commit.message().unwrap_or(""); 412 let message = commit.message().unwrap_or("");
413 for prefix in trailer::parse_trailers(message, trailer::PATCH_TOKEN) { 413 for prefix in trailer::parse_trailers(message, trailer::PATCH_TOKEN) {
414 if seen_prefix.insert(prefix.clone()) { 414 if seen_prefix.insert(prefix.clone()) {
415 found.push(FoundTrailer { prefix, commit: oid }); 415 found.push(FoundTrailer {
416 prefix,
417 commit: oid,
418 });
416 } 419 }
417 } 420 }
418 } 421 }
src/patch.rs
Old New
@@ -152,9 +152,7 @@ fn count_unread(repo: &git2::Repository, id: &str) -> Option<usize> {
152 } 152 }
153 153
154 let mut revwalk = repo.revwalk().ok()?; 154 let mut revwalk = repo.revwalk().ok()?;
155 revwalk 155 revwalk.set_sorting(git2::Sort::TOPOLOGICAL).ok()?;
156 .set_sorting(git2::Sort::TOPOLOGICAL)
157 .ok()?;
158 revwalk.push(tip).ok()?; 156 revwalk.push(tip).ok()?;
159 revwalk.hide(seen_oid).ok()?; 157 revwalk.hide(seen_oid).ok()?;
160 Some(revwalk.count()) 158 Some(revwalk.count())
@@ -2040,14 +2038,8 @@ pub fn merge(
2040 2038
2041 let author = get_author(repo)?; 2039 let author = get_author(repo)?;
2042 let sk = signing::load_signing_key(&signing::signing_key_dir()?)?; 2040 let sk = signing::load_signing_key(&signing::signing_key_dir()?)?;
2043 let outcome = crate::merge_scan::record_merge( 2041 let outcome =
2044 repo, 2042 crate::merge_scan::record_merge(repo, &events_ref, &patch, commit_oid, &author, &sk)?;
2045 &events_ref,
2046 &patch,
2047 commit_oid,
2048 &author,
2049 &sk,
2050 )?;
2051 // Reconcile the `fixes` issue whether or not the merge was freshly 2043 // Reconcile the `fixes` issue whether or not the merge was freshly
2052 // emitted, so a close that failed the first time is retried rather than 2044 // emitted, so a close that failed the first time is retried rather than
2053 // stranded. Idempotent: an already-closed issue is left alone. 2045 // stranded. Idempotent: an already-closed issue is left alone.
src/release.rs
Old New
@@ -382,8 +382,15 @@ mod tests {
382 let dir_that_looks_like_a_file = tmp.path().join("payload.tar.gz"); 382 let dir_that_looks_like_a_file = tmp.path().join("payload.tar.gz");
383 std::fs::create_dir(&dir_that_looks_like_a_file).unwrap(); 383 std::fs::create_dir(&dir_that_looks_like_a_file).unwrap();
384 384
385 let err = 385 let err = publish(
386 publish(&repo, "origin", "v1", &[dir_that_looks_like_a_file], false, false).unwrap_err(); 386 &repo,
387 "origin",
388 "v1",
389 &[dir_that_looks_like_a_file],
390 false,
391 false,
392 )
393 .unwrap_err();
387 assert!( 394 assert!(
388 err.to_string().contains("not a regular file"), 395 err.to_string().contains("not a regular file"),
389 "got: {}", 396 "got: {}",
src/server/http/mod.rs
Old New
@@ -35,7 +35,10 @@ pub fn router(state: AppState) -> Router {
35 .route("/{repo_name}/tree", axum::routing::get(repo::tree_root)) 35 .route("/{repo_name}/tree", axum::routing::get(repo::tree_root))
36 .route("/{repo_name}/tree/{*rest}", axum::routing::get(repo::tree)) 36 .route("/{repo_name}/tree/{*rest}", axum::routing::get(repo::tree))
37 .route("/{repo_name}/blob/{*rest}", axum::routing::get(repo::blob)) 37 .route("/{repo_name}/blob/{*rest}", axum::routing::get(repo::blob))
38 .route("/{repo_name}/history/{*rest}", axum::routing::get(repo::history)) 38 .route(
39 "/{repo_name}/history/{*rest}",
40 axum::routing::get(repo::history),
41 )
39 .route("/{repo_name}/diff/{oid}", axum::routing::get(repo::diff)) 42 .route("/{repo_name}/diff/{oid}", axum::routing::get(repo::diff))
40 .route("/{repo_name}/patches", axum::routing::get(repo::patches)) 43 .route("/{repo_name}/patches", axum::routing::get(repo::patches))
41 .route( 44 .route(
src/server/http/repo/issues.rs
Old New
@@ -197,7 +197,11 @@ pub async fn issue_detail(
197 labels: is.labels.join(", "), 197 labels: is.labels.join(", "),
198 assignees: is.assignees.join(", "), 198 assignees: is.assignees.join(", "),
199 close_reason: is.close_reason, 199 close_reason: is.close_reason,
200 comments: is.comments.into_iter().map(CommentView::from_state).collect(), 200 comments: is
201 .comments
202 .into_iter()
203 .map(CommentView::from_state)
204 .collect(),
201 }; 205 };
202 206
203 IssueDetailTemplate { 207 IssueDetailTemplate {
src/server/http/repo/mod.rs
Old New
@@ -1,9 +1,9 @@
1 mod commits; 1 mod commits;
2 mod diff; 2 mod diff;
3 mod patch_diff;
4 mod history; 3 mod history;
5 mod issues; 4 mod issues;
6 mod overview; 5 mod overview;
6 mod patch_diff;
7 mod patches; 7 mod patches;
8 mod readme; 8 mod readme;
9 mod releases; 9 mod releases;
@@ -11,10 +11,10 @@ mod tree;
11 11
12 pub use commits::{commits, commits_ref}; 12 pub use commits::{commits, commits_ref};
13 pub use diff::diff; 13 pub use diff::diff;
14 pub use patch_diff::patch_diff;
15 pub use history::history; 14 pub use history::history;
16 pub use issues::{issue_detail, issues}; 15 pub use issues::{issue_detail, issues};
17 pub use overview::overview; 16 pub use overview::overview;
17 pub use patch_diff::patch_diff;
18 pub use patches::{patch_detail, patches}; 18 pub use patches::{patch_detail, patches};
19 pub use releases::{release_download, releases}; 19 pub use releases::{release_download, releases};
20 pub use tree::{blob, tree, tree_root}; 20 pub use tree::{blob, tree, tree_root};
src/server/http/repo/overview.rs
Old New
@@ -4,8 +4,9 @@ use axum::extract::{Path, State};
4 use axum::response::{IntoResponse, Response}; 4 use axum::response::{IntoResponse, Response};
5 5
6 use super::{ 6 use super::{
7 AppState, OverviewCommit, collab_counts, head_branch_name, open_repo, recent_commits, 7 collab_counts, head_branch_name, open_repo,
8 readme::{self, RenderedReadme}, 8 readme::{self, RenderedReadme},
9 recent_commits, AppState, OverviewCommit,
9 }; 10 };
10 11
11 #[derive(Debug)] 12 #[derive(Debug)]
src/server/http/repo/patches.rs
Old New
@@ -362,7 +362,11 @@ pub async fn patch_detail(
362 resolved_at: ic.resolved.as_ref().and_then(|r| r.revision), 362 resolved_at: ic.resolved.as_ref().and_then(|r| r.revision),
363 }) 363 })
364 .collect(), 364 .collect(),
365 comments: ps.comments.into_iter().map(CommentView::from_state).collect(), 365 comments: ps
366 .comments
367 .into_iter()
368 .map(CommentView::from_state)
369 .collect(),
366 }; 370 };
367 371
368 PatchDetailTemplate { 372 PatchDetailTemplate {
src/server/http/repo/readme.rs
Old New
@@ -18,11 +18,7 @@ const MAX_HTML_BYTES: usize = 2 * 1024 * 1024;
18 /// Returns `None` when no README is present, the blob is binary or invalid 18 /// Returns `None` when no README is present, the blob is binary or invalid
19 /// UTF-8, or any unexpected git2 error occurs. The README must never break 19 /// UTF-8, or any unexpected git2 error occurs. The README must never break
20 /// the overview page — failures degrade silently to "no README". 20 /// the overview page — failures degrade silently to "no README".
21 pub fn load_readme( 21 pub fn load_readme(repo: &Repository, repo_name: &str, branch: &str) -> Option<RenderedReadme> {
22 repo: &Repository,
23 repo_name: &str,
24 branch: &str,
25 ) -> Option<RenderedReadme> {
26 // Resolve the branch ref directly so this works on bare repos where HEAD 22 // Resolve the branch ref directly so this works on bare repos where HEAD
27 // may point to an unborn branch (e.g. `master` when only `main` is pushed). 23 // may point to an unborn branch (e.g. `master` when only `main` is pushed).
28 let obj = repo 24 let obj = repo
@@ -135,7 +131,11 @@ fn find_readme_entry(tree: &git2::Tree) -> Option<ReadmeEntry> {
135 Some(v) => v, 131 Some(v) => v,
136 None => continue, 132 None => continue,
137 }; 133 };
138 let candidate = ReadmeEntry { name: name.to_string(), oid: entry.id(), kind }; 134 let candidate = ReadmeEntry {
135 name: name.to_string(),
136 oid: entry.id(),
137 kind,
138 };
139 match &best { 139 match &best {
140 None => best = Some((score, candidate)), 140 None => best = Some((score, candidate)),
141 Some((cur_score, _)) if score < *cur_score => best = Some((score, candidate)), 141 Some((cur_score, _)) if score < *cur_score => best = Some((score, candidate)),
@@ -146,7 +146,7 @@ fn find_readme_entry(tree: &git2::Tree) -> Option<ReadmeEntry> {
146 } 146 }
147 147
148 fn render_markdown(src: &str) -> String { 148 fn render_markdown(src: &str) -> String {
149 use pulldown_cmark::{Options, Parser, html}; 149 use pulldown_cmark::{html, Options, Parser};
150 let mut opts = Options::empty(); 150 let mut opts = Options::empty();
151 opts.insert(Options::ENABLE_TABLES); 151 opts.insert(Options::ENABLE_TABLES);
152 opts.insert(Options::ENABLE_STRIKETHROUGH); 152 opts.insert(Options::ENABLE_STRIKETHROUGH);
@@ -196,7 +196,9 @@ mod tests {
196 use tempfile::TempDir; 196 use tempfile::TempDir;
197 197
198 fn load(repo: &Repository) -> Option<RenderedReadme> { 198 fn load(repo: &Repository) -> Option<RenderedReadme> {
199 let branch = repo.head().ok() 199 let branch = repo
200 .head()
201 .ok()
200 .and_then(|h| h.shorthand().map(String::from)) 202 .and_then(|h| h.shorthand().map(String::from))
201 .unwrap_or_else(|| "main".to_string()); 203 .unwrap_or_else(|| "main".to_string());
202 load_readme(repo, "test-repo", &branch) 204 load_readme(repo, "test-repo", &branch)
@@ -220,7 +222,8 @@ mod tests {
220 builder.write().unwrap() 222 builder.write().unwrap()
221 }; 223 };
222 let tree = repo.find_tree(tree_oid).unwrap(); 224 let tree = repo.find_tree(tree_oid).unwrap();
223 repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[]).unwrap(); 225 repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
226 .unwrap();
224 } 227 }
225 228
226 (repo, tmp) 229 (repo, tmp)
@@ -247,10 +250,7 @@ mod tests {
247 250
248 #[test] 251 #[test]
249 fn md_wins_over_txt() { 252 fn md_wins_over_txt() {
250 let (repo, _tmp) = repo_with_files(&[ 253 let (repo, _tmp) = repo_with_files(&[("README.md", b"# md\n"), ("README.txt", b"plain")]);
251 ("README.md", b"# md\n"),
252 ("README.txt", b"plain"),
253 ]);
254 let r = load(&repo).unwrap(); 254 let r = load(&repo).unwrap();
255 assert!(r.html.contains("<h1>md</h1>")); 255 assert!(r.html.contains("<h1>md</h1>"));
256 assert!(!r.html.contains("plain")); 256 assert!(!r.html.contains("plain"));
@@ -258,10 +258,8 @@ mod tests {
258 258
259 #[test] 259 #[test]
260 fn readme_wins_over_txt() { 260 fn readme_wins_over_txt() {
261 let (repo, _tmp) = repo_with_files(&[ 261 let (repo, _tmp) =
262 ("README", b"plain readme"), 262 repo_with_files(&[("README", b"plain readme"), ("README.txt", b"plain txt")]);
263 ("README.txt", b"plain txt"),
264 ]);
265 let r = load(&repo).unwrap(); 263 let r = load(&repo).unwrap();
266 assert!(r.html.contains("plain readme")); 264 assert!(r.html.contains("plain readme"));
267 assert!(!r.html.contains("plain txt")); 265 assert!(!r.html.contains("plain txt"));
@@ -269,10 +267,7 @@ mod tests {
269 267
270 #[test] 268 #[test]
271 fn mixed_case_md_still_wins_over_lowercase_txt() { 269 fn mixed_case_md_still_wins_over_lowercase_txt() {
272 let (repo, _tmp) = repo_with_files(&[ 270 let (repo, _tmp) = repo_with_files(&[("README.md", b"# md\n"), ("readme.txt", b"plain")]);
273 ("README.md", b"# md\n"),
274 ("readme.txt", b"plain"),
275 ]);
276 assert!(load(&repo).unwrap().html.contains("<h1>md</h1>")); 271 assert!(load(&repo).unwrap().html.contains("<h1>md</h1>"));
277 } 272 }
278 273
@@ -302,7 +297,8 @@ mod tests {
302 root.insert("README", root_blob, 0o100644).unwrap(); 297 root.insert("README", root_blob, 0o100644).unwrap();
303 let root_oid = root.write().unwrap(); 298 let root_oid = root.write().unwrap();
304 let tree = repo.find_tree(root_oid).unwrap(); 299 let tree = repo.find_tree(root_oid).unwrap();
305 repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[]).unwrap(); 300 repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
301 .unwrap();
306 302
307 let r = load(&repo).expect("root README must be found"); 303 let r = load(&repo).expect("root README must be found");
308 assert!(r.html.contains("actual root readme")); 304 assert!(r.html.contains("actual root readme"));
@@ -311,9 +307,7 @@ mod tests {
311 307
312 #[test] 308 #[test]
313 fn plain_readme_escapes_script_tag() { 309 fn plain_readme_escapes_script_tag() {
314 let (repo, _tmp) = repo_with_files(&[ 310 let (repo, _tmp) = repo_with_files(&[("README", b"<script>alert(1)</script>\nhello")]);
315 ("README", b"<script>alert(1)</script>\nhello"),
316 ]);
317 let r = load(&repo).unwrap(); 311 let r = load(&repo).unwrap();
318 assert!(r.html.starts_with("<pre>")); 312 assert!(r.html.starts_with("<pre>"));
319 assert!(r.html.contains("&lt;script&gt;")); 313 assert!(r.html.contains("&lt;script&gt;"));
@@ -334,7 +328,8 @@ mod tests {
334 root.insert("README.md", target_blob, 0o120000).unwrap(); 328 root.insert("README.md", target_blob, 0o120000).unwrap();
335 let root_oid = root.write().unwrap(); 329 let root_oid = root.write().unwrap();
336 let tree = repo.find_tree(root_oid).unwrap(); 330 let tree = repo.find_tree(root_oid).unwrap();
337 repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[]).unwrap(); 331 repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
332 .unwrap();
338 333
339 assert!(load(&repo).is_none()); 334 assert!(load(&repo).is_none());
340 } 335 }
@@ -343,9 +338,7 @@ mod tests {
343 fn markdown_strips_script_tag() { 338 fn markdown_strips_script_tag() {
344 // Passes because pulldown-cmark does not emit raw HTML without 339 // Passes because pulldown-cmark does not emit raw HTML without
345 // ENABLE_UNSAFE_HTML — defense-in-depth canary, not an ammonia test. 340 // ENABLE_UNSAFE_HTML — defense-in-depth canary, not an ammonia test.
346 let (repo, _tmp) = repo_with_files(&[ 341 let (repo, _tmp) = repo_with_files(&[("README.md", b"# t\n\n<script>alert(1)</script>\n")]);
347 ("README.md", b"# t\n\n<script>alert(1)</script>\n"),
348 ]);
349 let html = load(&repo).unwrap().html; 342 let html = load(&repo).unwrap().html;
350 assert!(!html.contains("<script>"), "got: {}", html); 343 assert!(!html.contains("<script>"), "got: {}", html);
351 } 344 }
@@ -354,9 +347,7 @@ mod tests {
354 fn markdown_strips_javascript_href() { 347 fn markdown_strips_javascript_href() {
355 // Passes because pulldown-cmark does not emit raw HTML without 348 // Passes because pulldown-cmark does not emit raw HTML without
356 // ENABLE_UNSAFE_HTML — defense-in-depth canary, not an ammonia test. 349 // ENABLE_UNSAFE_HTML — defense-in-depth canary, not an ammonia test.
357 let (repo, _tmp) = repo_with_files(&[ 350 let (repo, _tmp) = repo_with_files(&[("README.md", b"[click](javascript:alert(1))\n")]);
358 ("README.md", b"[click](javascript:alert(1))\n"),
359 ]);
360 let html = load(&repo).unwrap().html; 351 let html = load(&repo).unwrap().html;
361 assert!(!html.contains("javascript:"), "got: {}", html); 352 assert!(!html.contains("javascript:"), "got: {}", html);
362 } 353 }
@@ -365,9 +356,10 @@ mod tests {
365 fn markdown_strips_onerror_attribute() { 356 fn markdown_strips_onerror_attribute() {
366 // Passes because pulldown-cmark does not emit raw HTML without 357 // Passes because pulldown-cmark does not emit raw HTML without
367 // ENABLE_UNSAFE_HTML — defense-in-depth canary, not an ammonia test. 358 // ENABLE_UNSAFE_HTML — defense-in-depth canary, not an ammonia test.
368 let (repo, _tmp) = repo_with_files(&[ 359 let (repo, _tmp) = repo_with_files(&[(
369 ("README.md", b"<img src=\"https://x/y.png\" onerror=\"alert(1)\">\n"), 360 "README.md",
370 ]); 361 b"<img src=\"https://x/y.png\" onerror=\"alert(1)\">\n",
362 )]);
371 let html = load(&repo).unwrap().html; 363 let html = load(&repo).unwrap().html;
372 assert!(!html.contains("onerror"), "got: {}", html); 364 assert!(!html.contains("onerror"), "got: {}", html);
373 } 365 }
@@ -376,18 +368,18 @@ mod tests {
376 fn markdown_strips_iframe() { 368 fn markdown_strips_iframe() {
377 // Passes because pulldown-cmark does not emit raw HTML without 369 // Passes because pulldown-cmark does not emit raw HTML without
378 // ENABLE_UNSAFE_HTML — defense-in-depth canary, not an ammonia test. 370 // ENABLE_UNSAFE_HTML — defense-in-depth canary, not an ammonia test.
379 let (repo, _tmp) = repo_with_files(&[ 371 let (repo, _tmp) = repo_with_files(&[(
380 ("README.md", b"<iframe src=\"https://evil.example/\"></iframe>\n"), 372 "README.md",
381 ]); 373 b"<iframe src=\"https://evil.example/\"></iframe>\n",
374 )]);
382 let html = load(&repo).unwrap().html; 375 let html = load(&repo).unwrap().html;
383 assert!(!html.contains("<iframe"), "got: {}", html); 376 assert!(!html.contains("<iframe"), "got: {}", html);
384 } 377 }
385 378
386 #[test] 379 #[test]
387 fn markdown_strips_data_image_uri() { 380 fn markdown_strips_data_image_uri() {
388 let (repo, _tmp) = repo_with_files(&[ 381 let (repo, _tmp) =
389 ("README.md", b"<img src=\"data:image/png;base64,AAAA\">\n"), 382 repo_with_files(&[("README.md", b"<img src=\"data:image/png;base64,AAAA\">\n")]);
390 ]);
391 let html = load(&repo).unwrap().html; 383 let html = load(&repo).unwrap().html;
392 // Either the whole <img> is dropped or the src attr is gone. 384 // Either the whole <img> is dropped or the src attr is gone.
393 assert!(!html.contains("data:"), "got: {}", html); 385 assert!(!html.contains("data:"), "got: {}", html);
@@ -423,7 +415,12 @@ mod tests {
423 for _ in 0..100_000usize { 415 for _ in 0..100_000usize {
424 src.push_str("`a`\n\n"); 416 src.push_str("`a`\n\n");
425 } 417 }
426 assert!(src.len() < MAX_BLOB_BYTES, "source {} >= blob cap {}", src.len(), MAX_BLOB_BYTES); 418 assert!(
419 src.len() < MAX_BLOB_BYTES,
420 "source {} >= blob cap {}",
421 src.len(),
422 MAX_BLOB_BYTES
423 );
427 let (repo, _tmp) = repo_with_files(&[("README.md", src.as_bytes())]); 424 let (repo, _tmp) = repo_with_files(&[("README.md", src.as_bytes())]);
428 let r = load(&repo).unwrap(); 425 let r = load(&repo).unwrap();
429 assert!(r.html.contains("too large"), "expected bomb to trip cap"); 426 assert!(r.html.contains("too large"), "expected bomb to trip cap");
@@ -435,7 +432,10 @@ mod tests {
435 let bytes = vec![b'&'; 500 * 1024]; 432 let bytes = vec![b'&'; 500 * 1024];
436 let (repo, _tmp) = repo_with_files(&[("README", &bytes)]); 433 let (repo, _tmp) = repo_with_files(&[("README", &bytes)]);
437 let r = load(&repo).unwrap(); 434 let r = load(&repo).unwrap();
438 assert!(r.html.contains("too large"), "expected plain-text bomb to trip cap"); 435 assert!(
436 r.html.contains("too large"),
437 "expected plain-text bomb to trip cap"
438 );
439 } 439 }
440 440
441 #[test] 441 #[test]
src/server/ssh/auth.rs
Old New
@@ -20,7 +20,11 @@ pub fn parse_authorized_keys(content: &str) -> Vec<AuthorizedKey> {
20 let key_type = parts.next()?.to_string(); 20 let key_type = parts.next()?.to_string();
21 let key_data = parts.next()?.to_string(); 21 let key_data = parts.next()?.to_string();
22 let comment = parts.next().map(|s| s.to_string()); 22 let comment = parts.next().map(|s| s.to_string());
23 Some(AuthorizedKey { key_type, key_data, comment }) 23 Some(AuthorizedKey {
24 key_type,
25 key_data,
26 comment,
27 })
24 }) 28 })
25 .collect() 29 .collect()
26 } 30 }
@@ -36,8 +40,8 @@ fn is_rsa_key_type(key_type: &str) -> bool {
36 40
37 pub fn is_authorized(keys: &[AuthorizedKey], key_type: &str, key_data: &str) -> bool { 41 pub fn is_authorized(keys: &[AuthorizedKey], key_type: &str, key_data: &str) -> bool {
38 keys.iter().any(|k| { 42 keys.iter().any(|k| {
39 let type_matches = k.key_type == key_type 43 let type_matches =
40 || (is_rsa_key_type(&k.key_type) && is_rsa_key_type(key_type)); 44 k.key_type == key_type || (is_rsa_key_type(&k.key_type) && is_rsa_key_type(key_type));
41 type_matches && k.key_data == key_data 45 type_matches && k.key_data == key_data
42 }) 46 })
43 } 47 }
src/signing.rs
Old New
@@ -356,7 +356,10 @@ mod tests {
356 let keys: Vec<&String> = map.keys().collect(); 356 let keys: Vec<&String> = map.keys().collect();
357 let mut sorted = keys.clone(); 357 let mut sorted = keys.clone();
358 sorted.sort(); 358 sorted.sort();
359 assert_eq!(keys, sorted, "top-level JSON keys must be alphabetically sorted"); 359 assert_eq!(
360 keys, sorted,
361 "top-level JSON keys must be alphabetically sorted"
362 );
360 } else { 363 } else {
361 panic!("expected JSON object"); 364 panic!("expected JSON object");
362 } 365 }
src/state.rs
Old New
@@ -95,7 +95,13 @@ impl BodyOverrides {
95 /// Record a correction, keeping the one that wins on `(clock, oid)`. 95 /// Record a correction, keeping the one that wins on `(clock, oid)`.
96 /// `>=` matches the status fold: later clock wins, and on a tie the 96 /// `>=` matches the status fold: later clock wins, and on a tie the
97 /// lexicographically larger OID does. 97 /// lexicographically larger OID does.
98 fn record(&mut self, target: String, key: (u64, String), author: &Author, body: Option<String>) { 98 fn record(
99 &mut self,
100 target: String,
101 key: (u64, String),
102 author: &Author,
103 body: Option<String>,
104 ) {
99 let candidate = BodyOverride { 105 let candidate = BodyOverride {
100 key, 106 key,
101 author_email: author.email.clone(), 107 author_email: author.email.clone(),
@@ -1606,10 +1612,7 @@ impl PatchState {
1606 new_body.clone_into(&mut r.body); 1612 new_body.clone_into(&mut r.body);
1607 r.edited = true; 1613 r.edited = true;
1608 } 1614 }
1609 } else if let Some(rev) = s 1615 } else if let Some(rev) = s.revisions.iter_mut().find(|rev| rev.event_id == *target)
1610 .revisions
1611 .iter_mut()
1612 .find(|rev| rev.event_id == *target)
1613 { 1616 {
1614 // Same rule: the commit and tree a revision points at are 1617 // Same rule: the commit and tree a revision points at are
1615 // immutable, the description is not. 1618 // immutable, the description is not.
@@ -1756,7 +1759,10 @@ fn patch_refs_under(
1756 // A revision ref, not a patch. 1759 // A revision ref, not a patch.
1757 Some(_) => continue, 1760 Some(_) => continue,
1758 // The older layout: no suffix at all, the ref is the DAG. 1761 // The older layout: no suffix at all, the ref is the DAG.
1759 None => ref_name.strip_prefix(prefix).unwrap_or_default().to_string(), 1762 None => ref_name
1763 .strip_prefix(prefix)
1764 .unwrap_or_default()
1765 .to_string(),
1760 }; 1766 };
1761 if id.is_empty() { 1767 if id.is_empty() {
1762 continue; 1768 continue;
@@ -1920,7 +1926,11 @@ fn all_ids(repo: &Repository, kind: &str) -> Result<Vec<String>, crate::error::E
1920 .into_iter() 1926 .into_iter()
1921 .map(|(_, id)| id) 1927 .map(|(_, id)| id)
1922 .collect(); 1928 .collect();
1923 ids.extend(collab_archive_refs(repo, kind)?.into_iter().map(|(_, id)| id)); 1929 ids.extend(
1930 collab_archive_refs(repo, kind)?
1931 .into_iter()
1932 .map(|(_, id)| id),
1933 );
1924 ids.sort_unstable(); 1934 ids.sort_unstable();
1925 ids.dedup(); 1935 ids.dedup();
1926 Ok(ids) 1936 Ok(ids)
src/sync.rs
Old New
@@ -354,7 +354,8 @@ fn print_refname_conflict_advice(remote: &str, ids: &[String]) {
354 "\nThat deletes only {} listed above, and only on '{}'. Nothing local is\n\ 354 "\nThat deletes only {} listed above, and only on '{}'. Nothing local is\n\
355 touched: the next sync republishes the same events under `<id>/events`,\n\ 355 touched: the next sync republishes the same events under `<id>/events`,\n\
356 which is where every current version reads them from.", 356 which is where every current version reads them from.",
357 refs_above, remote 357 refs_above,
358 remote
358 ); 359 );
359 } 360 }
360 361
@@ -368,7 +369,8 @@ fn print_sync_summary(result: &SyncResult) {
368 } else { 369 } else {
369 errln!( 370 errln!(
370 "\nSync partially failed: {} of {} refs pushed.", 371 "\nSync partially failed: {} of {} refs pushed.",
371 succeeded, total 372 succeeded,
373 total
372 ); 374 );
373 } 375 }
374 376
@@ -411,7 +413,8 @@ fn print_sync_summary(result: &SyncResult) {
411 if retryable > 0 { 413 if retryable > 0 {
412 errln!( 414 errln!(
413 "\nRun `git-collab sync --remote {}` again to retry {} failed ref(s).", 415 "\nRun `git-collab sync --remote {}` again to retry {} failed ref(s).",
414 result.remote, retryable 416 result.remote,
417 retryable
415 ); 418 );
416 } 419 }
417 } 420 }
src/timeline.rs
Old New
@@ -413,7 +413,9 @@ fn authorized(
413 target: &str, 413 target: &str,
414 author: &Author, 414 author: &Author,
415 ) -> bool { 415 ) -> bool {
416 owners.get(target).is_some_and(|owner| *owner == author.email) 416 owners
417 .get(target)
418 .is_some_and(|owner| *owner == author.email)
417 } 419 }
418 420
419 /// Render the timeline as one line per event. 421 /// Render the timeline as one line per event.
@@ -485,9 +487,7 @@ pub fn to_writer(entries: &[Entry], writer: &mut dyn std::io::Write) -> Result<(
485 | Kind::Resolved { target } 487 | Kind::Resolved { target }
486 | Kind::CommentUnresolved { target } => parts.push(format!("{:.8}", target)), 488 | Kind::CommentUnresolved { target } => parts.push(format!("{:.8}", target)),
487 Kind::Closed { reason } => parts.extend(reason.clone()), 489 Kind::Closed { reason } => parts.extend(reason.clone()),
488 Kind::Merged { commit } => { 490 Kind::Merged { commit } => parts.extend(commit.as_deref().map(|c| format!("{:.8}", c))),
489 parts.extend(commit.as_deref().map(|c| format!("{:.8}", c)))
490 }
491 Kind::Reopened => {} 491 Kind::Reopened => {}
492 } 492 }
493 493
src/tui/events.rs
Old New
@@ -399,7 +399,10 @@ fn submit_comment(
399 return; 399 return;
400 }; 400 };
401 let short = app.patch_abbrev.of(&patch.id).to_string(); 401 let short = app.patch_abbrev.of(&patch.id).to_string();
402 let revision = patch.revisions.get(app.patch_revision_idx).map(|r| r.number); 402 let revision = patch
403 .revisions
404 .get(app.patch_revision_idx)
405 .map(|r| r.number);
403 406
404 // Inline when the cursor is on a line of the diff, on the thread 407 // Inline when the cursor is on a line of the diff, on the thread
405 // otherwise. Both go through `patch::comment`, which is the CLI's own 408 // otherwise. Both go through `patch::comment`, which is the CLI's own
@@ -457,7 +460,10 @@ fn submit_review(
457 return; 460 return;
458 }; 461 };
459 let short = app.patch_abbrev.of(&patch.id).to_string(); 462 let short = app.patch_abbrev.of(&patch.id).to_string();
460 let revision = patch.revisions.get(app.patch_revision_idx).map(|r| r.number); 463 let revision = patch
464 .revisions
465 .get(app.patch_revision_idx)
466 .map(|r| r.number);
461 let seed = comment_seed( 467 let seed = comment_seed(
462 &format!( 468 &format!(
463 "Review of patch {}: {}\n# Verdict: {}", 469 "Review of patch {}: {}\n# Verdict: {}",
@@ -488,8 +494,7 @@ fn toggle_resolve(app: &mut App, repo: &Repository) {
488 return; 494 return;
489 }; 495 };
490 let RowTarget::Comment { oid, resolved } = app.cursor_target() else { 496 let RowTarget::Comment { oid, resolved } = app.cursor_target() else {
491 app.status_msg = 497 app.status_msg = Some("Move to an inline comment to mark it answered (j/k).".to_string());
492 Some("Move to an inline comment to mark it answered (j/k).".to_string());
493 return; 498 return;
494 }; 499 };
495 let outcome = if resolved { 500 let outcome = if resolved {
@@ -604,8 +609,12 @@ fn open_patch_detail(app: &mut App, repo: &Repository, patch: crate::state::Patc
604 609
605 fn regenerate_patch_diff(app: &mut App, repo: &Repository) { 610 fn regenerate_patch_diff(app: &mut App, repo: &Repository) {
606 if let Some(ref patch) = app.current_patch { 611 if let Some(ref patch) = app.current_patch {
607 match generate_patch_diff_for(repo, patch, app.patch_revision_idx, app.patch_interdiff_mode) 612 match generate_patch_diff_for(
608 { 613 repo,
614 patch,
615 app.patch_revision_idx,
616 app.patch_interdiff_mode,
617 ) {
609 Ok(rows) => app.patch_diff = rows, 618 Ok(rows) => app.patch_diff = rows,
610 Err(e) => app.set_diff_text(&format!("(error generating diff: {})", e)), 619 Err(e) => app.set_diff_text(&format!("(error generating diff: {})", e)),
611 } 620 }
@@ -631,14 +640,20 @@ mod tests {
631 /// comment made of hash marks. 640 /// comment made of hash marks.
632 #[test] 641 #[test]
633 fn an_untouched_buffer_is_blank() { 642 fn an_untouched_buffer_is_blank() {
634 let seed = comment_seed("Commenting on src/lib.rs:3", vec![" let x = 1;".to_string()]); 643 let seed = comment_seed(
644 "Commenting on src/lib.rs:3",
645 vec![" let x = 1;".to_string()],
646 );
635 assert!(strip_comments(&seed).trim().is_empty()); 647 assert!(strip_comments(&seed).trim().is_empty());
636 } 648 }
637 649
638 /// Only a leading `#` is a comment marker; a `#` inside a line is prose. 650 /// Only a leading `#` is a comment marker; a `#` inside a line is prose.
639 #[test] 651 #[test]
640 fn a_hash_inside_a_line_survives() { 652 fn a_hash_inside_a_line_survives() {
641 assert_eq!(strip_comments("issue #12 is the same bug"), "issue #12 is the same bug"); 653 assert_eq!(
654 strip_comments("issue #12 is the same bug"),
655 "issue #12 is the same bug"
656 );
642 } 657 }
643 658
644 /// The buffer says what is being written about and where the cursor was, 659 /// The buffer says what is being written about and where the cursor was,
src/tui/keys.rs
Old New
@@ -405,7 +405,12 @@ const ISSUE_VERBS: &[Entry] = &[
405 e(&[k('/')], Action::BeginSearch, "search", true), 405 e(&[k('/')], Action::BeginSearch, "search", true),
406 e(&[k('r')], Action::Reload, "refresh", true), 406 e(&[k('r')], Action::Reload, "refresh", true),
407 e(&[k('n')], Action::BeginCreateIssue, "new issue", true), 407 e(&[k('n')], Action::BeginCreateIssue, "new issue", true),
408 e(&[k('o')], Action::Checkout, "check out the linked patch", false), 408 e(
409 &[k('o')],
410 Action::Checkout,
411 "check out the linked patch",
412 false,
413 ),
409 e( 414 e(
410 &[c(KeyCode::PageDown)], 415 &[c(KeyCode::PageDown)],
411 Action::ScrollPageDown, 416 Action::ScrollPageDown,
@@ -501,14 +506,24 @@ const PATCH_DETAIL: &[Entry] = &[
501 e(&[k('o')], Action::Checkout, "checkout", true), 506 e(&[k('o')], Action::Checkout, "checkout", true),
502 e(&[k('w')], Action::ToggleWrap, "wrap", true), 507 e(&[k('w')], Action::ToggleWrap, "wrap", true),
503 e(&[ctrl('e')], Action::ScrollRowDown, "scroll a row", false), 508 e(&[ctrl('e')], Action::ScrollRowDown, "scroll a row", false),
504 e(&[ctrl('y')], Action::ScrollRowUp, "scroll a row back", false), 509 e(
510 &[ctrl('y')],
511 Action::ScrollRowUp,
512 "scroll a row back",
513 false,
514 ),
505 e( 515 e(
506 &[c(KeyCode::PageDown)], 516 &[c(KeyCode::PageDown)],
507 Action::CursorPageDown, 517 Action::CursorPageDown,
508 "page down", 518 "page down",
509 false, 519 false,
510 ), 520 ),
511 e(&[c(KeyCode::PageUp)], Action::CursorPageUp, "page up", false), 521 e(
522 &[c(KeyCode::PageUp)],
523 Action::CursorPageUp,
524 "page up",
525 false,
526 ),
512 ]; 527 ];
513 528
514 const EVENT_HISTORY: &[Entry] = &[ 529 const EVENT_HISTORY: &[Entry] = &[
@@ -538,7 +553,12 @@ const EVENT_DETAIL: &[Entry] = &[
538 "page down", 553 "page down",
539 false, 554 false,
540 ), 555 ),
541 e(&[c(KeyCode::PageUp)], Action::ScrollPageUp, "page up", false), 556 e(
557 &[c(KeyCode::PageUp)],
558 Action::ScrollPageUp,
559 "page up",
560 false,
561 ),
542 ]; 562 ];
543 563
544 const REVIEW_VERDICT: &[Entry] = &[ 564 const REVIEW_VERDICT: &[Entry] = &[
src/tui/mod.rs
Old New
@@ -1739,9 +1739,7 @@ mod tests {
1739 fn logical_index_of_diff_line(app: &App, line: u32) -> usize { 1739 fn logical_index_of_diff_line(app: &App, line: u32) -> usize {
1740 app.patch_lines 1740 app.patch_lines
1741 .iter() 1741 .iter()
1742 .position(|r| { 1742 .position(|r| matches!(&r.target, RowTarget::Diff { line: l, .. } if *l == line))
1743 matches!(&r.target, RowTarget::Diff { line: l, .. } if *l == line)
1744 })
1745 .expect("a diff line with that anchor") 1743 .expect("a diff line with that anchor")
1746 } 1744 }
1747 1745
@@ -1861,8 +1859,7 @@ mod tests {
1861 1859
1862 let highlighted = (1..buf.area.height - 1) 1860 let highlighted = (1..buf.area.height - 1)
1863 .filter(|y| { 1861 .filter(|y| {
1864 (0..buf.area.width) 1862 (0..buf.area.width).any(|x| buf.cell((x, *y)).unwrap().bg == Color::DarkGray)
1865 .any(|x| buf.cell((x, *y)).unwrap().bg == Color::DarkGray)
1866 }) 1863 })
1867 .count(); 1864 .count();
1868 assert_eq!( 1865 assert_eq!(
@@ -2110,7 +2107,9 @@ mod tests {
2110 ] 2107 ]
2111 ); 2108 );
2112 assert!( 2109 assert!(
2113 app.patch_lines.iter().all(|r| !r.line.to_string().contains('\n')), 2110 app.patch_lines
2111 .iter()
2112 .all(|r| !r.line.to_string().contains('\n')),
2114 "a rendered line still holds a newline" 2113 "a rendered line still holds a newline"
2115 ); 2114 );
2116 } 2115 }
@@ -2635,7 +2634,11 @@ mod tests {
2635 crossterm::event::KeyModifiers::empty(), 2634 crossterm::event::KeyModifiers::empty(),
2636 ); 2635 );
2637 let text = buffer_to_string(&render_app(&mut app)); 2636 let text = buffer_to_string(&render_app(&mut app));
2638 assert!(text.contains("Ctrl-e"), "no Ctrl-e in the overlay:\n{}", text); 2637 assert!(
2638 text.contains("Ctrl-e"),
2639 "no Ctrl-e in the overlay:\n{}",
2640 text
2641 );
2639 assert!( 2642 assert!(
2640 text.contains("Patch detail"), 2643 text.contains("Patch detail"),
2641 "the overlay does not say which pane it is about:\n{}", 2644 "the overlay does not say which pane it is about:\n{}",
src/tui/state.rs
Old New
@@ -599,14 +599,16 @@ impl App {
599 let len = self.event_history.len(); 599 let len = self.event_history.len();
600 if len > 0 { 600 if len > 0 {
601 let current = self.event_list_state.selected().unwrap_or(0); 601 let current = self.event_list_state.selected().unwrap_or(0);
602 self.event_list_state.select(Some((current + 1).min(len - 1))); 602 self.event_list_state
603 .select(Some((current + 1).min(len - 1)));
603 } 604 }
604 KeyAction::Continue 605 KeyAction::Continue
605 } 606 }
606 Action::EventPrev => { 607 Action::EventPrev => {
607 if !self.event_history.is_empty() { 608 if !self.event_history.is_empty() {
608 let current = self.event_list_state.selected().unwrap_or(0); 609 let current = self.event_list_state.selected().unwrap_or(0);
609 self.event_list_state.select(Some(current.saturating_sub(1))); 610 self.event_list_state
611 .select(Some(current.saturating_sub(1)));
610 } 612 }
611 KeyAction::Continue 613 KeyAction::Continue
612 } 614 }
@@ -638,8 +640,7 @@ impl App {
638 if self.linked_patch_for_selected().is_some() { 640 if self.linked_patch_for_selected().is_some() {
639 KeyAction::OpenPatchDetail 641 KeyAction::OpenPatchDetail
640 } else { 642 } else {
641 self.status_msg = 643 self.status_msg = Some("No patch is linked to this issue yet.".to_string());
642 Some("No patch is linked to this issue yet.".to_string());
643 KeyAction::Continue 644 KeyAction::Continue
644 } 645 }
645 } 646 }
src/tui/widgets.rs
Old New
@@ -340,7 +340,11 @@ impl Rows {
340 /// scroll stays a row offset. A width of zero means the pane has not been drawn 340 /// scroll stays a row offset. A width of zero means the pane has not been drawn
341 /// yet and nothing is known about how much room there is, so the lines pass 341 /// yet and nothing is known about how much room there is, so the lines pass
342 /// through one for one. 342 /// through one for one.
343 pub(crate) fn wrap_detail_rows(lines: &[DetailRow], width: u16, wrap_diff: bool) -> Vec<DisplayRow> { 343 pub(crate) fn wrap_detail_rows(
344 lines: &[DetailRow],
345 width: u16,
346 wrap_diff: bool,
347 ) -> Vec<DisplayRow> {
344 let width = width as usize; 348 let width = width as usize;
345 let mut out = Vec::with_capacity(lines.len()); 349 let mut out = Vec::with_capacity(lines.len());
346 for (logical, row) in lines.iter().enumerate() { 350 for (logical, row) in lines.iter().enumerate() {
@@ -494,8 +498,7 @@ fn render_help(frame: &mut Frame, app: &App, area: Rect) {
494 .max() 498 .max()
495 .unwrap_or(0); 499 .unwrap_or(0);
496 // ` <keys> <label>` inside a border on each side. 500 // ` <keys> <label>` inside a border on each side.
497 let width = (keys_width + label_width + 4) 501 let width = (keys_width + label_width + 4).max(context.title().chars().count() + 9) as u16;
498 .max(context.title().chars().count() + 9) as u16;
499 let width = width.min(area.width); 502 let width = width.min(area.width);
500 let height = (rows.len() as u16 + 2).min(area.height); 503 let height = (rows.len() as u16 + 2).min(area.height);
501 504
tests/alias_test.rs
Old New
@@ -17,8 +17,8 @@ use git_collab::cli::Cli;
17 fn parse(args: &[&str]) -> String { 17 fn parse(args: &[&str]) -> String {
18 let mut argv = vec!["git-collab"]; 18 let mut argv = vec!["git-collab"];
19 argv.extend_from_slice(args); 19 argv.extend_from_slice(args);
20 let cli = Cli::try_parse_from(&argv) 20 let cli =
21 .unwrap_or_else(|e| panic!("failed to parse {:?}: {}", args, e)); 21 Cli::try_parse_from(&argv).unwrap_or_else(|e| panic!("failed to parse {:?}: {}", args, e));
22 format!("{:?}", cli.command) 22 format!("{:?}", cli.command)
23 } 23 }
24 24
@@ -85,7 +85,10 @@ fn key_generate_reaches_the_same_generator_as_init_key() {
85 #[test] 85 #[test]
86 fn new_is_a_synonym_for_open_and_create() { 86 fn new_is_a_synonym_for_open_and_create() {
87 assert_same(&["issue", "new", "-t", "t"], &["issue", "open", "-t", "t"]); 87 assert_same(&["issue", "new", "-t", "t"], &["issue", "open", "-t", "t"]);
88 assert_same(&["patch", "new", "-t", "t"], &["patch", "create", "-t", "t"]); 88 assert_same(
89 &["patch", "new", "-t", "t"],
90 &["patch", "create", "-t", "t"],
91 );
89 } 92 }
90 93
91 #[test] 94 #[test]
tests/body_edit_test.rs
Old New
@@ -118,7 +118,14 @@ fn a_patch_thread_comment_can_be_corrected() {
118 let json = repo.run_ok(&["patch", "show", &id, "--json"]); 118 let json = repo.run_ok(&["patch", "show", &id, "--json"]);
119 let comment_id = first_comment_id(&json, "comments"); 119 let comment_id = first_comment_id(&json, "comments");
120 120
121 repo.run_ok(&["patch", "edit-comment", &id, &comment_id[..8], "-b", "right"]); 121 repo.run_ok(&[
122 "patch",
123 "edit-comment",
124 &id,
125 &comment_id[..8],
126 "-b",
127 "right",
128 ]);
122 129
123 let json = repo.run_ok(&["patch", "show", &id, "--json"]); 130 let json = repo.run_ok(&["patch", "show", &id, "--json"]);
124 assert_eq!(body_at(&json, "/comments/0/body"), "right"); 131 assert_eq!(body_at(&json, "/comments/0/body"), "right");
@@ -145,7 +152,14 @@ fn an_inline_comment_can_be_corrected_without_moving() {
145 let json = repo.run_ok(&["patch", "show", &id, "--json"]); 152 let json = repo.run_ok(&["patch", "show", &id, "--json"]);
146 let comment_id = first_comment_id(&json, "inline_comments"); 153 let comment_id = first_comment_id(&json, "inline_comments");
147 154
148 repo.run_ok(&["patch", "edit-comment", &id, &comment_id[..8], "-b", "right"]); 155 repo.run_ok(&[
156 "patch",
157 "edit-comment",
158 &id,
159 &comment_id[..8],
160 "-b",
161 "right",
162 ]);
149 163
150 let json = repo.run_ok(&["patch", "show", &id, "--json"]); 164 let json = repo.run_ok(&["patch", "show", &id, "--json"]);
151 assert_eq!(body_at(&json, "/inline_comments/0/body"), "right"); 165 assert_eq!(body_at(&json, "/inline_comments/0/body"), "right");
@@ -231,10 +245,7 @@ fn editing_with_no_body_opens_the_editor_seeded_with_the_current_text() {
231 "#!/bin/sh\nprintf '%s and more' \"$(cat \"$1\")\" > \"$1\"\n", 245 "#!/bin/sh\nprintf '%s and more' \"$(cat \"$1\")\" > \"$1\"\n",
232 ); 246 );
233 247
234 repo.run_in_pty( 248 repo.run_in_pty(&["issue", "edit-comment", &id, &comment_id[..8]], &editor);
235 &["issue", "edit-comment", &id, &comment_id[..8]],
236 &editor,
237 );
238 249
239 let json = repo.run_ok(&["issue", "show", &id, "--json"]); 250 let json = repo.run_ok(&["issue", "show", &id, "--json"]);
240 assert_eq!(body_at(&json, "/comments/0/body"), "seed me and more"); 251 assert_eq!(body_at(&json, "/comments/0/body"), "seed me and more");
@@ -266,11 +277,20 @@ fn a_deleted_comment_leaves_a_tombstone_in_place() {
266 let id = repo.patch_create("tombstone"); 277 let id = repo.patch_create("tombstone");
267 repo.run_ok(&["patch", "comment", &id, "-b", "first"]); 278 repo.run_ok(&["patch", "comment", &id, "-b", "first"]);
268 repo.run_ok(&["patch", "comment", &id, "-b", "regrettable probe"]); 279 repo.run_ok(&["patch", "comment", &id, "-b", "regrettable probe"]);
269 repo.run_ok(&["patch", "comment", &id, "-b", "third, replying to the above"]); 280 repo.run_ok(&[
281 "patch",
282 "comment",
283 &id,
284 "-b",
285 "third, replying to the above",
286 ]);
270 287
271 let json = repo.run_ok(&["patch", "show", &id, "--json"]); 288 let json = repo.run_ok(&["patch", "show", &id, "--json"]);
272 let value: serde_json::Value = serde_json::from_str(&json).unwrap(); 289 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
273 let target = value["comments"][1]["commit_id"].as_str().unwrap().to_string(); 290 let target = value["comments"][1]["commit_id"]
291 .as_str()
292 .unwrap()
293 .to_string();
274 294
275 repo.run_ok(&["patch", "delete-comment", &id, &target[..8]]); 295 repo.run_ok(&["patch", "delete-comment", &id, &target[..8]]);
276 296
@@ -465,7 +485,10 @@ fn an_edited_comment_keeps_its_position() {
465 485
466 let json = repo.run_ok(&["issue", "show", &id, "--json"]); 486 let json = repo.run_ok(&["issue", "show", &id, "--json"]);
467 let value: serde_json::Value = serde_json::from_str(&json).unwrap(); 487 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
468 let target = value["comments"][0]["commit_id"].as_str().unwrap().to_string(); 488 let target = value["comments"][0]["commit_id"]
489 .as_str()
490 .unwrap()
491 .to_string();
469 492
470 repo.run_ok(&["issue", "edit-comment", &id, &target[..8], "-b", "FIRST"]); 493 repo.run_ok(&["issue", "edit-comment", &id, &target[..8], "-b", "FIRST"]);
471 494
@@ -508,7 +531,8 @@ fn concurrent_edits_of_one_comment_converge() {
508 531
509 // Her other clone, offline, edits the same comment from the fork point. 532 // Her other clone, offline, edits the same comment from the fork point.
510 let branch_ref = "refs/collab/issues/other-clone"; 533 let branch_ref = "refs/collab/issues/other-clone";
511 repo.reference(branch_ref, fork_point, false, "fork").unwrap(); 534 repo.reference(branch_ref, fork_point, false, "fork")
535 .unwrap();
512 let b_edit = append( 536 let b_edit = append(
513 &repo, 537 &repo,
514 branch_ref, 538 branch_ref,
@@ -566,7 +590,8 @@ fn a_delete_and_an_edit_racing_converge() {
566 let a_tip = repo.refname_to_id(&ref_name).unwrap(); 590 let a_tip = repo.refname_to_id(&ref_name).unwrap();
567 591
568 let branch_ref = "refs/collab/issues/delete-clone"; 592 let branch_ref = "refs/collab/issues/delete-clone";
569 repo.reference(branch_ref, fork_point, false, "fork").unwrap(); 593 repo.reference(branch_ref, fork_point, false, "fork")
594 .unwrap();
570 let delete_oid = append( 595 let delete_oid = append(
571 &repo, 596 &repo,
572 branch_ref, 597 branch_ref,
@@ -583,7 +608,8 @@ fn a_delete_and_an_edit_racing_converge() {
583 let one = issue_state(&repo, merged_ref, &id); 608 let one = issue_state(&repo, merged_ref, &id);
584 609
585 let other_ref = "refs/collab/issues/dm-other"; 610 let other_ref = "refs/collab/issues/dm-other";
586 repo.reference(other_ref, delete_oid, false, "copy").unwrap(); 611 repo.reference(other_ref, delete_oid, false, "copy")
612 .unwrap();
587 dag::reconcile(&repo, other_ref, &ref_name, &alice(), &sk).unwrap(); 613 dag::reconcile(&repo, other_ref, &ref_name, &alice(), &sk).unwrap();
588 let two = issue_state(&repo, other_ref, &id); 614 let two = issue_state(&repo, other_ref, &id);
589 615
@@ -610,7 +636,10 @@ fn a_revision_body_can_be_corrected() {
610 636
611 let json = repo.run_ok(&["patch", "show", &id, "--json"]); 637 let json = repo.run_ok(&["patch", "show", &id, "--json"]);
612 let value: serde_json::Value = serde_json::from_str(&json).unwrap(); 638 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
613 let commit_before = value["revisions"][1]["commit"].as_str().unwrap().to_string(); 639 let commit_before = value["revisions"][1]["commit"]
640 .as_str()
641 .unwrap()
642 .to_string();
614 assert_eq!(value["revisions"][1]["body"], "test"); 643 assert_eq!(value["revisions"][1]["body"], "test");
615 644
616 repo.run_ok(&[ 645 repo.run_ok(&[
@@ -703,7 +732,11 @@ fn reading_a_patch_never_moves_its_event_ref() {
703 repo.run_ok(&["patch", "delete-comment", &id, &comment_id[..8]]); 732 repo.run_ok(&["patch", "delete-comment", &id, &comment_id[..8]]);
704 733
705 let events_ref = repo 734 let events_ref = repo
706 .git(&["for-each-ref", "--format=%(refname)", "refs/collab/patches/"]) 735 .git(&[
736 "for-each-ref",
737 "--format=%(refname)",
738 "refs/collab/patches/",
739 ])
707 .lines() 740 .lines()
708 .find(|l| l.ends_with("/events")) 741 .find(|l| l.ends_with("/events"))
709 .expect("patch events ref") 742 .expect("patch events ref")
@@ -802,7 +835,14 @@ fn an_edited_comment_is_marked_as_edited() {
802 835
803 let json = repo.run_ok(&["issue", "show", &id, "--json"]); 836 let json = repo.run_ok(&["issue", "show", &id, "--json"]);
804 let comment_id = first_comment_id(&json, "comments"); 837 let comment_id = first_comment_id(&json, "comments");
805 repo.run_ok(&["issue", "edit-comment", &id, &comment_id[..8], "-b", "after"]); 838 repo.run_ok(&[
839 "issue",
840 "edit-comment",
841 &id,
842 &comment_id[..8],
843 "-b",
844 "after",
845 ]);
806 846
807 let out = repo.run_ok(&["issue", "show", &id]); 847 let out = repo.run_ok(&["issue", "show", &id]);
808 assert!( 848 assert!(
tests/body_input_test.rs
Old New
@@ -66,7 +66,10 @@ fn body_file_has_a_long_form_too() {
66 ]); 66 ]);
67 67
68 let json = repo.run_ok(&["issue", "show", &id, "--json"]); 68 let json = repo.run_ok(&["issue", "show", &id, "--json"]);
69 assert_eq!(json_field(&json, "/comments/0/body"), "from the long form\n"); 69 assert_eq!(
70 json_field(&json, "/comments/0/body"),
71 "from the long form\n"
72 );
70 } 73 }
71 74
72 #[test] 75 #[test]
@@ -157,7 +160,11 @@ fn stdin_body_survives_on_every_command_that_takes_one() {
157 &["issue", "open", "-t", "opened from stdin", "-F", "-"], 160 &["issue", "open", "-t", "opened from stdin", "-F", "-"],
158 NASTY.as_bytes(), 161 NASTY.as_bytes(),
159 ); 162 );
160 let issue_id = out.trim().strip_prefix("Opened issue ").unwrap().to_string(); 163 let issue_id = out
164 .trim()
165 .strip_prefix("Opened issue ")
166 .unwrap()
167 .to_string();
161 let json = repo.run_ok(&["issue", "show", &issue_id, "--json"]); 168 let json = repo.run_ok(&["issue", "show", &issue_id, "--json"]);
162 assert_eq!(json_field(&json, "/body"), NASTY, "issue open"); 169 assert_eq!(json_field(&json, "/body"), NASTY, "issue open");
163 170
@@ -186,7 +193,11 @@ fn stdin_body_survives_on_every_command_that_takes_one() {
186 NASTY.as_bytes(), 193 NASTY.as_bytes(),
187 ); 194 );
188 repo.git(&["checkout", "main"]); 195 repo.git(&["checkout", "main"]);
189 let patch_id = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 196 let patch_id = out
197 .trim()
198 .strip_prefix("Created patch ")
199 .unwrap()
200 .to_string();
190 let json = repo.run_ok(&["patch", "show", &patch_id, "--json"]); 201 let json = repo.run_ok(&["patch", "show", &patch_id, "--json"]);
191 assert_eq!(json_field(&json, "/body"), NASTY, "patch create"); 202 assert_eq!(json_field(&json, "/body"), NASTY, "patch create");
192 203
@@ -196,7 +207,11 @@ fn stdin_body_survives_on_every_command_that_takes_one() {
196 repo.run_stdin_ok(&["patch", "revise", &patch_id, "-F", "-"], NASTY.as_bytes()); 207 repo.run_stdin_ok(&["patch", "revise", &patch_id, "-F", "-"], NASTY.as_bytes());
197 repo.git(&["checkout", "main"]); 208 repo.git(&["checkout", "main"]);
198 let json = repo.run_ok(&["patch", "show", &patch_id, "--json"]); 209 let json = repo.run_ok(&["patch", "show", &patch_id, "--json"]);
199 assert_eq!(json_field(&json, "/revisions/1/body"), NASTY, "patch revise"); 210 assert_eq!(
211 json_field(&json, "/revisions/1/body"),
212 NASTY,
213 "patch revise"
214 );
200 } 215 }
201 216
202 // =========================================================================== 217 // ===========================================================================
tests/cli_surface_test.rs
Old New
@@ -67,7 +67,8 @@ fn scanned_files() -> Vec<PathBuf> {
67 } 67 }
68 68
69 fn collect_rs(dir: &Path, out: &mut Vec<PathBuf>) { 69 fn collect_rs(dir: &Path, out: &mut Vec<PathBuf>) {
70 let entries = std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read {}: {}", dir.display(), e)); 70 let entries =
71 std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read {}: {}", dir.display(), e));
71 for entry in entries { 72 for entry in entries {
72 let path = entry.unwrap().path(); 73 let path = entry.unwrap().path();
73 if path.is_dir() { 74 if path.is_dir() {
@@ -104,7 +105,8 @@ const BINARY_HEADS: [&str; 3] = ["git-collab ", "{} ", "collab "];
104 const DELIMITERS: [char; 2] = ['`', '\'']; 105 const DELIMITERS: [char; 2] = ['`', '\''];
105 106
106 fn citations_in(file: &Path) -> (Vec<Citation>, Vec<Citation>) { 107 fn citations_in(file: &Path) -> (Vec<Citation>, Vec<Citation>) {
107 let text = std::fs::read_to_string(file).unwrap_or_else(|e| panic!("read {}: {}", file.display(), e)); 108 let text =
109 std::fs::read_to_string(file).unwrap_or_else(|e| panic!("read {}: {}", file.display(), e));
108 let mut good = Vec::new(); 110 let mut good = Vec::new();
109 let mut wrong_binary = Vec::new(); 111 let mut wrong_binary = Vec::new();
110 112
@@ -153,7 +155,10 @@ fn openings(line: &str) -> Vec<(usize, &'static str, Option<char>)> {
153 // real spellings are looked for here. 155 // real spellings are looked for here.
154 let trimmed = line.trim_start(); 156 let trimmed = line.trim_start();
155 if let Some(cmd) = trimmed.strip_prefix("$ ") { 157 if let Some(cmd) = trimmed.strip_prefix("$ ") {
156 if let Some(head) = ["git-collab ", "collab "].iter().find(|h| cmd.starts_with(**h)) { 158 if let Some(head) = ["git-collab ", "collab "]
159 .iter()
160 .find(|h| cmd.starts_with(**h))
161 {
157 let offset = line.len() - cmd.len(); 162 let offset = line.len() - cmd.len();
158 found.push((offset, *head, None)); 163 found.push((offset, *head, None));
159 } 164 }
tests/cli_test.rs
Old New
@@ -970,7 +970,11 @@ fn test_git_merge_shows_a_hint_and_patch_merge_records_it() {
970 970
971 // Create patch pointing at the feature branch 971 // Create patch pointing at the feature branch
972 let out = repo.run_ok(&["patch", "create", "-t", "Add feature", "-B", "feature"]); 972 let out = repo.run_ok(&["patch", "create", "-t", "Add feature", "-B", "feature"]);
973 let id = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 973 let id = out
974 .trim()
975 .strip_prefix("Created patch ")
976 .unwrap()
977 .to_string();
974 978
975 // Merge via git directly 979 // Merge via git directly
976 repo.git(&["merge", "feature"]); 980 repo.git(&["merge", "feature"]);
@@ -982,7 +986,11 @@ fn test_git_merge_shows_a_hint_and_patch_merge_records_it() {
982 986
983 repo.run_ok(&["patch", "merge", &id]); 987 repo.run_ok(&["patch", "merge", &id]);
984 let out = repo.run_ok(&["patch", "show", &id]); 988 let out = repo.run_ok(&["patch", "show", &id]);
985 assert!(out.contains("[merged]") && !out.contains("[merged?]"), "{}", out); 989 assert!(
990 out.contains("[merged]") && !out.contains("[merged?]"),
991 "{}",
992 out
993 );
986 } 994 }
987 995
988 #[test] 996 #[test]
tests/comment_resolution_test.rs
Old New
@@ -126,7 +126,16 @@ fn rebased_patch_with_resolved_comment() -> (TestRepo, String, String) {
126 126
127 // A reviewer leaves an inline comment on r1. 127 // A reviewer leaves an inline comment on r1.
128 repo.run_ok(&[ 128 repo.run_ok(&[
129 "patch", "comment", &id, "--file", "feature.txt", "--line", "1", "--revision", "1", "-b", 129 "patch",
130 "comment",
131 &id,
132 "--file",
133 "feature.txt",
134 "--line",
135 "1",
136 "--revision",
137 "1",
138 "-b",
130 "wrong value here", 139 "wrong value here",
131 ]); 140 ]);
132 141
@@ -157,7 +166,15 @@ fn resolving_a_comment_records_who_and_at_which_revision() {
157 let repo = TestRepo::new("Alice", "alice@example.com"); 166 let repo = TestRepo::new("Alice", "alice@example.com");
158 let id = patch_over_a_file(&repo, "needs a guard"); 167 let id = patch_over_a_file(&repo, "needs a guard");
159 repo.run_ok(&[ 168 repo.run_ok(&[
160 "patch", "comment", &id, "--file", "feature.txt", "--line", "1", "-b", "add a guard", 169 "patch",
170 "comment",
171 &id,
172 "--file",
173 "feature.txt",
174 "--line",
175 "1",
176 "-b",
177 "add a guard",
161 ]); 178 ]);
162 let json = repo.run_ok(&["patch", "show", &id, "--json"]); 179 let json = repo.run_ok(&["patch", "show", &id, "--json"]);
163 let comment = first_inline_id(&json); 180 let comment = first_inline_id(&json);
@@ -177,7 +194,9 @@ fn resolving_a_comment_records_who_and_at_which_revision() {
177 "resolution defaults to the patch's latest revision" 194 "resolution defaults to the patch's latest revision"
178 ); 195 );
179 assert!( 196 assert!(
180 resolved["commit_id"].as_str().is_some_and(|s| s.len() == 40), 197 resolved["commit_id"]
198 .as_str()
199 .is_some_and(|s| s.len() == 40),
181 "the resolving event's full id must be in --json: {}", 200 "the resolving event's full id must be in --json: {}",
182 resolved 201 resolved
183 ); 202 );
@@ -227,7 +246,15 @@ fn an_unresolved_comment_reports_resolved_null_rather_than_omitting_it() {
227 let repo = TestRepo::new("Alice", "alice@example.com"); 246 let repo = TestRepo::new("Alice", "alice@example.com");
228 let id = patch_over_a_file(&repo, "nothing resolved"); 247 let id = patch_over_a_file(&repo, "nothing resolved");
229 repo.run_ok(&[ 248 repo.run_ok(&[
230 "patch", "comment", &id, "--file", "feature.txt", "--line", "1", "-b", "look here", 249 "patch",
250 "comment",
251 &id,
252 "--file",
253 "feature.txt",
254 "--line",
255 "1",
256 "-b",
257 "look here",
231 ]); 258 ]);
232 259
233 let json = repo.run_ok(&["patch", "show", &id, "--json"]); 260 let json = repo.run_ok(&["patch", "show", &id, "--json"]);
@@ -247,7 +274,15 @@ fn patch_show_marks_resolved_threads_and_says_who_resolved_them() {
247 let repo = TestRepo::new("Alice", "alice@example.com"); 274 let repo = TestRepo::new("Alice", "alice@example.com");
248 let id = patch_over_a_file(&repo, "marked up"); 275 let id = patch_over_a_file(&repo, "marked up");
249 repo.run_ok(&[ 276 repo.run_ok(&[
250 "patch", "comment", &id, "--file", "feature.txt", "--line", "1", "-b", "add a guard", 277 "patch",
278 "comment",
279 &id,
280 "--file",
281 "feature.txt",
282 "--line",
283 "1",
284 "-b",
285 "add a guard",
251 ]); 286 ]);
252 let json = repo.run_ok(&["patch", "show", &id, "--json"]); 287 let json = repo.run_ok(&["patch", "show", &id, "--json"]);
253 let comment = first_inline_id(&json); 288 let comment = first_inline_id(&json);
@@ -259,11 +294,7 @@ fn patch_show_marks_resolved_threads_and_says_who_resolved_them() {
259 "patch show must say a thread was resolved: {}", 294 "patch show must say a thread was resolved: {}",
260 out 295 out
261 ); 296 );
262 assert!( 297 assert!(out.contains("Alice"), "and who resolved it: {}", out);
263 out.contains("Alice"),
264 "and who resolved it: {}",
265 out
266 );
267 } 298 }
268 299
269 #[test] 300 #[test]
@@ -271,7 +302,15 @@ fn reopening_clears_the_resolution_and_both_events_stay_in_the_record() {
271 let repo = TestRepo::new("Alice", "alice@example.com"); 302 let repo = TestRepo::new("Alice", "alice@example.com");
272 let id = patch_over_a_file(&repo, "disputed"); 303 let id = patch_over_a_file(&repo, "disputed");
273 repo.run_ok(&[ 304 repo.run_ok(&[
274 "patch", "comment", &id, "--file", "feature.txt", "--line", "1", "-b", "not done", 305 "patch",
306 "comment",
307 &id,
308 "--file",
309 "feature.txt",
310 "--line",
311 "1",
312 "-b",
313 "not done",
275 ]); 314 ]);
276 let json = repo.run_ok(&["patch", "show", &id, "--json"]); 315 let json = repo.run_ok(&["patch", "show", &id, "--json"]);
277 let comment = first_inline_id(&json); 316 let comment = first_inline_id(&json);
@@ -339,7 +378,11 @@ fn a_new_revision_leaves_a_resolution_at_the_revision_it_was_claimed_against() {
339 repo.git(&["checkout", "-b", "feat"]); 378 repo.git(&["checkout", "-b", "feat"]);
340 repo.commit_file("feature.txt", "v1\n", "feature v1"); 379 repo.commit_file("feature.txt", "v1\n", "feature v1");
341 let out = repo.run_ok(&["patch", "create", "-t", "moving target", "-B", "feat"]); 380 let out = repo.run_ok(&["patch", "create", "-t", "moving target", "-B", "feat"]);
342 let id = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 381 let id = out
382 .trim()
383 .strip_prefix("Created patch ")
384 .unwrap()
385 .to_string();
343 386
344 repo.run_ok(&[ 387 repo.run_ok(&[
345 "patch", 388 "patch",
@@ -415,7 +458,8 @@ fn a_concurrent_resolve_and_reopen_converge() {
415 458
416 // The reviewer, offline, disagrees from the same fork point. 459 // The reviewer, offline, disagrees from the same fork point.
417 let branch_ref = "refs/collab/patches/other-clone/events"; 460 let branch_ref = "refs/collab/patches/other-clone/events";
418 repo.reference(branch_ref, fork_point, false, "fork").unwrap(); 461 repo.reference(branch_ref, fork_point, false, "fork")
462 .unwrap();
419 let reopen_oid = append( 463 let reopen_oid = append(
420 &repo, 464 &repo,
421 branch_ref, 465 branch_ref,
@@ -433,7 +477,8 @@ fn a_concurrent_resolve_and_reopen_converge() {
433 let one = patch_state(&repo, merged_ref, &id); 477 let one = patch_state(&repo, merged_ref, &id);
434 478
435 let other_ref = "refs/collab/patches/merged-other-way/events"; 479 let other_ref = "refs/collab/patches/merged-other-way/events";
436 repo.reference(other_ref, reopen_oid, false, "copy").unwrap(); 480 repo.reference(other_ref, reopen_oid, false, "copy")
481 .unwrap();
437 dag::reconcile(&repo, other_ref, &ref_name, &alice(), &sk).unwrap(); 482 dag::reconcile(&repo, other_ref, &ref_name, &alice(), &sk).unwrap();
438 let two = patch_state(&repo, other_ref, &id); 483 let two = patch_state(&repo, other_ref, &id);
439 484
@@ -570,10 +615,26 @@ fn resolving_by_an_ambiguous_prefix_errors_like_every_other_prefix() {
570 let repo = TestRepo::new("Alice", "alice@example.com"); 615 let repo = TestRepo::new("Alice", "alice@example.com");
571 let id = patch_over_a_file(&repo, "ambiguity"); 616 let id = patch_over_a_file(&repo, "ambiguity");
572 repo.run_ok(&[ 617 repo.run_ok(&[
573 "patch", "comment", &id, "--file", "feature.txt", "--line", "1", "-b", "one", 618 "patch",
619 "comment",
620 &id,
621 "--file",
622 "feature.txt",
623 "--line",
624 "1",
625 "-b",
626 "one",
574 ]); 627 ]);
575 repo.run_ok(&[ 628 repo.run_ok(&[
576 "patch", "comment", &id, "--file", "feature.txt", "--line", "1", "-b", "two", 629 "patch",
630 "comment",
631 &id,
632 "--file",
633 "feature.txt",
634 "--line",
635 "1",
636 "-b",
637 "two",
577 ]); 638 ]);
578 639
579 // The empty-ish prefix every comment shares. 640 // The empty-ish prefix every comment shares.
@@ -596,9 +657,17 @@ fn rendering_answers_does_not_write_to_the_dag() {
596 let (repo, id, comment) = rebased_patch_with_resolved_comment(); 657 let (repo, id, comment) = rebased_patch_with_resolved_comment();
597 repo.run_ok(&["patch", "resolve", &id, &comment[..8]]); 658 repo.run_ok(&["patch", "resolve", &id, &comment[..8]]);
598 659
599 let before = repo.git(&["for-each-ref", "--format=%(refname) %(objectname)", "refs/collab"]); 660 let before = repo.git(&[
661 "for-each-ref",
662 "--format=%(refname) %(objectname)",
663 "refs/collab",
664 ]);
600 repo.run_ok(&["patch", "diff", &id, "--answers", &comment[..8]]); 665 repo.run_ok(&["patch", "diff", &id, "--answers", &comment[..8]]);
601 let after = repo.git(&["for-each-ref", "--format=%(refname) %(objectname)", "refs/collab"]); 666 let after = repo.git(&[
667 "for-each-ref",
668 "--format=%(refname) %(objectname)",
669 "refs/collab",
670 ]);
602 671
603 assert_eq!( 672 assert_eq!(
604 before, after, 673 before, after,
tests/commit_link_test.rs
Old New
@@ -8,9 +8,7 @@ use git_collab::state::IssueState;
8 use tempfile::TempDir; 8 use tempfile::TempDir;
9 9
10 mod common; 10 mod common;
11 use common::{ 11 use common::{add_commit_link, alice, init_repo, open_issue, test_signing_key, ScopedTestConfig};
12 add_commit_link, alice, init_repo, open_issue, test_signing_key, ScopedTestConfig,
13 };
14 12
15 /// Append a commit-link event with an explicitly chosen `clock` value, 13 /// Append a commit-link event with an explicitly chosen `clock` value,
16 /// bypassing `dag::append_event`'s automatic clock-bump. The new commit's 14 /// bypassing `dag::append_event`'s automatic clock-bump. The new commit's
@@ -53,8 +51,15 @@ fn append_commit_link_with_clock(
53 let sig = author_signature(author).unwrap(); 51 let sig = author_signature(author).unwrap();
54 let parent_oid = repo.refname_to_id(ref_name).unwrap(); 52 let parent_oid = repo.refname_to_id(ref_name).unwrap();
55 let parent = repo.find_commit(parent_oid).unwrap(); 53 let parent = repo.find_commit(parent_oid).unwrap();
56 repo.commit(Some(ref_name), &sig, &sig, "issue.commit_link", &tree, &[&parent]) 54 repo.commit(
57 .unwrap() 55 Some(ref_name),
56 &sig,
57 &sig,
58 "issue.commit_link",
59 &tree,
60 &[&parent],
61 )
62 .unwrap()
58 } 63 }
59 64
60 fn test_author() -> Author { 65 fn test_author() -> Author {
@@ -160,8 +165,7 @@ fn issue_state_dedups_commit_links_keeps_lower_clock_even_when_appended_later()
160 assert_eq!(issue.linked_commits[0].commit, target_sha); 165 assert_eq!(issue.linked_commits[0].commit, target_sha);
161 // The lower-clock event wins, even though it was appended later. 166 // The lower-clock event wins, even though it was appended later.
162 assert_eq!( 167 assert_eq!(
163 issue.linked_commits[0].event_author.name, 168 issue.linked_commits[0].event_author.name, "Bob",
164 "Bob",
165 "expected Bob's lower-clock event to win the tiebreak" 169 "expected Bob's lower-clock event to win the tiebreak"
166 ); 170 );
167 } 171 }
tests/commit_msg_hook_test.rs
Old New
@@ -56,7 +56,8 @@ fn repo_with_hook_and_patch() -> (TestRepo, String) {
56 } 56 }
57 57
58 fn full_id(repo: &TestRepo, short: &str) -> String { 58 fn full_id(repo: &TestRepo, short: &str) -> String {
59 let json: Value = serde_json::from_str(&repo.run_ok(&["patch", "show", short, "--json"])).unwrap(); 59 let json: Value =
60 serde_json::from_str(&repo.run_ok(&["patch", "show", short, "--json"])).unwrap();
60 json["id"].as_str().unwrap().to_string() 61 json["id"].as_str().unwrap().to_string()
61 } 62 }
62 63
@@ -135,8 +136,13 @@ fn init_installs_the_commit_msg_hook() {
135 136
136 let path = hook_path(&repo); 137 let path = hook_path(&repo);
137 assert!(path.exists(), "init did not install a hook:\n{}", out); 138 assert!(path.exists(), "init did not install a hook:\n{}", out);
138 let mode = std::os::unix::fs::PermissionsExt::mode(&std::fs::metadata(&path).unwrap().permissions()); 139 let mode =
139 assert!(mode & 0o111 != 0, "hook is not executable (mode {:o})", mode); 140 std::os::unix::fs::PermissionsExt::mode(&std::fs::metadata(&path).unwrap().permissions());
141 assert!(
142 mode & 0o111 != 0,
143 "hook is not executable (mode {:o})",
144 mode
145 );
140 assert!( 146 assert!(
141 out.contains("commit-msg hook"), 147 out.contains("commit-msg hook"),
142 "init did not report the hook:\n{}", 148 "init did not report the hook:\n{}",
@@ -152,7 +158,10 @@ fn init_installs_the_hook_even_with_no_remotes() {
152 let repo = TestRepo::new("Alice", "alice@example.com"); 158 let repo = TestRepo::new("Alice", "alice@example.com");
153 assert!(repo.git(&["remote"]).trim().is_empty()); 159 assert!(repo.git(&["remote"]).trim().is_empty());
154 repo.run_ok(&["init"]); 160 repo.run_ok(&["init"]);
155 assert!(hook_path(&repo).exists(), "no hook in a repo with no remotes"); 161 assert!(
162 hook_path(&repo).exists(),
163 "no hook in a repo with no remotes"
164 );
156 } 165 }
157 166
158 #[test] 167 #[test]
@@ -212,7 +221,8 @@ fn init_recognizes_a_foreign_hook_that_already_calls_git_collab() {
212 let repo = TestRepo::new("Alice", "alice@example.com"); 221 let repo = TestRepo::new("Alice", "alice@example.com");
213 let path = hook_path(&repo); 222 let path = hook_path(&repo);
214 std::fs::create_dir_all(path.parent().unwrap()).unwrap(); 223 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
215 let theirs = "#!/bin/sh\ngit-collab hooks run-commit-msg \"$1\" >/dev/null 2>&1 || true\nexit 0\n"; 224 let theirs =
225 "#!/bin/sh\ngit-collab hooks run-commit-msg \"$1\" >/dev/null 2>&1 || true\nexit 0\n";
216 std::fs::write(&path, theirs).unwrap(); 226 std::fs::write(&path, theirs).unwrap();
217 227
218 let out = repo.run_ok(&["init"]); 228 let out = repo.run_ok(&["init"]);
@@ -287,7 +297,11 @@ fn hooks_status_reports_what_the_hook_would_stamp() {
287 // when something is wrong. This is the command that explains it. 297 // when something is wrong. This is the command that explains it.
288 let (repo, id) = repo_with_hook_and_patch(); 298 let (repo, id) = repo_with_hook_and_patch();
289 let out = repo.run_ok(&["hooks", "status"]); 299 let out = repo.run_ok(&["hooks", "status"]);
290 assert!(out.contains("installed"), "status did not report installation:\n{}", out); 300 assert!(
301 out.contains("installed"),
302 "status did not report installation:\n{}",
303 out
304 );
291 assert!( 305 assert!(
292 out.contains(&id[..8]), 306 out.contains(&id[..8]),
293 "status did not name the patch it would stamp:\n{}", 307 "status did not name the patch it would stamp:\n{}",
@@ -350,11 +364,7 @@ fn round_trip_message_already_ending_in_an_issue_trailer() {
350 // which silently breaks the commit-issue link the author wrote by hand. 364 // which silently breaks the commit-issue link the author wrote by hand.
351 let (repo, id) = repo_with_hook_and_patch(); 365 let (repo, id) = repo_with_hook_and_patch();
352 let issue = repo.issue_open("something to fix"); 366 let issue = repo.issue_open("something to fix");
353 let stored = commit_and_read_back( 367 let stored = commit_and_read_back(&repo, "a.txt", &format!("subject\n\nIssue: {}", issue));
354 &repo,
355 "a.txt",
356 &format!("subject\n\nIssue: {}", issue),
357 );
358 assert_round_trips(&stored, &id); 368 assert_round_trips(&stored, &id);
359 assert_eq!( 369 assert_eq!(
360 parse_trailers(&stored, ISSUE_TOKEN), 370 parse_trailers(&stored, ISSUE_TOKEN),
@@ -640,7 +650,10 @@ fn the_hook_exits_zero_when_the_message_file_is_missing() {
640 .current_dir(repo.dir.path()) 650 .current_dir(repo.dir.path())
641 .output() 651 .output()
642 .unwrap(); 652 .unwrap();
643 assert!(out.status.success(), "a missing message file failed the hook"); 653 assert!(
654 out.status.success(),
655 "a missing message file failed the hook"
656 );
644 } 657 }
645 658
646 // --------------------------------------------------------------------------- 659 // ---------------------------------------------------------------------------
@@ -666,7 +679,11 @@ fn a_hook_stamped_commit_is_recorded_as_merged_by_sync() {
666 repo.git(&["checkout", "-b", "feature"]); 679 repo.git(&["checkout", "-b", "feature"]);
667 repo.commit_file("f.txt", "one", "first commit"); 680 repo.commit_file("f.txt", "one", "first commit");
668 let out = repo.run_ok(&["patch", "create", "-t", "feature work", "-B", "feature"]); 681 let out = repo.run_ok(&["patch", "create", "-t", "feature work", "-B", "feature"]);
669 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 682 let short = out
683 .trim()
684 .strip_prefix("Created patch ")
685 .unwrap()
686 .to_string();
670 687
671 // A commit made after the patch exists — the one the hook can stamp. 688 // A commit made after the patch exists — the one the hook can stamp.
672 repo.commit_file("f.txt", "two", "more work"); 689 repo.commit_file("f.txt", "two", "more work");
@@ -683,7 +700,8 @@ fn a_hook_stamped_commit_is_recorded_as_merged_by_sync() {
683 repo.git(&["merge", "--no-ff", "-m", "merge feature", "feature"]); 700 repo.git(&["merge", "--no-ff", "-m", "merge feature", "feature"]);
684 repo.run_ok(&["sync"]); 701 repo.run_ok(&["sync"]);
685 702
686 let json: Value = serde_json::from_str(&repo.run_ok(&["patch", "show", &short, "--json"])).unwrap(); 703 let json: Value =
704 serde_json::from_str(&repo.run_ok(&["patch", "show", &short, "--json"])).unwrap();
687 assert_eq!( 705 assert_eq!(
688 json["status"].as_str().unwrap().to_lowercase(), 706 json["status"].as_str().unwrap().to_lowercase(),
689 "merged", 707 "merged",
tests/interdiff_test.rs
Old New
@@ -434,10 +434,18 @@ fn an_auto_merging_replay_shows_only_the_authors_hunk_and_writes_nothing() {
434 let repo = TestRepo::new("Alice", "alice@example.com"); 434 let repo = TestRepo::new("Alice", "alice@example.com");
435 repo.commit_file("both.txt", "a\nb\nc\nd\ne\nf\ng\n", "seed"); 435 repo.commit_file("both.txt", "a\nb\nc\nd\ne\nf\ng\n", "seed");
436 repo.git(&["checkout", "-b", "feat-automerge"]); 436 repo.git(&["checkout", "-b", "feat-automerge"]);
437 repo.commit_file("both.txt", "a\nb\nc\nd\ne\nf\nGGG\n", "patch edits the tail"); 437 repo.commit_file(
438 "both.txt",
439 "a\nb\nc\nd\ne\nf\nGGG\n",
440 "patch edits the tail",
441 );
438 let id = create_patch(&repo, "feat-automerge", "Auto-merging replay"); 442 let id = create_patch(&repo, "feat-automerge", "Auto-merging replay");
439 repo.git(&["checkout", "main"]); 443 repo.git(&["checkout", "main"]);
440 repo.commit_file("both.txt", "AAA\nb\nc\nd\ne\nf\ng\n", "upstream edits the head"); 444 repo.commit_file(
445 "both.txt",
446 "AAA\nb\nc\nd\ne\nf\ng\n",
447 "upstream edits the head",
448 );
441 repo.git(&["checkout", "feat-automerge"]); 449 repo.git(&["checkout", "feat-automerge"]);
442 repo.git(&["rebase", "main"]); 450 repo.git(&["rebase", "main"]);
443 repo.commit_file("both.txt", "AAA\nb\nc\nd\ne\nf\nHHH\n", "address review"); 451 repo.commit_file("both.txt", "AAA\nb\nc\nd\ne\nf\nHHH\n", "address review");
@@ -449,12 +457,7 @@ fn an_auto_merging_replay_shows_only_the_authors_hunk_and_writes_nothing() {
449 let out = repo.run_ok(&["patch", "diff", &id, "--between", "1", "2"]); 457 let out = repo.run_ok(&["patch", "diff", &id, "--between", "1", "2"]);
450 let after = count_objects(&objects); 458 let after = count_objects(&objects);
451 459
452 assert_eq!( 460 assert_eq!(changed_files(&out), vec!["both.txt".to_string()], "{}", out);
453 changed_files(&out),
454 vec!["both.txt".to_string()],
455 "{}",
456 out
457 );
458 assert!( 461 assert!(
459 out.contains("-GGG") && out.contains("+HHH"), 462 out.contains("-GGG") && out.contains("+HHH"),
460 "the author's hunk must be the one shown: {}", 463 "the author's hunk must be the one shown: {}",
tests/issue_reopen_test.rs
Old New
@@ -201,7 +201,11 @@ fn a_concurrent_close_and_reopen_converge_on_the_later_reopen() {
201 201
202 alice.run_ok(&["issue", "close", &short]); 202 alice.run_ok(&["issue", "close", &short]);
203 // Bob's extra tick, so his reopen is the strictly later event. 203 // Bob's extra tick, so his reopen is the strictly later event.
204 collab_in(&alice, &bob, &["issue", "comment", &short, "-b", "still here"]); 204 collab_in(
205 &alice,
206 &bob,
207 &["issue", "comment", &short, "-b", "still here"],
208 );
205 collab_in(&alice, &bob, &["issue", "reopen", &short]); 209 collab_in(&alice, &bob, &["issue", "reopen", &short]);
206 210
207 alice.run_ok(&["sync"]); 211 alice.run_ok(&["sync"]);
@@ -255,7 +259,11 @@ fn both_clones_keep_every_concurrent_event() {
255 259
256 alice.run_ok(&["issue", "comment", &short, "-b", "from alice"]); 260 alice.run_ok(&["issue", "comment", &short, "-b", "from alice"]);
257 alice.run_ok(&["issue", "close", &short]); 261 alice.run_ok(&["issue", "close", &short]);
258 collab_in(&alice, &bob, &["issue", "comment", &short, "-b", "from bob"]); 262 collab_in(
263 &alice,
264 &bob,
265 &["issue", "comment", &short, "-b", "from bob"],
266 );
259 collab_in(&alice, &bob, &["issue", "reopen", &short]); 267 collab_in(&alice, &bob, &["issue", "reopen", &short]);
260 268
261 alice.run_ok(&["sync"]); 269 alice.run_ok(&["sync"]);
tests/merge_recording_test.rs
Old New
@@ -57,7 +57,11 @@ fn patch_with_trailer(repo: &TestRepo, branch: &str, file: &str) -> String {
57 repo.git(&["checkout", "-b", branch]); 57 repo.git(&["checkout", "-b", branch]);
58 repo.commit_file(file, "content", &format!("work on {}", branch)); 58 repo.commit_file(file, "content", &format!("work on {}", branch));
59 let out = repo.run_ok(&["patch", "create", "-t", branch, "-B", branch]); 59 let out = repo.run_ok(&["patch", "create", "-t", branch, "-B", branch]);
60 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 60 let short = out
61 .trim()
62 .strip_prefix("Created patch ")
63 .unwrap()
64 .to_string();
61 stamp_head(repo, &short); 65 stamp_head(repo, &short);
62 repo.run_ok(&["patch", "revise", &short, "-b", "stamped"]); 66 repo.run_ok(&["patch", "revise", &short, "-b", "stamped"]);
63 repo.git(&["checkout", "main"]); 67 repo.git(&["checkout", "main"]);
@@ -87,7 +91,11 @@ fn count_events(repo: &TestRepo, ref_name: &str) -> usize {
87 } 91 }
88 92
89 fn patch_events_ref(repo: &TestRepo, short: &str) -> String { 93 fn patch_events_ref(repo: &TestRepo, short: &str) -> String {
90 let out = repo.git(&["for-each-ref", "--format=%(refname)", "refs/collab/patches/"]); 94 let out = repo.git(&[
95 "for-each-ref",
96 "--format=%(refname)",
97 "refs/collab/patches/",
98 ]);
91 out.lines() 99 out.lines()
92 .find(|r| r.contains(short) && r.ends_with("/events")) 100 .find(|r| r.contains(short) && r.ends_with("/events"))
93 .unwrap_or_else(|| panic!("no events ref for {} in {}", short, out)) 101 .unwrap_or_else(|| panic!("no events ref for {} in {}", short, out))
@@ -111,7 +119,11 @@ fn patch_fixing_issue(repo: &TestRepo, branch: &str, file: &str, issue: &str) ->
111 let out = repo.run_ok(&[ 119 let out = repo.run_ok(&[
112 "patch", "create", "-t", branch, "-B", branch, "--fixes", issue, 120 "patch", "create", "-t", branch, "-B", branch, "--fixes", issue,
113 ]); 121 ]);
114 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 122 let short = out
123 .trim()
124 .strip_prefix("Created patch ")
125 .unwrap()
126 .to_string();
115 stamp_head(repo, &short); 127 stamp_head(repo, &short);
116 repo.run_ok(&["patch", "revise", &short, "-b", "stamped"]); 128 repo.run_ok(&["patch", "revise", &short, "-b", "stamped"]);
117 repo.git(&["checkout", "main"]); 129 repo.git(&["checkout", "main"]);
@@ -215,7 +227,11 @@ fn patch_merge_records_the_merge() {
215 repo.git(&["checkout", "-b", "feat"]); 227 repo.git(&["checkout", "-b", "feat"]);
216 repo.commit_file("a.txt", "x", "the patch"); 228 repo.commit_file("a.txt", "x", "the patch");
217 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 229 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
218 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 230 let short = out
231 .trim()
232 .strip_prefix("Created patch ")
233 .unwrap()
234 .to_string();
219 repo.git(&["checkout", "main"]); 235 repo.git(&["checkout", "main"]);
220 repo.git(&["merge", "--ff-only", "feat"]); 236 repo.git(&["merge", "--ff-only", "feat"]);
221 237
@@ -231,7 +247,11 @@ fn patch_merge_records_the_base_tip_as_the_landing_commit() {
231 repo.git(&["checkout", "-b", "feat"]); 247 repo.git(&["checkout", "-b", "feat"]);
232 repo.commit_file("a.txt", "x", "the patch"); 248 repo.commit_file("a.txt", "x", "the patch");
233 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 249 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
234 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 250 let short = out
251 .trim()
252 .strip_prefix("Created patch ")
253 .unwrap()
254 .to_string();
235 repo.git(&["checkout", "main"]); 255 repo.git(&["checkout", "main"]);
236 squash_merge_keeping_message(&repo, "feat"); 256 squash_merge_keeping_message(&repo, "feat");
237 let landed = repo.git(&["rev-parse", "main"]).trim().to_string(); 257 let landed = repo.git(&["rev-parse", "main"]).trim().to_string();
@@ -255,7 +275,11 @@ fn merged_state_survives_branch_deletion_and_a_cold_cache() {
255 repo.git(&["checkout", "-b", "feat"]); 275 repo.git(&["checkout", "-b", "feat"]);
256 repo.commit_file("a.txt", "x", "the patch"); 276 repo.commit_file("a.txt", "x", "the patch");
257 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 277 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
258 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 278 let short = out
279 .trim()
280 .strip_prefix("Created patch ")
281 .unwrap()
282 .to_string();
259 repo.git(&["checkout", "main"]); 283 repo.git(&["checkout", "main"]);
260 repo.git(&["merge", "--ff-only", "feat"]); 284 repo.git(&["merge", "--ff-only", "feat"]);
261 repo.run_ok(&["patch", "merge", &short]); 285 repo.run_ok(&["patch", "merge", &short]);
@@ -275,7 +299,11 @@ fn patch_merge_twice_records_nothing_the_second_time() {
275 repo.git(&["checkout", "-b", "feat"]); 299 repo.git(&["checkout", "-b", "feat"]);
276 repo.commit_file("a.txt", "x", "the patch"); 300 repo.commit_file("a.txt", "x", "the patch");
277 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 301 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
278 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 302 let short = out
303 .trim()
304 .strip_prefix("Created patch ")
305 .unwrap()
306 .to_string();
279 repo.git(&["checkout", "main"]); 307 repo.git(&["checkout", "main"]);
280 repo.git(&["merge", "--ff-only", "feat"]); 308 repo.git(&["merge", "--ff-only", "feat"]);
281 309
@@ -305,7 +333,11 @@ fn patch_merge_closes_the_fixed_issue() {
305 let out = repo.run_ok(&[ 333 let out = repo.run_ok(&[
306 "patch", "create", "-t", "The fix", "-B", "feat", "--fixes", &issue, 334 "patch", "create", "-t", "The fix", "-B", "feat", "--fixes", &issue,
307 ]); 335 ]);
308 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 336 let short = out
337 .trim()
338 .strip_prefix("Created patch ")
339 .unwrap()
340 .to_string();
309 repo.git(&["checkout", "main"]); 341 repo.git(&["checkout", "main"]);
310 repo.git(&["merge", "--ff-only", "feat"]); 342 repo.git(&["merge", "--ff-only", "feat"]);
311 343
@@ -490,7 +522,11 @@ fn fixes_closes_the_issue_exactly_once_across_repeated_syncs() {
490 let out = repo.run_ok(&[ 522 let out = repo.run_ok(&[
491 "patch", "create", "-t", "The fix", "-B", "feat", "--fixes", &issue, 523 "patch", "create", "-t", "The fix", "-B", "feat", "--fixes", &issue,
492 ]); 524 ]);
493 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 525 let short = out
526 .trim()
527 .strip_prefix("Created patch ")
528 .unwrap()
529 .to_string();
494 stamp_head(&repo, &short); 530 stamp_head(&repo, &short);
495 repo.run_ok(&["patch", "revise", &short, "-b", "stamped"]); 531 repo.run_ok(&["patch", "revise", &short, "-b", "stamped"]);
496 repo.git(&["checkout", "main"]); 532 repo.git(&["checkout", "main"]);
@@ -530,7 +566,11 @@ fn a_fixes_issue_that_is_already_closed_gets_no_further_close() {
530 let out = repo.run_ok(&[ 566 let out = repo.run_ok(&[
531 "patch", "create", "-t", "The fix", "-B", "feat", "--fixes", &issue, 567 "patch", "create", "-t", "The fix", "-B", "feat", "--fixes", &issue,
532 ]); 568 ]);
533 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 569 let short = out
570 .trim()
571 .strip_prefix("Created patch ")
572 .unwrap()
573 .to_string();
534 stamp_head(&repo, &short); 574 stamp_head(&repo, &short);
535 repo.run_ok(&["patch", "revise", &short, "-b", "stamped"]); 575 repo.run_ok(&["patch", "revise", &short, "-b", "stamped"]);
536 repo.git(&["checkout", "main"]); 576 repo.git(&["checkout", "main"]);
@@ -575,7 +615,11 @@ fn a_close_that_did_not_happen_with_the_merge_is_retried_by_the_next_scan() {
575 let out = repo.run_ok(&[ 615 let out = repo.run_ok(&[
576 "patch", "create", "-t", "The fix", "-B", "feat", "--fixes", &issue, 616 "patch", "create", "-t", "The fix", "-B", "feat", "--fixes", &issue,
577 ]); 617 ]);
578 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 618 let short = out
619 .trim()
620 .strip_prefix("Created patch ")
621 .unwrap()
622 .to_string();
579 stamp_head(&repo, &short); 623 stamp_head(&repo, &short);
580 repo.run_ok(&["patch", "revise", &short, "-b", "stamped"]); 624 repo.run_ok(&["patch", "revise", &short, "-b", "stamped"]);
581 repo.git(&["checkout", "main"]); 625 repo.git(&["checkout", "main"]);
@@ -607,7 +651,11 @@ fn a_reachable_patch_with_no_merge_event_shows_as_merged_with_a_question_mark()
607 repo.git(&["checkout", "-b", "feat"]); 651 repo.git(&["checkout", "-b", "feat"]);
608 repo.commit_file("a.txt", "x", "the patch"); 652 repo.commit_file("a.txt", "x", "the patch");
609 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 653 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
610 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 654 let short = out
655 .trim()
656 .strip_prefix("Created patch ")
657 .unwrap()
658 .to_string();
611 repo.git(&["checkout", "main"]); 659 repo.git(&["checkout", "main"]);
612 repo.git(&["merge", "--ff-only", "feat"]); 660 repo.git(&["merge", "--ff-only", "feat"]);
613 661
@@ -631,7 +679,11 @@ fn displaying_a_reachable_patch_writes_no_event() {
631 repo.git(&["checkout", "-b", "feat"]); 679 repo.git(&["checkout", "-b", "feat"]);
632 repo.commit_file("a.txt", "x", "the patch"); 680 repo.commit_file("a.txt", "x", "the patch");
633 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 681 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
634 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 682 let short = out
683 .trim()
684 .strip_prefix("Created patch ")
685 .unwrap()
686 .to_string();
635 repo.git(&["checkout", "main"]); 687 repo.git(&["checkout", "main"]);
636 repo.git(&["merge", "--ff-only", "feat"]); 688 repo.git(&["merge", "--ff-only", "feat"]);
637 689
@@ -658,7 +710,11 @@ fn sync_names_the_patches_that_look_merged_but_are_not_recorded() {
658 repo.git(&["checkout", "-b", "feat"]); 710 repo.git(&["checkout", "-b", "feat"]);
659 repo.commit_file("a.txt", "x", "the patch"); 711 repo.commit_file("a.txt", "x", "the patch");
660 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 712 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
661 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 713 let short = out
714 .trim()
715 .strip_prefix("Created patch ")
716 .unwrap()
717 .to_string();
662 repo.git(&["checkout", "main"]); 718 repo.git(&["checkout", "main"]);
663 repo.git(&["merge", "--ff-only", "feat"]); 719 repo.git(&["merge", "--ff-only", "feat"]);
664 720
@@ -676,7 +732,11 @@ fn a_recorded_merge_is_not_reported_as_a_hint() {
676 repo.git(&["checkout", "-b", "feat"]); 732 repo.git(&["checkout", "-b", "feat"]);
677 repo.commit_file("a.txt", "x", "the patch"); 733 repo.commit_file("a.txt", "x", "the patch");
678 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 734 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
679 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 735 let short = out
736 .trim()
737 .strip_prefix("Created patch ")
738 .unwrap()
739 .to_string();
680 repo.git(&["checkout", "main"]); 740 repo.git(&["checkout", "main"]);
681 repo.git(&["merge", "--ff-only", "feat"]); 741 repo.git(&["merge", "--ff-only", "feat"]);
682 repo.run_ok(&["patch", "merge", &short]); 742 repo.run_ok(&["patch", "merge", &short]);
@@ -817,7 +877,11 @@ fn patch_merge_does_not_reclose_an_issue_reopened_after_its_merge() {
817 let out = repo.run_ok(&[ 877 let out = repo.run_ok(&[
818 "patch", "create", "-t", "The fix", "-B", "feat", "--fixes", &issue, 878 "patch", "create", "-t", "The fix", "-B", "feat", "--fixes", &issue,
819 ]); 879 ]);
820 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 880 let short = out
881 .trim()
882 .strip_prefix("Created patch ")
883 .unwrap()
884 .to_string();
821 repo.git(&["checkout", "main"]); 885 repo.git(&["checkout", "main"]);
822 repo.git(&["merge", "--ff-only", "feat"]); 886 repo.git(&["merge", "--ff-only", "feat"]);
823 887
@@ -868,7 +932,11 @@ fn two_clones_recording_a_merge_concurrently_converge() {
868 alice.git(&["checkout", "-b", "feat"]); 932 alice.git(&["checkout", "-b", "feat"]);
869 alice.commit_file("a.txt", "x", "the patch"); 933 alice.commit_file("a.txt", "x", "the patch");
870 let out = alice.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 934 let out = alice.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
871 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 935 let short = out
936 .trim()
937 .strip_prefix("Created patch ")
938 .unwrap()
939 .to_string();
872 alice.git(&["checkout", "main"]); 940 alice.git(&["checkout", "main"]);
873 alice.git(&["merge", "--ff-only", "feat"]); 941 alice.git(&["merge", "--ff-only", "feat"]);
874 alice.git(&["push", "origin", "main"]); 942 alice.git(&["push", "origin", "main"]);
@@ -898,9 +966,12 @@ fn two_clones_recording_a_merge_concurrently_converge() {
898 collab_in(&alice, &bob, &["sync"]); 966 collab_in(&alice, &bob, &["sync"]);
899 alice.run_ok(&["sync"]); 967 alice.run_ok(&["sync"]);
900 968
901 let bob_json: Value = 969 let bob_json: Value = serde_json::from_str(&collab_in(
902 serde_json::from_str(&collab_in(&alice, &bob, &["patch", "show", &short, "--json"])) 970 &alice,
903 .unwrap(); 971 &bob,
972 &["patch", "show", &short, "--json"],
973 ))
974 .unwrap();
904 assert_eq!(show_json(&alice, &short)["status"], "merged"); 975 assert_eq!(show_json(&alice, &short)["status"], "merged");
905 assert_eq!(bob_json["status"], "merged"); 976 assert_eq!(bob_json["status"], "merged");
906 977
@@ -922,7 +993,11 @@ fn json_reports_the_merge_hint_beside_the_status() {
922 repo.git(&["checkout", "-b", "feat"]); 993 repo.git(&["checkout", "-b", "feat"]);
923 repo.commit_file("a.txt", "x", "the patch"); 994 repo.commit_file("a.txt", "x", "the patch");
924 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 995 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
925 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 996 let short = out
997 .trim()
998 .strip_prefix("Created patch ")
999 .unwrap()
1000 .to_string();
926 repo.git(&["checkout", "main"]); 1001 repo.git(&["checkout", "main"]);
927 1002
928 // Not merged: no hint at all, so its absence never reads as an assertion. 1003 // Not merged: no hint at all, so its absence never reads as an assertion.
@@ -964,7 +1039,11 @@ fn the_recorded_merge_commit_reaches_show_json_and_patch_log() {
964 repo.git(&["checkout", "-b", "feat"]); 1039 repo.git(&["checkout", "-b", "feat"]);
965 repo.commit_file("a.txt", "x", "the patch"); 1040 repo.commit_file("a.txt", "x", "the patch");
966 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 1041 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
967 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 1042 let short = out
1043 .trim()
1044 .strip_prefix("Created patch ")
1045 .unwrap()
1046 .to_string();
968 repo.git(&["checkout", "main"]); 1047 repo.git(&["checkout", "main"]);
969 squash_merge_keeping_message(&repo, "feat"); 1048 squash_merge_keeping_message(&repo, "feat");
970 let landed = repo.git(&["rev-parse", "main"]).trim().to_string(); 1049 let landed = repo.git(&["rev-parse", "main"]).trim().to_string();
@@ -1014,7 +1093,11 @@ fn displaying_a_merged_patch_writes_no_event() {
1014 repo.git(&["checkout", "-b", "feat"]); 1093 repo.git(&["checkout", "-b", "feat"]);
1015 repo.commit_file("a.txt", "x", "the patch"); 1094 repo.commit_file("a.txt", "x", "the patch");
1016 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 1095 let out = repo.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
1017 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 1096 let short = out
1097 .trim()
1098 .strip_prefix("Created patch ")
1099 .unwrap()
1100 .to_string();
1018 repo.git(&["checkout", "main"]); 1101 repo.git(&["checkout", "main"]);
1019 repo.git(&["merge", "--ff-only", "feat"]); 1102 repo.git(&["merge", "--ff-only", "feat"]);
1020 repo.run_ok(&["patch", "merge", &short]); 1103 repo.run_ok(&["patch", "merge", &short]);
@@ -1048,7 +1131,11 @@ fn two_clones_recording_different_merge_commits_agree_on_one() {
1048 alice.git(&["checkout", "-b", "feat"]); 1131 alice.git(&["checkout", "-b", "feat"]);
1049 alice.commit_file("a.txt", "x", "the patch"); 1132 alice.commit_file("a.txt", "x", "the patch");
1050 let out = alice.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]); 1133 let out = alice.run_ok(&["patch", "create", "-t", "Feature", "-B", "feat"]);
1051 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 1134 let short = out
1135 .trim()
1136 .strip_prefix("Created patch ")
1137 .unwrap()
1138 .to_string();
1052 alice.git(&["checkout", "main"]); 1139 alice.git(&["checkout", "main"]);
1053 alice.git(&["merge", "--ff-only", "feat"]); 1140 alice.git(&["merge", "--ff-only", "feat"]);
1054 let first = alice.git(&["rev-parse", "main"]).trim().to_string(); 1141 let first = alice.git(&["rev-parse", "main"]).trim().to_string();
tests/mutating_json_test.rs
Old New
@@ -55,7 +55,8 @@ fn assert_full_id(value: &Value, what: &str) {
55 s.len() 55 s.len()
56 ); 56 );
57 assert!( 57 assert!(
58 s.chars().all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()), 58 s.chars()
59 .all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()),
59 "{} must be a hex object name, got {:?}", 60 "{} must be a hex object name, got {:?}",
60 what, 61 what,
61 s 62 s
@@ -127,7 +128,15 @@ fn patch_create_json_carries_the_fixed_issue_in_full() {
127 let json = json_ok( 128 let json = json_ok(
128 &repo, 129 &repo,
129 &[ 130 &[
130 "patch", "create", "-t", "Fix", "-B", "feat", "--fixes", &issue[..8], "--json", 131 "patch",
132 "create",
133 "-t",
134 "Fix",
135 "-B",
136 "feat",
137 "--fixes",
138 &issue[..8],
139 "--json",
131 ], 140 ],
132 ); 141 );
133 assert_full_id(&json["patch"], "patch"); 142 assert_full_id(&json["patch"], "patch");
@@ -199,13 +208,7 @@ fn every_issue_mutation_reports_the_issue_in_full() {
199 208
200 let deleted = json_ok( 209 let deleted = json_ok(
201 &repo, 210 &repo,
202 &[ 211 &["issue", "delete-comment", short, &comment_id[..8], "--json"],
203 "issue",
204 "delete-comment",
205 short,
206 &comment_id[..8],
207 "--json",
208 ],
209 ); 212 );
210 assert_eq!(deleted["action"], "issue.delete_comment"); 213 assert_eq!(deleted["action"], "issue.delete_comment");
211 assert_eq!(deleted["comment"], comment_id); 214 assert_eq!(deleted["comment"], comment_id);
@@ -222,7 +225,8 @@ fn every_issue_mutation_reports_the_issue_in_full() {
222 let deleted = json_ok(&repo, &["issue", "delete", short, "--json"]); 225 let deleted = json_ok(&repo, &["issue", "delete", short, "--json"]);
223 assert_eq!(deleted["action"], "issue.delete"); 226 assert_eq!(deleted["action"], "issue.delete");
224 assert_eq!( 227 assert_eq!(
225 deleted["issue"], id.as_str(), 228 deleted["issue"],
229 id.as_str(),
226 "the id of something deleted is exactly what it was" 230 "the id of something deleted is exactly what it was"
227 ); 231 );
228 } 232 }
@@ -267,7 +271,9 @@ fn every_patch_mutation_reports_the_patch_in_full() {
267 271
268 let review = json_ok( 272 let review = json_ok(
269 &repo, 273 &repo,
270 &["patch", "review", short, "-v", "approve", "-b", "lgtm", "--json"], 274 &[
275 "patch", "review", short, "-v", "approve", "-b", "lgtm", "--json",
276 ],
271 ); 277 );
272 assert_eq!(review["action"], "patch.review"); 278 assert_eq!(review["action"], "patch.review");
273 assert_eq!(review["patch"], id.as_str()); 279 assert_eq!(review["patch"], id.as_str());
@@ -317,7 +323,14 @@ fn every_patch_mutation_reports_the_patch_in_full() {
317 let revised = json_ok( 323 let revised = json_ok(
318 &repo, 324 &repo,
319 &[ 325 &[
320 "patch", "revise", short, "-B", "feat", "-b", "round two", "--json", 326 "patch",
327 "revise",
328 short,
329 "-B",
330 "feat",
331 "-b",
332 "round two",
333 "--json",
321 ], 334 ],
322 ); 335 );
323 assert_eq!(revised["action"], "patch.revision"); 336 assert_eq!(revised["action"], "patch.revision");
@@ -328,7 +341,15 @@ fn every_patch_mutation_reports_the_patch_in_full() {
328 341
329 let edited_rev = json_ok( 342 let edited_rev = json_ok(
330 &repo, 343 &repo,
331 &["patch", "edit-revision", short, "2", "-b", "fixed", "--json"], 344 &[
345 "patch",
346 "edit-revision",
347 short,
348 "2",
349 "-b",
350 "fixed",
351 "--json",
352 ],
332 ); 353 );
333 assert_eq!(edited_rev["action"], "patch.edit_revision"); 354 assert_eq!(edited_rev["action"], "patch.edit_revision");
334 assert_eq!(edited_rev["patch"], id.as_str()); 355 assert_eq!(edited_rev["patch"], id.as_str());
@@ -366,7 +387,15 @@ fn patch_merge_json_names_the_patch_the_commit_and_the_issue_it_closed() {
366 let created = json_ok( 387 let created = json_ok(
367 &repo, 388 &repo,
368 &[ 389 &[
369 "patch", "create", "-t", "Fix", "-B", "feat", "--fixes", &issue[..8], "--json", 390 "patch",
391 "create",
392 "-t",
393 "Fix",
394 "-B",
395 "feat",
396 "--fixes",
397 &issue[..8],
398 "--json",
370 ], 399 ],
371 ); 400 );
372 let id = created["patch"].as_str().unwrap().to_string(); 401 let id = created["patch"].as_str().unwrap().to_string();
@@ -422,7 +451,10 @@ fn patch_reopen_json_reports_the_merge_commit_it_cleared() {
422 fn key_and_identity_mutations_report_what_they_changed() { 451 fn key_and_identity_mutations_report_what_they_changed() {
423 let repo = TestRepo::new("Alice", "alice@example.com"); 452 let repo = TestRepo::new("Alice", "alice@example.com");
424 453
425 let added = json_ok(&repo, &["key", "add", "--self", "--label", "mine", "--json"]); 454 let added = json_ok(
455 &repo,
456 &["key", "add", "--self", "--label", "mine", "--json"],
457 );
426 assert_eq!(added["action"], "key.add"); 458 assert_eq!(added["action"], "key.add");
427 assert_eq!(added["added"], true); 459 assert_eq!(added["added"], true);
428 assert_eq!(added["label"], "mine"); 460 assert_eq!(added["label"], "mine");
tests/patch_reopen_test.rs
Old New
@@ -66,7 +66,11 @@ fn patch_on_branch(repo: &TestRepo, branch: &str, file: &str) -> String {
66 repo.git(&["checkout", "-b", branch]); 66 repo.git(&["checkout", "-b", branch]);
67 repo.commit_file(file, "content", &format!("work on {}", branch)); 67 repo.commit_file(file, "content", &format!("work on {}", branch));
68 let out = repo.run_ok(&["patch", "create", "-t", branch, "-B", branch]); 68 let out = repo.run_ok(&["patch", "create", "-t", branch, "-B", branch]);
69 let short = out.trim().strip_prefix("Created patch ").unwrap().to_string(); 69 let short = out
70 .trim()
71 .strip_prefix("Created patch ")
72 .unwrap()
73 .to_string();
70 repo.git(&["checkout", "main"]); 74 repo.git(&["checkout", "main"]);
71 short 75 short
72 } 76 }
@@ -298,9 +302,12 @@ fn a_concurrent_close_and_reopen_converge() {
298 alice.run_ok(&["sync"]); 302 alice.run_ok(&["sync"]);
299 303
300 let alice_status = show_json(&alice, &short)["status"].clone(); 304 let alice_status = show_json(&alice, &short)["status"].clone();
301 let bob_json: Value = 305 let bob_json: Value = serde_json::from_str(&collab_in(
302 serde_json::from_str(&collab_in(&alice, &bob, &["patch", "show", &short, "--json"])) 306 &alice,
303 .unwrap(); 307 &bob,
308 &["patch", "show", &short, "--json"],
309 ))
310 .unwrap();
304 assert_eq!( 311 assert_eq!(
305 alice_status, bob_json["status"], 312 alice_status, bob_json["status"],
306 "a close and a reopen that never saw each other must still land on one answer" 313 "a close and a reopen that never saw each other must still land on one answer"
tests/release_cli_test.rs
Old New
@@ -388,7 +388,8 @@ fn assert_full_sha256(value: &serde_json::Value, expected: &str) {
388 s.len() 388 s.len()
389 ); 389 );
390 assert!( 390 assert!(
391 s.chars().all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()), 391 s.chars()
392 .all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()),
392 "sha256 must be lowercase hex, got {:?}", 393 "sha256 must be lowercase hex, got {:?}",
393 s 394 s
394 ); 395 );
@@ -562,7 +563,10 @@ fn a_failing_delete_prints_the_error_object_on_stdout() {
562 563
563 // Rejected client-side, before any network round trip: the same contract 564 // Rejected client-side, before any network round trip: the same contract
564 // has to hold on the path that never reaches the server. 565 // has to hold on the path that never reaches the server.
565 let bad = release_cmd(&harness, &["delete", "../evil", "--remote", "srv", "--json"]); 566 let bad = release_cmd(
567 &harness,
568 &["delete", "../evil", "--remote", "srv", "--json"],
569 );
566 let message = json_err(&bad, "deleting an invalid version with --json"); 570 let message = json_err(&bad, "deleting an invalid version with --json");
567 assert!( 571 assert!(
568 message.contains("invalid version"), 572 message.contains("invalid version"),
tests/revision_refs_test.rs
Old New
@@ -361,7 +361,8 @@ fn merging_the_base_branch_forward_makes_the_patch_look_merged() {
361 361
362 assert!(looks_merged(&repo, &short)); 362 assert!(looks_merged(&repo, &short));
363 assert_eq!( 363 assert_eq!(
364 show_json(&repo, &short)["status"], "open", 364 show_json(&repo, &short)["status"],
365 "open",
365 "the hint is not the status" 366 "the hint is not the status"
366 ); 367 );
367 } 368 }
tests/server_behavior_test.rs
Old New
@@ -187,7 +187,11 @@ fn readme_md_renders_on_repo_overview_page() {
187 harness.push_head(); 187 harness.push_head();
188 188
189 let overview = harness.get_ok(&format!("/{}", harness.repo_name())); 189 let overview = harness.get_ok(&format!("/{}", harness.repo_name()));
190 assert!(overview.body.contains("<h1>Hello World</h1>"), "missing h1: {}", overview.body); 190 assert!(
191 overview.body.contains("<h1>Hello World</h1>"),
192 "missing h1: {}",
193 overview.body
194 );
191 assert!(overview.body.contains("href=\"https://example.com\"")); 195 assert!(overview.body.contains("href=\"https://example.com\""));
192 // Sanity: the new card wrapper exists. 196 // Sanity: the new card wrapper exists.
193 assert!(overview.body.contains("class=\"readme-body\"")); 197 assert!(overview.body.contains("class=\"readme-body\""));
@@ -197,11 +201,9 @@ fn readme_md_renders_on_repo_overview_page() {
197 fn missing_readme_does_not_break_overview_page() { 201 fn missing_readme_does_not_break_overview_page() {
198 let harness = ServerHarness::new("behavior-no-readme"); 202 let harness = ServerHarness::new("behavior-no-readme");
199 203
200 harness.work_repo().commit_file( 204 harness
201 "src/lib.rs", 205 .work_repo()
202 "pub fn x() {}\n", 206 .commit_file("src/lib.rs", "pub fn x() {}\n", "add lib");
203 "add lib",
204 );
205 harness.push_head(); 207 harness.push_head();
206 208
207 let overview = harness.get_ok(&format!("/{}", harness.repo_name())); 209 let overview = harness.get_ok(&format!("/{}", harness.repo_name()));
tests/sync_diagnostics_test.rs
Old New
@@ -171,8 +171,11 @@ fn ordinary_push_failure_still_gets_retry_advice() {
171 // ref layout, and clearing it does make a retry succeed. 171 // ref layout, and clearing it does make a retry succeed.
172 let lock_dir = bare.path().join("refs/collab/patches").join(&id); 172 let lock_dir = bare.path().join("refs/collab/patches").join(&id);
173 std::fs::create_dir_all(&lock_dir).unwrap(); 173 std::fs::create_dir_all(&lock_dir).unwrap();
174 std::fs::write(lock_dir.join("events.lock"), "0000000000000000000000000000000000000000\n") 174 std::fs::write(
175 .unwrap(); 175 lock_dir.join("events.lock"),
176 "0000000000000000000000000000000000000000\n",
177 )
178 .unwrap();
176 179
177 let stderr = repo.run_err(&["sync", "--remote", "origin"]); 180 let stderr = repo.run_err(&["sync", "--remote", "origin"]);
178 181
tests/sync_test.rs
Old New
@@ -502,12 +502,7 @@ fn test_auto_sync_remote_config_pins_to_single_remote() {
502 fn add_unreachable_remote(repo_dir: &std::path::Path, remote_name: &str) { 502 fn add_unreachable_remote(repo_dir: &std::path::Path, remote_name: &str) {
503 let missing = repo_dir.join("no-such-remote.git"); 503 let missing = repo_dir.join("no-such-remote.git");
504 Command::new("git") 504 Command::new("git")
505 .args([ 505 .args(["remote", "add", remote_name, missing.to_str().unwrap()])
506 "remote",
507 "add",
508 remote_name,
509 missing.to_str().unwrap(),
510 ])
511 .current_dir(repo_dir) 506 .current_dir(repo_dir)
512 .status() 507 .status()
513 .unwrap(); 508 .unwrap();
@@ -584,11 +579,7 @@ fn test_auto_sync_output_stays_off_stdout() {
584 let stderr = String::from_utf8(output.stderr).unwrap(); 579 let stderr = String::from_utf8(output.stderr).unwrap();
585 580
586 // stdout is the command's own result, whole and alone. 581 // stdout is the command's own result, whole and alone.
587 assert!( 582 assert!(stdout.starts_with("Opened issue "), "stdout: {:?}", stdout);
588 stdout.starts_with("Opened issue "),
589 "stdout: {:?}",
590 stdout
591 );
592 assert_eq!( 583 assert_eq!(
593 stdout.lines().count(), 584 stdout.lines().count(),
594 1, 585 1,
@@ -596,7 +587,12 @@ fn test_auto_sync_output_stays_off_stdout() {
596 stdout, 587 stdout,
597 stderr 588 stderr
598 ); 589 );
599 for noise in ["Fetching from", "Pushing to", "Sync complete", "Pushed refs/"] { 590 for noise in [
591 "Fetching from",
592 "Pushing to",
593 "Sync complete",
594 "Pushed refs/",
595 ] {
600 assert!( 596 assert!(
601 !stdout.contains(noise), 597 !stdout.contains(noise),
602 "{:?} is auto-sync's output, not the command's:\nstdout: {}", 598 "{:?} is auto-sync's output, not the command's:\nstdout: {}",
@@ -1635,10 +1631,7 @@ fn commit_link_scan_emits_event_for_matching_trailer() {
1635 1631
1636 // Run the scanner directly (we test the sync integration in later tests). 1632 // Run the scanner directly (we test the sync integration in later tests).
1637 let author = git_collab::identity::get_author(&alice_repo).unwrap(); 1633 let author = git_collab::identity::get_author(&alice_repo).unwrap();
1638 let sk = signing::load_signing_key( 1634 let sk = signing::load_signing_key(&signing::signing_key_dir().unwrap()).unwrap();
1639 &signing::signing_key_dir().unwrap(),
1640 )
1641 .unwrap();
1642 let emitted = commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(); 1635 let emitted = commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap();
1643 assert_eq!(emitted, 1); 1636 assert_eq!(emitted, 1);
1644 1637
@@ -1684,9 +1677,18 @@ fn commit_link_scan_is_idempotent_across_runs() {
1684 let author = git_collab::identity::get_author(&alice_repo).unwrap(); 1677 let author = git_collab::identity::get_author(&alice_repo).unwrap();
1685 let sk = signing::load_signing_key(&signing::signing_key_dir().unwrap()).unwrap(); 1678 let sk = signing::load_signing_key(&signing::signing_key_dir().unwrap()).unwrap();
1686 1679
1687 assert_eq!(commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(), 1); 1680 assert_eq!(
1688 assert_eq!(commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(), 0); 1681 commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(),
1689 assert_eq!(commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(), 0); 1682 1
1683 );
1684 assert_eq!(
1685 commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(),
1686 0
1687 );
1688 assert_eq!(
1689 commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(),
1690 0
1691 );
1690 1692
1691 let issue = IssueState::from_ref_uncached(&alice_repo, &issue_ref, &issue_id).unwrap(); 1693 let issue = IssueState::from_ref_uncached(&alice_repo, &issue_ref, &issue_id).unwrap();
1692 assert_eq!(issue.linked_commits.len(), 1); 1694 assert_eq!(issue.linked_commits.len(), 1);
@@ -1713,7 +1715,10 @@ fn commit_link_scan_walks_all_local_branches_and_dedups_shared_ancestors() {
1713 1715
1714 // Should emit exactly one event despite the commit being reachable from 1716 // Should emit exactly one event despite the commit being reachable from
1715 // two branch tips. 1717 // two branch tips.
1716 assert_eq!(commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(), 1); 1718 assert_eq!(
1719 commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(),
1720 1
1721 );
1717 let issue = IssueState::from_ref_uncached(&alice_repo, &issue_ref, &issue_id).unwrap(); 1722 let issue = IssueState::from_ref_uncached(&alice_repo, &issue_ref, &issue_id).unwrap();
1718 assert_eq!(issue.linked_commits.len(), 1); 1723 assert_eq!(issue.linked_commits.len(), 1);
1719 } 1724 }
@@ -1725,16 +1730,15 @@ fn commit_link_scan_handles_multiple_issue_trailers_on_one_commit() {
1725 let (issue_ref_a, id_a) = open_issue(&alice_repo, &alice(), "bug a"); 1730 let (issue_ref_a, id_a) = open_issue(&alice_repo, &alice(), "bug a");
1726 let (issue_ref_b, id_b) = open_issue(&alice_repo, &alice(), "bug b"); 1731 let (issue_ref_b, id_b) = open_issue(&alice_repo, &alice(), "bug b");
1727 1732
1728 let message = format!( 1733 let message = format!("Fix both\n\nIssue: {}\nIssue: {}", &id_a[..8], &id_b[..8]);
1729 "Fix both\n\nIssue: {}\nIssue: {}",
1730 &id_a[..8],
1731 &id_b[..8]
1732 );
1733 make_commit_with_message(&alice_repo, &message); 1734 make_commit_with_message(&alice_repo, &message);
1734 1735
1735 let author = git_collab::identity::get_author(&alice_repo).unwrap(); 1736 let author = git_collab::identity::get_author(&alice_repo).unwrap();
1736 let sk = signing::load_signing_key(&signing::signing_key_dir().unwrap()).unwrap(); 1737 let sk = signing::load_signing_key(&signing::signing_key_dir().unwrap()).unwrap();
1737 assert_eq!(commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(), 2); 1738 assert_eq!(
1739 commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(),
1740 2
1741 );
1738 1742
1739 let issue_a = IssueState::from_ref_uncached(&alice_repo, &issue_ref_a, &id_a).unwrap(); 1743 let issue_a = IssueState::from_ref_uncached(&alice_repo, &issue_ref_a, &id_a).unwrap();
1740 let issue_b = IssueState::from_ref_uncached(&alice_repo, &issue_ref_b, &id_b).unwrap(); 1744 let issue_b = IssueState::from_ref_uncached(&alice_repo, &issue_ref_b, &id_b).unwrap();
@@ -1748,14 +1752,14 @@ fn commit_link_scan_skips_unknown_prefix_without_error() {
1748 let alice_repo = cluster.alice_repo(); 1752 let alice_repo = cluster.alice_repo();
1749 1753
1750 // No issue exists. Commit uses a completely unrelated prefix. 1754 // No issue exists. Commit uses a completely unrelated prefix.
1751 make_commit_with_message( 1755 make_commit_with_message(&alice_repo, "Fix\n\nIssue: zzzzzzzz");
1752 &alice_repo,
1753 "Fix\n\nIssue: zzzzzzzz",
1754 );
1755 1756
1756 let author = git_collab::identity::get_author(&alice_repo).unwrap(); 1757 let author = git_collab::identity::get_author(&alice_repo).unwrap();
1757 let sk = signing::load_signing_key(&signing::signing_key_dir().unwrap()).unwrap(); 1758 let sk = signing::load_signing_key(&signing::signing_key_dir().unwrap()).unwrap();
1758 assert_eq!(commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(), 0); 1759 assert_eq!(
1760 commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(),
1761 0
1762 );
1759 } 1763 }
1760 1764
1761 #[test] 1765 #[test]
@@ -1773,8 +1777,7 @@ fn commit_link_scan_skips_genuinely_ambiguous_prefix() {
1773 } 1777 }
1774 1778
1775 // Find a first-char that has at least 2 matching IDs. 1779 // Find a first-char that has at least 2 matching IDs.
1776 let mut counts: std::collections::HashMap<char, Vec<&str>> = 1780 let mut counts: std::collections::HashMap<char, Vec<&str>> = std::collections::HashMap::new();
1777 std::collections::HashMap::new();
1778 for id in &ids { 1781 for id in &ids {
1779 let c = id.chars().next().unwrap(); 1782 let c = id.chars().next().unwrap();
1780 counts.entry(c).or_default().push(id.as_str()); 1783 counts.entry(c).or_default().push(id.as_str());
@@ -1829,7 +1832,10 @@ fn commit_link_scan_skips_archived_issues_with_warning() {
1829 1832
1830 let author = git_collab::identity::get_author(&alice_repo).unwrap(); 1833 let author = git_collab::identity::get_author(&alice_repo).unwrap();
1831 let sk = signing::load_signing_key(&signing::signing_key_dir().unwrap()).unwrap(); 1834 let sk = signing::load_signing_key(&signing::signing_key_dir().unwrap()).unwrap();
1832 assert_eq!(commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(), 0); 1835 assert_eq!(
1836 commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(),
1837 0
1838 );
1833 1839
1834 // Confirm the archived ref did not accrue a new event: the archived 1840 // Confirm the archived ref did not accrue a new event: the archived
1835 // DAG tip should still be the archive-time tip. 1841 // DAG tip should still be the archive-time tip.
@@ -1865,7 +1871,10 @@ fn commit_link_scan_no_op_on_detached_head_with_no_branches() {
1865 let author = git_collab::identity::get_author(&alice_repo).unwrap(); 1871 let author = git_collab::identity::get_author(&alice_repo).unwrap();
1866 let sk = signing::load_signing_key(&signing::signing_key_dir().unwrap()).unwrap(); 1872 let sk = signing::load_signing_key(&signing::signing_key_dir().unwrap()).unwrap();
1867 // No branches to walk — silent no-op. 1873 // No branches to walk — silent no-op.
1868 assert_eq!(commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(), 0); 1874 assert_eq!(
1875 commit_link::scan_and_link(&alice_repo, &author, &sk).unwrap(),
1876 0
1877 );
1869 } 1878 }
1870 1879
1871 #[test] 1880 #[test]
@@ -1913,7 +1922,10 @@ fn commit_link_scan_dedups_against_remote_originated_events() {
1913 let bob_ref = format!("refs/collab/issues/{}", issue_id); 1922 let bob_ref = format!("refs/collab/issues/{}", issue_id);
1914 let bob_issue = IssueState::from_ref_uncached(&bob_repo, &bob_ref, &issue_id).unwrap(); 1923 let bob_issue = IssueState::from_ref_uncached(&bob_repo, &bob_ref, &issue_id).unwrap();
1915 assert_eq!(bob_issue.linked_commits.len(), 1); 1924 assert_eq!(bob_issue.linked_commits.len(), 1);
1916 assert_eq!(bob_issue.linked_commits[0].commit, linked_commit.to_string()); 1925 assert_eq!(
1926 bob_issue.linked_commits[0].commit,
1927 linked_commit.to_string()
1928 );
1917 } 1929 }
1918 1930
1919 #[test] 1931 #[test]
@@ -2260,8 +2272,7 @@ fn one_unreadable_patch_does_not_take_down_the_sync() {
2260 }; 2272 };
2261 2273
2262 let alice_repo = Repository::open(cluster.alice_dir.path()).unwrap(); 2274 let alice_repo = Repository::open(cluster.alice_dir.path()).unwrap();
2263 sync::sync(&alice_repo, "origin") 2275 sync::sync(&alice_repo, "origin").expect("one unreadable patch must not fail the whole sync");
2264 .expect("one unreadable patch must not fail the whole sync");
2265 2276
2266 // Everything else went through. 2277 // Everything else went through.
2267 let bob_repo = cluster.bob_repo(); 2278 let bob_repo = cluster.bob_repo();
@@ -2379,7 +2390,10 @@ fn sync_does_not_adopt_a_revision_ref_the_signed_dag_does_not_vouch_for() {
2379 .args([ 2390 .args([
2380 "push", 2391 "push",
2381 "origin", 2392 "origin",
2382 &format!("{}:refs/collab/patches/{}/rev/{}", planted, orphan_id, planted), 2393 &format!(
2394 "{}:refs/collab/patches/{}/rev/{}",
2395 planted, orphan_id, planted
2396 ),
2383 &format!("{}:refs/collab/patches/{}/rev/{}", planted, id, planted), 2397 &format!("{}:refs/collab/patches/{}/rev/{}", planted, id, planted),
2384 &format!("{}:refs/collab/patches/{}/rev/{}", planted, id, feat), 2398 &format!("{}:refs/collab/patches/{}/rev/{}", planted, id, feat),
2385 &format!("{}:refs/collab/patches/{}/r/9", planted, id), 2399 &format!("{}:refs/collab/patches/{}/r/9", planted, id),
@@ -2460,9 +2474,17 @@ fn concurrent_revise_leaves_every_revision_reachable() {
2460 &["patch", "revise", short, "-B", "feat"], 2474 &["patch", "revise", short, "-B", "feat"],
2461 ); 2475 );
2462 2476
2463 sync::sync(&Repository::open(cluster.alice_dir.path()).unwrap(), "origin").unwrap(); 2477 sync::sync(
2478 &Repository::open(cluster.alice_dir.path()).unwrap(),
2479 "origin",
2480 )
2481 .unwrap();
2464 sync::sync(&Repository::open(cluster.bob_dir.path()).unwrap(), "origin").unwrap(); 2482 sync::sync(&Repository::open(cluster.bob_dir.path()).unwrap(), "origin").unwrap();
2465 sync::sync(&Repository::open(cluster.alice_dir.path()).unwrap(), "origin").unwrap(); 2483 sync::sync(
2484 &Repository::open(cluster.alice_dir.path()).unwrap(),
2485 "origin",
2486 )
2487 .unwrap();
2466 2488
2467 for (label, dir) in [ 2489 for (label, dir) in [
2468 ("alice", cluster.alice_dir.path()), 2490 ("alice", cluster.alice_dir.path()),
@@ -2541,10 +2563,7 @@ fn a_stale_revision_ref_on_the_remote_does_not_wedge_a_later_sync() {
2541 .args([ 2563 .args([
2542 "push", 2564 "push",
2543 "origin", 2565 "origin",
2544 &format!( 2566 &format!("{}:refs/collab/patches/{}/rev/{}", stranded, id, stranded),
2545 "{}:refs/collab/patches/{}/rev/{}",
2546 stranded, id, stranded
2547 ),
2548 ]) 2567 ])
2549 .current_dir(cluster.alice_dir.path()) 2568 .current_dir(cluster.alice_dir.path())
2550 .status() 2569 .status()
@@ -2573,11 +2592,7 @@ fn a_stale_revision_ref_on_the_remote_does_not_wedge_a_later_sync() {
2573 2592
2574 // Bob's revision reached the remote, and a third clone picks it up cleanly. 2593 // Bob's revision reached the remote, and a third clone picks it up cleanly.
2575 let carol_dir = TempDir::new().unwrap(); 2594 let carol_dir = TempDir::new().unwrap();
2576 let carol = Repository::clone( 2595 let carol = Repository::clone(cluster.bare_dir().to_str().unwrap(), carol_dir.path()).unwrap();
2577 cluster.bare_dir().to_str().unwrap(),
2578 carol_dir.path(),
2579 )
2580 .unwrap();
2581 { 2596 {
2582 let mut config = carol.config().unwrap(); 2597 let mut config = carol.config().unwrap();
2583 config.set_str("user.name", "Carol").unwrap(); 2598 config.set_str("user.name", "Carol").unwrap();
tests/timeline_test.rs
Old New
@@ -23,7 +23,14 @@ use common::TestRepo;
23 fn patch_with_a_review_round(repo: &TestRepo) -> String { 23 fn patch_with_a_review_round(repo: &TestRepo) -> String {
24 repo.git(&["checkout", "-b", "feat-timeline"]); 24 repo.git(&["checkout", "-b", "feat-timeline"]);
25 repo.commit_file("a.txt", "first", "v1"); 25 repo.commit_file("a.txt", "first", "v1");
26 let out = repo.run_ok(&["patch", "create", "-t", "Timeline patch", "-B", "feat-timeline"]); 26 let out = repo.run_ok(&[
27 "patch",
28 "create",
29 "-t",
30 "Timeline patch",
31 "-B",
32 "feat-timeline",
33 ]);
27 let id = out 34 let id = out
28 .trim() 35 .trim()
29 .strip_prefix("Created patch ") 36 .strip_prefix("Created patch ")
@@ -62,14 +69,22 @@ fn timeline_interleaves_revisions_comments_and_reviews_in_order() {
62 // Every kind of event is present. `patch log` alone shows only the 69 // Every kind of event is present. `patch log` alone shows only the
63 // revisions; `patch show` alone shows only the comment and the reviews. 70 // revisions; `patch show` alone shows only the comment and the reviews.
64 assert!(out.contains("r1"), "timeline must show revision 1: {}", out); 71 assert!(out.contains("r1"), "timeline must show revision 1: {}", out);
65 assert!(out.contains("needs a test"), "timeline must show the comment: {}", out); 72 assert!(
73 out.contains("needs a test"),
74 "timeline must show the comment: {}",
75 out
76 );
66 assert!( 77 assert!(
67 out.contains("request-changes"), 78 out.contains("request-changes"),
68 "timeline must show the review verdict: {}", 79 "timeline must show the review verdict: {}",
69 out 80 out
70 ); 81 );
71 assert!(out.contains("r2"), "timeline must show revision 2: {}", out); 82 assert!(out.contains("r2"), "timeline must show revision 2: {}", out);
72 assert!(out.contains("approve"), "timeline must show the approval: {}", out); 83 assert!(
84 out.contains("approve"),
85 "timeline must show the approval: {}",
86 out
87 );
73 88
74 // ...and in the order they happened. This is the assertion that makes it a 89 // ...and in the order they happened. This is the assertion that makes it a
75 // timeline rather than two lists printed one after the other. 90 // timeline rather than two lists printed one after the other.
@@ -206,7 +221,10 @@ fn timeline_json_carries_the_same_sequence() {
206 assert_eq!(entries[1]["revision"], 1, "the comment was made against r1"); 221 assert_eq!(entries[1]["revision"], 1, "the comment was made against r1");
207 assert_eq!(entries[2]["revision"], 1, "the review was made against r1"); 222 assert_eq!(entries[2]["revision"], 1, "the review was made against r1");
208 assert_eq!(entries[3]["revision"], 2); 223 assert_eq!(entries[3]["revision"], 2);
209 assert_eq!(entries[4]["revision"], 2, "the approval was made against r2"); 224 assert_eq!(
225 entries[4]["revision"], 2,
226 "the approval was made against r2"
227 );
210 228
211 assert_eq!(entries[2]["verdict"], "request-changes"); 229 assert_eq!(entries[2]["verdict"], "request-changes");
212 assert_eq!(entries[4]["verdict"], "approve"); 230 assert_eq!(entries[4]["verdict"], "approve");
@@ -362,7 +380,9 @@ fn timeline_ignores_a_correction_the_fold_refused() {
362 .filter_map(|r| r.ok()) 380 .filter_map(|r| r.ok())
363 .find_map(|r| { 381 .find_map(|r| {
364 let name = r.name()?.to_string(); 382 let name = r.name()?.to_string();
365 let full = name.strip_prefix("refs/collab/patches/")?.strip_suffix("/events")?; 383 let full = name
384 .strip_prefix("refs/collab/patches/")?
385 .strip_suffix("/events")?;
366 full.starts_with(&id).then(|| full.to_string()) 386 full.starts_with(&id).then(|| full.to_string())
367 }) 387 })
368 .expect("patch events ref") 388 .expect("patch events ref")
tests/trailer_test.rs
Old New
@@ -38,7 +38,10 @@ fn empty_message() {
38 38
39 #[test] 39 #[test]
40 fn single_trailer_in_pure_block() { 40 fn single_trailer_in_pure_block() {
41 both_tokens("Fix thing\n\nSome context in the body.\n\n{}: abc", &["abc"]); 41 both_tokens(
42 "Fix thing\n\nSome context in the body.\n\n{}: abc",
43 &["abc"],
44 );
42 } 45 }
43 46
44 #[test] 47 #[test]
tests/tui_review_test.rs
Old New
@@ -17,7 +17,10 @@ use std::time::Duration;
17 fn editor_writing(repo: &TestRepo, name: &str, body: &str) -> String { 17 fn editor_writing(repo: &TestRepo, name: &str, body: &str) -> String {
18 repo.write_script( 18 repo.write_script(
19 name, 19 name,
20 &format!("#!/bin/sh\ncat > \"$1\" <<'GITCOLLABEOF'\n{}\nGITCOLLABEOF\n", body), 20 &format!(
21 "#!/bin/sh\ncat > \"$1\" <<'GITCOLLABEOF'\n{}\nGITCOLLABEOF\n",
22 body
23 ),
21 ) 24 )
22 } 25 }
23 26
@@ -26,8 +29,19 @@ fn repo_with_patch() -> (TestRepo, String) {
26 let repo = TestRepo::new("Reviewer", "reviewer@example.com"); 29 let repo = TestRepo::new("Reviewer", "reviewer@example.com");
27 repo.commit_file("src/lib.rs", "one\ntwo\nthree\nfour\n", "seed"); 30 repo.commit_file("src/lib.rs", "one\ntwo\nthree\nfour\n", "seed");
28 repo.git(&["checkout", "-b", "feature/x"]); 31 repo.git(&["checkout", "-b", "feature/x"]);
29 repo.commit_file("src/lib.rs", "one\ntwo changed\nthree\nfour\nfive\n", "work"); 32 repo.commit_file(
30 let out = repo.run_ok(&["patch", "create", "-t", "A patch to review", "-B", "feature/x"]); 33 "src/lib.rs",
34 "one\ntwo changed\nthree\nfour\nfive\n",
35 "work",
36 );
37 let out = repo.run_ok(&[
38 "patch",
39 "create",
40 "-t",
41 "A patch to review",
42 "-B",
43 "feature/x",
44 ]);
31 let id = out 45 let id = out
32 .trim() 46 .trim()
33 .strip_prefix("Created patch ") 47 .strip_prefix("Created patch ")
@@ -158,8 +172,19 @@ fn a_diff_line_wider_than_the_pane_is_not_clipped_away() {
158 // Far wider than the ~63 columns the detail pane gets at 100 across, with 172 // Far wider than the ~63 columns the detail pane gets at 100 across, with
159 // the far end named so the test can look for it rather than for a length. 173 // the far end named so the test can look for it rather than for a length.
160 let wide = format!("let x = \"{}\"; // ENDOFTHEWIDELINE", "w".repeat(300)); 174 let wide = format!("let x = \"{}\"; // ENDOFTHEWIDELINE", "w".repeat(300));
161 repo.commit_file("src/lib.rs", &format!("one\ntwo\n{}\n", wide), "a wide line"); 175 repo.commit_file(
162 repo.run_ok(&["patch", "create", "-t", "A wide patch", "-B", "feature/wide"]); 176 "src/lib.rs",
177 &format!("one\ntwo\n{}\n", wide),
178 "a wide line",
179 );
180 repo.run_ok(&[
181 "patch",
182 "create",
183 "-t",
184 "A wide patch",
185 "-B",
186 "feature/wide",
187 ]);
163 repo.git(&["checkout", "main"]); 188 repo.git(&["checkout", "main"]);
164 189
165 let out = repo.run_dashboard_driven(100, 45, &[], |dash| { 190 let out = repo.run_dashboard_driven(100, 45, &[], |dash| {
tests/version_test.rs
Old New
@@ -79,7 +79,10 @@ fn server_binary_reports_version_with_build_commit() {
79 "git-collab-server --version should exit 0: {}", 79 "git-collab-server --version should exit 0: {}",
80 String::from_utf8_lossy(&output.stderr) 80 String::from_utf8_lossy(&output.stderr)
81 ); 81 );
82 assert_version_output("git-collab-server", &String::from_utf8_lossy(&output.stdout)); 82 assert_version_output(
83 "git-collab-server",
84 &String::from_utf8_lossy(&output.stdout),
85 );
83 } 86 }
84 87
85 /// `--version` is a question about the binary, not about a repository, so it 88 /// `--version` is a question about the binary, not about a repository, so it
@@ -201,7 +204,10 @@ fn a_passed_commit_is_used_when_there_is_no_checkout() {
201 /// wrapper that knows more than the build tree does. 204 /// wrapper that knows more than the build tree does.
202 #[test] 205 #[test]
203 fn a_passed_commit_overrides_the_discovered_one() { 206 fn a_passed_commit_overrides_the_discovered_one() {
204 assert_eq!(resolve_build_commit(Some(FULL), Some(OTHER)), Ok(Some(FULL))); 207 assert_eq!(
208 resolve_build_commit(Some(FULL), Some(OTHER)),
209 Ok(Some(FULL))
210 );
205 } 211 }
206 212
207 #[test] 213 #[test]
@@ -349,7 +355,9 @@ fn main() {
349 for (key, value) in env { 355 for (key, value) in env {
350 cmd.env(key, value); 356 cmd.env(key, value);
351 } 357 }
352 let out = cmd.output().expect("failed to run cargo for the probe crate"); 358 let out = cmd
359 .output()
360 .expect("failed to run cargo for the probe crate");
353 assert!( 361 assert!(
354 out.status.success(), 362 out.status.success(),
355 "probe build failed: {}", 363 "probe build failed: {}",
tests/web_rendering_test.rs
Old New
@@ -101,7 +101,10 @@ fn list_timestamps_are_short_with_the_full_value_on_hover() {
101 format!("/{}/patches", harness.repo_name()), 101 format!("/{}/patches", harness.repo_name()),
102 &full_patch_stamp, 102 &full_patch_stamp,
103 ), 103 ),
104 (format!("/{}/issues", harness.repo_name()), &full_issue_stamp), 104 (
105 format!("/{}/issues", harness.repo_name()),
106 &full_issue_stamp,
107 ),
105 ] { 108 ] {
106 let body = harness.get_ok(&path).body; 109 let body = harness.get_ok(&path).body;
107 assert!( 110 assert!(
@@ -125,9 +128,9 @@ fn list_timestamps_are_short_with_the_full_value_on_hover() {
125 fn patch_detail_timestamps_are_short_with_the_full_value_on_hover() { 128 fn patch_detail_timestamps_are_short_with_the_full_value_on_hover() {
126 let harness = ServerHarness::new("render-detail-stamps"); 129 let harness = ServerHarness::new("render-detail-stamps");
127 let patch_id = patch_on_ephemeral_branch(&harness, "Reviewed patch", "feat/reviewed"); 130 let patch_id = patch_on_ephemeral_branch(&harness, "Reviewed patch", "feat/reviewed");
128 harness 131 harness.work_repo().run_ok(&[
129 .work_repo() 132 "patch", "review", &patch_id, "-v", "approve", "-b", "Looks ok",
130 .run_ok(&["patch", "review", &patch_id, "-v", "approve", "-b", "Looks ok"]); 133 ]);
131 harness 134 harness
132 .work_repo() 135 .work_repo()
133 .run_ok(&["patch", "comment", &patch_id, "-b", "A thread comment"]); 136 .run_ok(&["patch", "comment", &patch_id, "-b", "A thread comment"]);
@@ -135,11 +138,7 @@ fn patch_detail_timestamps_are_short_with_the_full_value_on_hover() {
135 harness.push_collab_refs(); 138 harness.push_collab_refs();
136 139
137 let body = harness 140 let body = harness
138 .get_ok(&format!( 141 .get_ok(&format!("/{}/patches/{}", harness.repo_name(), patch_id))
139 "/{}/patches/{}",
140 harness.repo_name(),
141 patch_id
142 ))
143 .body; 142 .body;
144 assert!( 143 assert!(
145 !has_nanosecond_timestamp(&body), 144 !has_nanosecond_timestamp(&body),
@@ -171,11 +170,7 @@ fn merged_in_is_abbreviated_like_the_revision_commits_beside_it() {
171 assert_eq!(merge_commit.len(), 40, "the event records the full oid"); 170 assert_eq!(merge_commit.len(), 40, "the event records the full oid");
172 171
173 let body = harness 172 let body = harness
174 .get_ok(&format!( 173 .get_ok(&format!("/{}/patches/{}", harness.repo_name(), patch_id))
175 "/{}/patches/{}",
176 harness.repo_name(),
177 patch_id
178 ))
179 .body; 174 .body;
180 175
181 // The link still goes to the full oid; only the text is abbreviated. 176 // The link still goes to the full oid; only the text is abbreviated.
@@ -308,11 +303,7 @@ fn the_patch_detail_page_still_records_the_branch_it_came_from() {
308 harness.push_collab_refs(); 303 harness.push_collab_refs();
309 304
310 let body = harness 305 let body = harness
311 .get_ok(&format!( 306 .get_ok(&format!("/{}/patches/{}", harness.repo_name(), patch_id))
312 "/{}/patches/{}",
313 harness.repo_name(),
314 patch_id
315 ))
316 .body; 307 .body;
317 assert!( 308 assert!(
318 body.contains(EPHEMERAL_BRANCH), 309 body.contains(EPHEMERAL_BRANCH),