forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
4.4 KiB
1//! The fsck run: `git fsck` on every repo the database lists, then `PRAGMA integrity_check` on the
2//! live database. Used by the scheduler, the admin page, and the CLI and internal route.
3
4use forge_core::{Result, Store};
5use forge_git::GitEnv;
6use serde::{Deserialize, Serialize};
7
8/// Longest git output kept per repo in a problem line.
9const MAX_OUTPUT_CHARS: usize = 2000;
10
11/// Outcome of [`run_fsck`]. `problems` lists one line per failed repo, plus one for the database
12/// when its check fails. The run is clean when the list is empty.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct FsckSummary {
15 pub full: bool,
16 pub repos: usize,
17 pub problems: Vec<String>,
18}
19
20impl FsckSummary {
21 pub fn is_ok(&self) -> bool {
22 self.problems.is_empty()
23 }
24}
25
26/// Checks every repo and the database. A problem with one repo does not stop the others. Only a
27/// failure to query the database or to start git is an error.
28pub async fn run_fsck(store: &Store, git: &GitEnv, full: bool) -> Result<FsckSummary> {
29 let ids = store.repo_ids().await?;
30 let mut problems = Vec::new();
31 for id in &ids {
32 let outcome = match forge_git::repo::repo_path(&git.data_dir, id) {
33 Ok(path) => forge_git::fsck::fsck(git, &path, full).await,
34 Err(e) => Err(e),
35 };
36 match outcome {
37 Ok(report) if report.ok => {}
38 Ok(report) => problems.push(format!(
39 "repo {id}: {}",
40 truncate(&report.output, MAX_OUTPUT_CHARS)
41 )),
42 Err(e) => problems.push(format!("repo {id}: {e}")),
43 }
44 // A wiki that exists is checked the same way, as its own repo.
45 if let Ok(wiki) = forge_git::repo::wiki_path(&git.data_dir, id)
46 && wiki.join("HEAD").is_file()
47 {
48 match forge_git::fsck::fsck(git, &wiki, full).await {
49 Ok(report) if report.ok => {}
50 Ok(report) => problems.push(format!(
51 "repo {id} wiki: {}",
52 truncate(&report.output, MAX_OUTPUT_CHARS)
53 )),
54 Err(e) => problems.push(format!("repo {id} wiki: {e}")),
55 }
56 }
57 }
58 let db = store.integrity_check().await?;
59 if db.as_slice() != ["ok"] {
60 problems.push(format!(
61 "database: {}",
62 truncate(&db.join("; "), MAX_OUTPUT_CHARS)
63 ));
64 }
65 Ok(FsckSummary {
66 full,
67 repos: ids.len(),
68 problems,
69 })
70}
71
72/// The first `max` characters of `text`, with an ellipsis when cut.
73fn truncate(text: &str, max: usize) -> String {
74 if text.chars().count() <= max {
75 return text.to_owned();
76 }
77 let mut cut: String = text.chars().take(max).collect();
78 cut.push('…');
79 cut
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use forge_core::model::Visibility;
86 use forge_git::repo::{init_bare, repo_path};
87
88 #[tokio::test]
89 async fn clean_instance_passes_and_a_damaged_repo_is_reported() {
90 let dir = tempfile::tempdir().expect("tempdir");
91 let data_dir = dir.path().join("data");
92 std::fs::create_dir_all(&data_dir).expect("data dir");
93 let store = Store::open(&data_dir.join("forge.db")).await.expect("open");
94 let alice = store
95 .create_user("alice", None, "hash", false)
96 .await
97 .expect("user");
98 let git = GitEnv {
99 git_bin: "git".into(),
100 data_dir: data_dir.clone(),
101 };
102 let repo = store
103 .create_repo(&alice.id, "demo", "", Visibility::Private, "main")
104 .await
105 .expect("repo");
106 let path = repo_path(&data_dir, &repo.id).expect("path");
107 init_bare(&git, &path, "main").await.expect("init");
108
109 let clean = run_fsck(&store, &git, false).await.expect("fsck");
110 assert_eq!(clean.repos, 1);
111 assert!(clean.is_ok(), "{:?}", clean.problems);
112
113 // Remove the repo from disk: the run reports it and keeps going.
114 std::fs::remove_dir_all(&path).expect("remove");
115 let broken = run_fsck(&store, &git, true).await.expect("fsck");
116 assert!(broken.full);
117 assert_eq!(broken.problems.len(), 1);
118 assert!(broken.problems[0].starts_with(&format!("repo {}:", repo.id)));
119 }
120
121 #[test]
122 fn truncate_keeps_short_text_and_cuts_long_text_on_char_boundaries() {
123 assert_eq!(truncate("abc", 3), "abc");
124 assert_eq!(truncate("héllo", 2), "hé…");
125 }
126}