Skip to main content

fluidattacks_core_git/
ops.rs

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