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