Skip to main content

fluidattacks_core_git/
ops.rs

1use std::path::Path;
2
3use crate::cli;
4use crate::errors::ResetError;
5
6/// Runs `git reset --hard` and removes the symlinks. `repo_path` must be the
7/// root of the working tree: git refuses any other path.
8pub async fn reset_repo(repo_path: &Path) -> Result<(), ResetError> {
9    cli::reset_hard(repo_path).await?;
10    let root = repo_path.to_path_buf();
11    tokio::task::spawn_blocking(move || super::fs::remove_symlinks(&root))
12        .await
13        .unwrap_or_else(|error| std::panic::resume_unwind(error.into_panic()))?;
14    Ok(())
15}
16
17#[cfg(test)]
18mod tests {
19    use super::reset_repo;
20    use crate::errors::ResetError;
21    use std::path::Path;
22
23    fn git(repo: &Path, args: &[&str]) {
24        let status = std::process::Command::new("git")
25            .arg("-C")
26            .arg(repo)
27            .args(args)
28            .status()
29            .unwrap();
30        assert!(status.success());
31    }
32
33    #[tokio::test]
34    async fn only_the_root_of_the_repository_gets_reset() {
35        let dir = tempfile::tempdir().unwrap();
36        std::env::set_var("GIT_CONFIG_GLOBAL", dir.path().join("gitconfig"));
37
38        let repo = dir.path().join("repo");
39        std::fs::create_dir_all(repo.join("sub/deep")).unwrap();
40        git(&repo, &["init", "-q"]);
41        std::fs::write(repo.join("f.txt"), b"committed").unwrap();
42        git(&repo, &["add", "-A"]);
43        git(
44            &repo,
45            &[
46                "-c",
47                "user.email=dev@fluidattacks.com",
48                "-c",
49                "user.name=dev",
50                "commit",
51                "-q",
52                "-m",
53                "files",
54            ],
55        );
56        std::fs::write(repo.join("f.txt"), b"dirty").unwrap();
57
58        for not_the_root in [repo.join("sub/deep"), repo.join(".git")] {
59            let outcome = reset_repo(&not_the_root).await;
60            assert!(matches!(outcome, Err(ResetError::Git(_))), "{outcome:?}");
61        }
62        assert_eq!(
63            std::fs::read_to_string(repo.join("f.txt")).unwrap(),
64            "dirty"
65        );
66
67        reset_repo(&repo).await.unwrap();
68        assert_eq!(
69            std::fs::read_to_string(repo.join("f.txt")).unwrap(),
70            "committed"
71        );
72    }
73
74    #[tokio::test]
75    async fn the_reset_runs_no_fsmonitor_of_the_repository() {
76        let dir = tempfile::tempdir().unwrap();
77        let marker = dir.path().join("pwned");
78        let payload = dir.path().join("payload.sh");
79        std::fs::write(&payload, format!("#!/bin/sh\ntouch {}\n", marker.display())).unwrap();
80        std::fs::set_permissions(
81            &payload,
82            std::os::unix::fs::PermissionsExt::from_mode(0o755),
83        )
84        .unwrap();
85
86        let repo = dir.path().join("repo");
87        std::fs::create_dir_all(&repo).unwrap();
88        git(&repo, &["init", "-q"]);
89        std::fs::write(repo.join("f.txt"), b"committed").unwrap();
90        git(&repo, &["add", "-A"]);
91        git(
92            &repo,
93            &[
94                "-c",
95                "user.email=dev@fluidattacks.com",
96                "-c",
97                "user.name=dev",
98                "commit",
99                "-q",
100                "-m",
101                "files",
102            ],
103        );
104        git(
105            &repo,
106            &["config", "core.fsmonitor", &payload.to_string_lossy()],
107        );
108
109        reset_repo(&repo).await.unwrap();
110        assert!(!marker.exists());
111    }
112
113    #[tokio::test]
114    #[cfg(unix)]
115    async fn a_symlink_that_cannot_be_removed_fails_the_reset() {
116        use std::os::unix::fs::PermissionsExt;
117
118        let dir = tempfile::tempdir().unwrap();
119        let repo = dir.path().join("repo");
120        let sealed = repo.join("sealed");
121        std::fs::create_dir_all(&sealed).unwrap();
122        std::os::unix::fs::symlink("target.txt", sealed.join("link.lnk")).unwrap();
123        git(&repo, &["init", "-q"]);
124        git(&repo, &["add", "-A"]);
125        git(
126            &repo,
127            &[
128                "-c",
129                "user.email=dev@fluidattacks.com",
130                "-c",
131                "user.name=dev",
132                "commit",
133                "-q",
134                "-m",
135                "files",
136            ],
137        );
138        std::fs::set_permissions(&sealed, std::fs::Permissions::from_mode(0o500)).unwrap();
139
140        // A privileged user, as the ci runner is, writes into the directory
141        // regardless of its mode, and then there is no failure to report.
142        if std::fs::write(sealed.join("probe"), b"").is_ok() {
143            std::fs::set_permissions(&sealed, std::fs::Permissions::from_mode(0o755)).unwrap();
144            return;
145        }
146
147        let result = reset_repo(&repo).await;
148
149        std::fs::set_permissions(&sealed, std::fs::Permissions::from_mode(0o755)).unwrap();
150        assert!(matches!(result, Err(ResetError::Symlinks(_))), "{result:?}");
151        assert!(sealed.join("link.lnk").is_symlink());
152    }
153}