1pub mod memory;
6
7mod diff;
8mod repo;
9mod revision;
10
11pub use diff::{DiffLine, FileDiff, Hunk, HunkKind, LineOrigin, Sign};
12pub use repo::{GitContext, GitError, Repo};
13pub use revision::{GitRevision, SourceLocation};
14
15#[cfg(test)]
16mod tests {
17 use super::*;
18 use std::path::{Path, PathBuf};
19 use std::process::Command;
20
21 pub(crate) fn git(root: &std::path::Path, args: &[&str]) {
22 Command::new("git")
23 .args(args)
24 .current_dir(root)
25 .output()
26 .unwrap();
27 }
28
29 pub(crate) fn fixture() -> (tempfile::TempDir, Repo, PathBuf) {
30 let dir = tempfile::tempdir().unwrap();
31 let root = dir.path();
32 git(root, &["init", "-q"]);
33 git(root, &["config", "user.email", "t@t.t"]);
34 git(root, &["config", "user.name", "t"]);
35 std::fs::write(root.join("f.rs"), "fn a() {}\nfn b() {}\nfn c() {}\n").unwrap();
36 git(root, &["add", "."]);
37 git(root, &["commit", "-qm", "init"]);
38 let repo = Repo::discover(root).unwrap();
39 let file = root.join("f.rs");
40 (dir, repo, file)
41 }
42
43 #[test]
44 fn clean_buffer_has_no_hunks() {
45 let (_d, repo, path) = fixture();
46 let content = repo.head_content(&path).unwrap();
47 assert!(repo.hunks(&path, &content).unwrap().is_empty());
48 }
49
50 #[test]
51 fn change_and_add_and_delete() {
52 let (_d, repo, path) = fixture();
53 let edited = "fn a() {}\nfn b2() {}\nfn c() {}\nfn d() {}\n";
54 let hunks = repo.hunks(&path, edited).unwrap();
55 assert_eq!(hunks.len(), 1);
56 assert_eq!(hunks[0].kind, HunkKind::Change);
57 assert!(hunks[0].covers(2, 4));
58 assert!(hunks[0].covers(4, 4));
59 assert!(!hunks[0].covers(1, 4));
60 assert!(hunks[0]
61 .lines
62 .iter()
63 .any(|l| l.origin == LineOrigin::Addition && l.text.starts_with(b"fn d")));
64 }
65
66 #[test]
69 fn line_numbers_track_both_sides() {
70 let (_d, repo, path) = fixture();
71 let edited = "fn a() {}\nfn b2() {}\nfn c() {}\nfn d() {}\n";
72 let hunks = repo.hunks(&path, edited).unwrap();
73 assert_eq!(hunks.len(), 1);
74 let h = &hunks[0];
75 let ctx = h
76 .lines
77 .iter()
78 .find(|l| l.origin == LineOrigin::Context)
79 .unwrap();
80 assert_eq!(
81 (ctx.old_lineno, ctx.new_lineno),
82 (Some(1), Some(1)),
83 "context lines carry both numbers, 1-based"
84 );
85 let add = h
86 .lines
87 .iter()
88 .find(|l| l.origin == LineOrigin::Addition && l.text.starts_with(b"fn d"))
89 .unwrap();
90 assert_eq!((add.old_lineno, add.new_lineno), (None, Some(4)));
91 let del = h
92 .lines
93 .iter()
94 .find(|l| l.origin == LineOrigin::Deletion)
95 .unwrap();
96 assert_eq!((del.old_lineno, del.new_lineno), (Some(2), None));
97 }
98
99 #[test]
100 fn pure_delete_marks_following_line() {
101 let (_d, repo, path) = fixture();
102 let edited = "fn a() {}\nfn c() {}\n";
103 let hunks = repo.hunks(&path, edited).unwrap();
104 assert_eq!(hunks.len(), 1);
105 assert_eq!(hunks[0].kind, HunkKind::Delete);
106 assert!(hunks[0].covers(2, 4)); }
108
109 #[test]
110 fn stage_hunk_applies_to_index() {
111 let (_d, repo, path) = fixture();
112 let edited = "fn a() {}\nfn b() {}\nfn c() {}\nfn d() {}\n";
113 let hunks = repo.hunks(&path, edited).unwrap();
114 assert_eq!(hunks.len(), 1);
115 assert_eq!(hunks[0].kind, HunkKind::Add);
116 let root = repo.workdir.clone();
117 repo.stage_hunk(Path::new("f.rs"), &hunks[0]).unwrap();
118 let out = Command::new("git")
119 .args([
120 "-C",
121 &root.display().to_string(),
122 "diff",
123 "--cached",
124 "--stat",
125 ])
126 .output()
127 .unwrap();
128 let stat = String::from_utf8_lossy(&out.stdout);
129 assert!(stat.contains("f.rs"), "{stat}");
130 }
131
132 #[test]
137 fn stage_hunk_is_byte_precise() {
138 let (_d, repo, path) = fixture();
139 let edited = "fn a() {}\nfn b2() {}\nfn c() {}\n";
140 let hunks = repo.hunks(&path, edited).unwrap();
141 assert_eq!(hunks.len(), 1);
142 repo.stage_hunk(Path::new("f.rs"), &hunks[0]).unwrap();
143 assert_eq!(
145 repo.index_content(&path).as_deref(),
146 Some("fn a() {}\nfn b2() {}\nfn c() {}\n")
147 );
148 assert_eq!(
149 repo.head_content(&path).as_deref(),
150 Some("fn a() {}\nfn b() {}\nfn c() {}\n")
151 );
152 let staged = repo.staged_hunks(&path).unwrap();
154 assert_eq!(staged.len(), 1);
155 repo.unstage_hunk(Path::new("f.rs"), &staged[0]).unwrap();
156 assert_eq!(
157 repo.index_content(&path).as_deref(),
158 Some("fn a() {}\nfn b() {}\nfn c() {}\n")
159 );
160 }
161
162 #[test]
163 fn stage_hunk_preserves_a_missing_final_newline() {
164 let (_d, repo, path) = fixture();
165 let edited = "fn a() {}\nfn b() {}\nfn c() {}";
167 let hunks = repo.hunks(&path, edited).unwrap();
168 repo.stage_hunk(Path::new("f.rs"), &hunks[0]).unwrap();
169 assert_eq!(repo.index_content(&path).as_deref(), Some(edited));
170 let staged = repo.staged_hunks(&path).unwrap();
171 repo.unstage_hunk(Path::new("f.rs"), &staged[0]).unwrap();
172 assert_eq!(
173 repo.index_content(&path).as_deref(),
174 Some("fn a() {}\nfn b() {}\nfn c() {}\n"),
175 "unstage restores the newline-terminated HEAD text"
176 );
177 }
178
179 #[test]
182 fn commit_file_diff_is_structured() {
183 let (_d, repo, path) = fixture();
184 let root = repo.workdir.clone();
185 std::fs::write(root.join("f.rs"), "fn a() {}\nfn b2() {}\nfn c() {}\n").unwrap();
186 git(&root, &["add", "."]);
187 git(&root, &["commit", "-qm", "change b"]);
188 let sha = String::from_utf8_lossy(
189 &Command::new("git")
190 .args(["-C", &root.display().to_string(), "rev-parse", "HEAD"])
191 .output()
192 .unwrap()
193 .stdout,
194 )
195 .trim()
196 .to_string();
197 let diff = repo.commit_file_diff(&sha, Path::new("f.rs")).unwrap();
198 assert_eq!(diff.added, 1);
199 assert_eq!(diff.deleted, 1);
200 assert_eq!(diff.hunks.len(), 1);
201 assert_eq!(diff.hunks[0].kind, HunkKind::Change);
202 assert!(diff.hunks[0].lines.iter().any(|l| l.text == b"fn b2() {}"));
203 let _ = path;
204 }
205
206 #[test]
209 fn commit_file_diff_root_commit() {
210 let (_d, repo, _path) = fixture();
211 let root = repo.workdir.clone();
212 let sha = String::from_utf8_lossy(
213 &Command::new("git")
214 .args(["-C", &root.display().to_string(), "rev-parse", "HEAD"])
215 .output()
216 .unwrap()
217 .stdout,
218 )
219 .trim()
220 .to_string();
221 let diff = repo.commit_file_diff(&sha, Path::new("f.rs")).unwrap();
222 assert_eq!(diff.added, 3);
223 assert_eq!(diff.deleted, 0);
224 assert!(diff
225 .hunks
226 .iter()
227 .all(|h| h.lines.iter().all(|l| l.old_lineno.is_none())));
228 }
229
230 #[test]
233 fn outside_workdir_is_typed_not_empty() {
234 let (_d, repo, _path) = fixture();
235 let outside = std::env::temp_dir().join("strop-outside-f.rs");
236 assert!(matches!(
237 repo.hunks(&outside, "x\n"),
238 Err(GitError::OutsideWorkdir)
239 ));
240 assert!(matches!(
241 repo.unstaged_hunks(&outside, "x\n"),
242 Err(GitError::OutsideWorkdir)
243 ));
244 assert!(matches!(
245 repo.staged_hunks(&outside),
246 Err(GitError::OutsideWorkdir)
247 ));
248 }
249
250 #[test]
254 fn untracked_file_is_all_add_not_failure() {
255 let (d, repo, _path) = fixture();
256 let untracked = d.path().join("new.rs");
257 std::fs::write(&untracked, "fn n() {}\n").unwrap();
258 assert!(repo.is_untracked(&untracked).unwrap());
259 let hunks = repo.unstaged_hunks(&untracked, "fn n() {}\n").unwrap();
260 assert_eq!(hunks.len(), 1);
261 assert_eq!(hunks[0].kind, HunkKind::Add);
262 assert_eq!(hunks[0].old_count, 0);
263 assert!(repo.unstaged_hunks(&untracked, "").unwrap().is_empty());
265 assert!(!repo.is_untracked(&_path).unwrap());
267 }
268
269 #[test]
272 fn unborn_head_stages_all_add() {
273 let dir = tempfile::tempdir().unwrap();
274 let root = dir.path();
275 git(root, &["init", "-q"]);
276 let repo = Repo::discover(root).unwrap();
277 std::fs::write(root.join("f.rs"), "fn a() {}\n").unwrap();
278 git(root, &["add", "f.rs"]);
279 let staged = repo.staged_hunks(&root.join("f.rs")).unwrap();
280 assert_eq!(staged.len(), 1);
281 assert_eq!(staged[0].kind, HunkKind::Add);
282 let ctx = repo.context();
284 assert_eq!(ctx.head_sha, None);
285 assert_eq!(ctx.workdir, root.to_path_buf());
286 }
287
288 #[test]
291 fn git_context_serde_and_equality() {
292 let (d, repo, _path) = fixture();
293 let ctx = repo.context();
294 let wire = serde_json::to_string(&ctx).unwrap();
295 assert_eq!(serde_json::from_str::<GitContext>(&wire).unwrap(), ctx);
296 assert!(ctx.head_sha.is_some());
297 std::fs::write(d.path().join("f.rs"), "fn a() {}\nfn z() {}\n").unwrap();
300 git(d.path(), &["commit", "-qam", "z"]);
301 let repo2 = Repo::discover(d.path()).unwrap();
302 assert_ne!(repo2.context(), ctx);
303 }
304}