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