1use std::cell::RefCell;
8use std::collections::{HashMap, HashSet};
9
10use super::{
11 ChangeStatus, CommitInfo, Diff, DiffLine, DiffLineKind, FileChange, RefKind, RefLabel,
12 RepoBackend, WorkingStatus,
13};
14
15pub struct FileEntry {
17 pub change: FileChange,
18 pub diff: Diff,
19}
20
21struct WorkingEntry {
26 change: FileChange,
27 diff: Diff,
28 staged: bool,
29}
30
31pub struct FixtureBackend {
32 path: String,
33 commits: Vec<CommitInfo>,
34 files: HashMap<usize, Vec<FileEntry>>,
35 working: RefCell<Vec<WorkingEntry>>,
37 amend_removed: RefCell<HashSet<String>>,
40 last_commit: RefCell<Option<(String, bool)>>,
42 signature: Option<(String, String)>,
44}
45
46impl FixtureBackend {
47 pub fn new(path: impl Into<String>) -> Self {
48 Self {
49 path: path.into(),
50 commits: Vec::new(),
51 files: HashMap::new(),
52 working: RefCell::new(Vec::new()),
53 amend_removed: RefCell::new(HashSet::new()),
54 last_commit: RefCell::new(None),
55 signature: Some(("Robert Lillack".to_string(), "rob@example.com".to_string())),
56 }
57 }
58
59 pub fn with_signature(mut self, signature: Option<(String, String)>) -> Self {
61 self.signature = signature;
62 self
63 }
64
65 fn head_files(&self) -> &[FileEntry] {
68 self.files.get(&0).map(Vec::as_slice).unwrap_or(&[])
69 }
70
71 pub fn add_commit(&mut self, info: CommitInfo, files: Vec<FileEntry>) -> &mut Self {
73 let idx = self.commits.len();
74 self.commits.push(info);
75 self.files.insert(idx, files);
76 self
77 }
78
79 pub fn add_working(
81 &mut self,
82 path: &str,
83 status: ChangeStatus,
84 staged: bool,
85 diff_lines: &[(DiffLineKind, &str)],
86 ) -> &mut Self {
87 self.working.borrow_mut().push(WorkingEntry {
88 change: FileChange {
89 path: path.to_string(),
90 old_path: None,
91 status,
92 },
93 diff: diff(diff_lines),
94 staged,
95 });
96 self
97 }
98
99 pub fn last_commit(&self) -> Option<(String, bool)> {
102 self.last_commit.borrow().clone()
103 }
104
105 pub fn sample() -> Self {
108 let mut be = FixtureBackend::new("/home/rob/dev/journey");
109
110 be.add_commit(
111 commit(
112 "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678",
113 "Add commit DAG graph view",
114 "Robert Lillack",
115 "rob@example.com",
116 1_716_500_000,
117 120,
118 &["b2c3d4e5f60718293a4b5c6d7e8f90123456789a"],
119 &[("main", RefKind::Head)],
120 ),
121 vec![file_entry(
122 "src/widgets/graph.rs",
123 None,
124 ChangeStatus::Added,
125 &[
126 (
127 DiffLineKind::FileHeader,
128 "diff --git a/src/widgets/graph.rs b/src/widgets/graph.rs",
129 ),
130 (DiffLineKind::FileHeader, "new file mode 100644"),
131 (DiffLineKind::HunkHeader, "@@ -0,0 +1,3 @@"),
132 (DiffLineKind::Addition, "+pub struct Graph {"),
133 (DiffLineKind::Addition, "+ lanes: Vec<Lane>,"),
134 (DiffLineKind::Addition, "+}"),
135 ],
136 )],
137 );
138
139 be.add_commit(
140 commit(
141 "b2c3d4e5f60718293a4b5c6d7e8f90123456789a",
142 "Build basic file list per commit",
143 "Robert Lillack",
144 "rob@example.com",
145 1_716_400_000,
146 120,
147 &["c3d4e5f60718293a4b5c6d7e8f90123456789ab2"],
148 &[("v0.2", RefKind::Tag)],
149 ),
150 vec![
151 file_entry(
152 "src/backend.rs",
153 None,
154 ChangeStatus::Modified,
155 &[
156 (
157 DiffLineKind::FileHeader,
158 "diff --git a/src/backend.rs b/src/backend.rs",
159 ),
160 (
161 DiffLineKind::HunkHeader,
162 "@@ -10,6 +10,10 @@ impl Backend {",
163 ),
164 (
165 DiffLineKind::Context,
166 " pub fn log(&self) -> Vec<Commit> {",
167 ),
168 (
169 DiffLineKind::Addition,
170 "+ // collect changed files too",
171 ),
172 (DiffLineKind::Addition, "+ self.changed_files();"),
173 (DiffLineKind::Context, " self.commits.clone()"),
174 (DiffLineKind::Context, " }"),
175 ],
176 ),
177 file_entry(
178 "src/main.rs",
179 None,
180 ChangeStatus::Modified,
181 &[
182 (
183 DiffLineKind::FileHeader,
184 "diff --git a/src/main.rs b/src/main.rs",
185 ),
186 (DiffLineKind::HunkHeader, "@@ -42,7 +42,7 @@"),
187 (DiffLineKind::Deletion, "- let files = vec![];"),
188 (
189 DiffLineKind::Addition,
190 "+ let files = backend.changed_files(idx);",
191 ),
192 ],
193 ),
194 ],
195 );
196
197 be.add_commit(
198 commit(
199 "c3d4e5f60718293a4b5c6d7e8f90123456789ab2",
200 "Show path in title",
201 "Robert Lillack",
202 "rob@example.com",
203 1_716_300_000,
204 120,
205 &["d4e5f60718293a4b5c6d7e8f90123456789ab2c3"],
206 &[],
207 ),
208 vec![file_entry(
209 "src/main.rs",
210 None,
211 ChangeStatus::Modified,
212 &[
213 (
214 DiffLineKind::FileHeader,
215 "diff --git a/src/main.rs b/src/main.rs",
216 ),
217 (DiffLineKind::HunkHeader, "@@ -20,1 +20,1 @@ fn title()"),
218 (DiffLineKind::Deletion, "- String::from(\"Journey\")"),
219 (
220 DiffLineKind::Addition,
221 "+ format!(\"Journey: {}\", path)",
222 ),
223 ],
224 )],
225 );
226
227 be.add_commit(
228 commit(
229 "d4e5f60718293a4b5c6d7e8f90123456789ab2c3",
230 "Rename boldFont() -> bold_font()",
231 "Robert Lillack",
232 "rob@example.com",
233 1_716_200_000,
234 120,
235 &["e5f60718293a4b5c6d7e8f90123456789ab2c3d4"],
236 &[("origin/main", RefKind::RemoteBranch)],
237 ),
238 vec![file_entry(
239 "src/style.rs",
240 None,
241 ChangeStatus::Modified,
242 &[
243 (
244 DiffLineKind::FileHeader,
245 "diff --git a/src/style.rs b/src/style.rs",
246 ),
247 (DiffLineKind::HunkHeader, "@@ -80,4 +80,4 @@"),
248 (DiffLineKind::Deletion, "-pub fn boldFont() -> Font {"),
249 (DiffLineKind::Addition, "+pub fn bold_font() -> Font {"),
250 ],
251 )],
252 );
253
254 be.add_commit(
255 commit(
256 "e5f60718293a4b5c6d7e8f90123456789ab2c3d4",
257 "Initial import",
258 "Robert Lillack",
259 "rob@example.com",
260 1_716_100_000,
261 120,
262 &[],
263 &[],
264 ),
265 vec![
266 file_entry(
267 "Cargo.toml",
268 None,
269 ChangeStatus::Added,
270 &[
271 (
272 DiffLineKind::FileHeader,
273 "diff --git a/Cargo.toml b/Cargo.toml",
274 ),
275 (DiffLineKind::FileHeader, "new file mode 100644"),
276 (DiffLineKind::HunkHeader, "@@ -0,0 +1,2 @@"),
277 (DiffLineKind::Addition, "+[package]"),
278 (DiffLineKind::Addition, "+name = \"journey\""),
279 ],
280 ),
281 file_entry(
282 "src/main.rs",
283 None,
284 ChangeStatus::Added,
285 &[
286 (
287 DiffLineKind::FileHeader,
288 "diff --git a/src/main.rs b/src/main.rs",
289 ),
290 (DiffLineKind::FileHeader, "new file mode 100644"),
291 (DiffLineKind::HunkHeader, "@@ -0,0 +1,1 @@"),
292 (DiffLineKind::Addition, "+fn main() {}"),
293 ],
294 ),
295 ],
296 );
297
298 be.add_working(
301 "src/ui.rs",
302 ChangeStatus::Modified,
303 false,
304 &[
305 (
306 DiffLineKind::FileHeader,
307 "diff --git a/src/ui.rs b/src/ui.rs",
308 ),
309 (
310 DiffLineKind::HunkHeader,
311 "@@ -40,6 +40,9 @@ impl GitClient {",
312 ),
313 (DiffLineKind::Context, " fn sync(&mut self) {"),
314 (
315 DiffLineKind::Addition,
316 "+ // refresh the working-tree panes",
317 ),
318 (DiffLineKind::Addition, "+ self.rescan();"),
319 (DiffLineKind::Context, " self.repaint();"),
320 (DiffLineKind::Context, " }"),
321 ],
322 );
323 be.add_working(
324 "notes.md",
325 ChangeStatus::Untracked,
326 false,
327 &[
328 (DiffLineKind::FileHeader, "diff --git a/notes.md b/notes.md"),
329 (DiffLineKind::FileHeader, "new file mode 100644"),
330 (DiffLineKind::HunkHeader, "@@ -0,0 +1,2 @@"),
331 (DiffLineKind::Addition, "+# Notes"),
332 (DiffLineKind::Addition, "+- wire up commit mode"),
333 ],
334 );
335 be.add_working(
336 "src/widgets/commit_panel.rs",
337 ChangeStatus::Added,
338 true,
339 &[
340 (
341 DiffLineKind::FileHeader,
342 "diff --git a/src/widgets/commit_panel.rs b/src/widgets/commit_panel.rs",
343 ),
344 (DiffLineKind::FileHeader, "new file mode 100644"),
345 (DiffLineKind::HunkHeader, "@@ -0,0 +1,3 @@"),
346 (DiffLineKind::Addition, "+pub struct CommitPanel {"),
347 (DiffLineKind::Addition, "+ message: String,"),
348 (DiffLineKind::Addition, "+}"),
349 ],
350 );
351 be.add_working(
352 "Cargo.toml",
353 ChangeStatus::Modified,
354 true,
355 &[
356 (
357 DiffLineKind::FileHeader,
358 "diff --git a/Cargo.toml b/Cargo.toml",
359 ),
360 (
361 DiffLineKind::HunkHeader,
362 "@@ -8,3 +8,4 @@ edition = \"2024\"",
363 ),
364 (DiffLineKind::Context, " [dependencies]"),
365 (
366 DiffLineKind::Addition,
367 "+git2 = { version = \"0.18\", default-features = false }",
368 ),
369 (
370 DiffLineKind::Context,
371 " saudade = { path = \"../saudade\" }",
372 ),
373 ],
374 );
375
376 be
377 }
378}
379
380impl RepoBackend for FixtureBackend {
381 fn path(&self) -> &str {
382 &self.path
383 }
384
385 fn commits(&self) -> &[CommitInfo] {
386 &self.commits
387 }
388
389 fn changed_files(&self, index: usize) -> Vec<FileChange> {
390 self.files
391 .get(&index)
392 .map(|entries| entries.iter().map(|e| e.change.clone()).collect())
393 .unwrap_or_default()
394 }
395
396 fn commit_diff(&self, index: usize) -> Diff {
397 let mut lines = Vec::new();
398 if let Some(entries) = self.files.get(&index) {
399 for entry in entries {
400 lines.extend(entry.diff.lines.iter().cloned());
401 }
402 }
403 Diff { lines }
404 }
405
406 fn file_diff(&self, index: usize, path: &str) -> Diff {
407 self.files
408 .get(&index)
409 .and_then(|entries| entries.iter().find(|e| e.change.path == path))
410 .map(|e| e.diff.clone())
411 .unwrap_or_default()
412 }
413
414 fn working_status(&self, amend: bool) -> WorkingStatus {
415 let mut status = WorkingStatus::default();
416 for entry in self.working.borrow().iter() {
417 if entry.staged {
418 status.staged.push(entry.change.clone());
419 } else {
420 status.unstaged.push(entry.change.clone());
421 }
422 }
423 if amend {
426 let removed = self.amend_removed.borrow();
427 for fe in self.head_files() {
428 if removed.contains(&fe.change.path) {
429 status.unstaged.push(fe.change.clone());
430 } else {
431 status.staged.push(fe.change.clone());
432 }
433 }
434 }
435 status
436 }
437
438 fn working_diff(&self, path: &str, _staged: bool, amend: bool) -> Diff {
439 if let Some(diff) = self
442 .working
443 .borrow()
444 .iter()
445 .find(|e| e.change.path == path)
446 .map(|e| e.diff.clone())
447 {
448 return diff;
449 }
450 if amend
451 && let Some(diff) = self
452 .head_files()
453 .iter()
454 .find(|fe| fe.change.path == path)
455 .map(|fe| fe.diff.clone())
456 {
457 return diff;
458 }
459 Diff::default()
460 }
461
462 fn stage(&self, path: &str) -> Result<(), String> {
463 let mut found = false;
464 for entry in self.working.borrow_mut().iter_mut() {
465 if entry.change.path == path {
466 entry.staged = true;
467 found = true;
468 }
469 }
470 if !found {
472 self.amend_removed.borrow_mut().remove(path);
473 }
474 Ok(())
475 }
476
477 fn unstage(&self, path: &str, amend: bool) -> Result<(), String> {
478 let mut found = false;
479 for entry in self.working.borrow_mut().iter_mut() {
480 if entry.change.path == path {
481 entry.staged = false;
482 found = true;
483 }
484 }
485 if !found && amend {
487 self.amend_removed.borrow_mut().insert(path.to_string());
488 }
489 Ok(())
490 }
491
492 fn revert(&self, path: &str) -> Result<(), String> {
493 self.working.borrow_mut().retain(|e| {
497 !(e.change.path == path && !e.staged && e.change.status != ChangeStatus::Untracked)
498 });
499 Ok(())
500 }
501
502 fn delete_untracked(&self, path: &str) -> Result<(), String> {
503 self.working.borrow_mut().retain(|e| {
506 !(e.change.path == path && !e.staged && e.change.status == ChangeStatus::Untracked)
507 });
508 Ok(())
509 }
510
511 fn commit(&self, message: &str, amend: bool) -> Result<(), String> {
512 if message.trim().is_empty() {
513 return Err("Please enter a commit message.".into());
514 }
515 self.working.borrow_mut().retain(|e| !e.staged);
518 self.amend_removed.borrow_mut().clear();
519 *self.last_commit.borrow_mut() = Some((message.to_string(), amend));
520 Ok(())
521 }
522
523 fn head_message(&self) -> Option<String> {
524 self.commits.first().map(|c| c.message.clone())
525 }
526
527 fn signature(&self) -> Option<(String, String)> {
528 self.signature.clone()
529 }
530}
531
532#[allow(clippy::too_many_arguments)]
534pub fn commit(
535 id: &str,
536 summary: &str,
537 author: &str,
538 email: &str,
539 time_seconds: i64,
540 time_offset_minutes: i32,
541 parents: &[&str],
542 refs: &[(&str, RefKind)],
543) -> CommitInfo {
544 CommitInfo {
545 id: id.to_string(),
546 short_id: id.chars().take(8).collect(),
547 summary: summary.to_string(),
548 message: format!("{summary}\n"),
549 author_name: author.to_string(),
550 author_email: email.to_string(),
551 committer_name: author.to_string(),
552 committer_email: email.to_string(),
553 time_seconds,
554 time_offset_minutes,
555 parents: parents.iter().map(|p| p.to_string()).collect(),
556 refs: refs
557 .iter()
558 .map(|(name, kind)| RefLabel {
559 name: name.to_string(),
560 kind: *kind,
561 })
562 .collect(),
563 }
564}
565
566fn file_entry(
567 path: &str,
568 old_path: Option<&str>,
569 status: ChangeStatus,
570 diff_lines: &[(DiffLineKind, &str)],
571) -> FileEntry {
572 FileEntry {
573 change: FileChange {
574 path: path.to_string(),
575 old_path: old_path.map(str::to_string),
576 status,
577 },
578 diff: diff(diff_lines),
579 }
580}
581
582fn diff(lines: &[(DiffLineKind, &str)]) -> Diff {
584 Diff {
585 lines: lines
586 .iter()
587 .map(|(kind, text)| DiffLine::new(*kind, text.to_string()))
588 .collect(),
589 }
590}