fluidattacks-core-git 0.68.0

Fluid Attacks Core Git
Documentation
use std::path::Path;

use crate::cli;
use crate::errors::ResetError;

/// Runs `git reset --hard` and removes the symlinks. `repo_path` must be the
/// root of the working tree: git refuses any other path.
pub async fn reset_repo(repo_path: &Path) -> Result<(), ResetError> {
    cli::reset_hard(repo_path).await?;
    let root = repo_path.to_path_buf();
    tokio::task::spawn_blocking(move || super::fs::remove_symlinks(&root))
        .await
        .unwrap_or_else(|error| std::panic::resume_unwind(error.into_panic()))?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::reset_repo;
    use crate::errors::ResetError;
    use std::path::Path;

    fn git(repo: &Path, args: &[&str]) {
        let status = std::process::Command::new("git")
            .arg("-C")
            .arg(repo)
            .args(args)
            .status()
            .unwrap();
        assert!(status.success());
    }

    #[tokio::test]
    async fn only_the_root_of_the_repository_gets_reset() {
        let dir = tempfile::tempdir().unwrap();
        std::env::set_var("GIT_CONFIG_GLOBAL", dir.path().join("gitconfig"));

        let repo = dir.path().join("repo");
        std::fs::create_dir_all(repo.join("sub/deep")).unwrap();
        git(&repo, &["init", "-q"]);
        std::fs::write(repo.join("f.txt"), b"committed").unwrap();
        git(&repo, &["add", "-A"]);
        git(
            &repo,
            &[
                "-c",
                "user.email=dev@fluidattacks.com",
                "-c",
                "user.name=dev",
                "commit",
                "-q",
                "-m",
                "files",
            ],
        );
        std::fs::write(repo.join("f.txt"), b"dirty").unwrap();

        for not_the_root in [repo.join("sub/deep"), repo.join(".git")] {
            let outcome = reset_repo(&not_the_root).await;
            assert!(matches!(outcome, Err(ResetError::Git(_))), "{outcome:?}");
        }
        assert_eq!(
            std::fs::read_to_string(repo.join("f.txt")).unwrap(),
            "dirty"
        );

        reset_repo(&repo).await.unwrap();
        assert_eq!(
            std::fs::read_to_string(repo.join("f.txt")).unwrap(),
            "committed"
        );
    }

    #[tokio::test]
    async fn the_reset_runs_no_fsmonitor_of_the_repository() {
        let dir = tempfile::tempdir().unwrap();
        let marker = dir.path().join("pwned");
        let payload = dir.path().join("payload.sh");
        std::fs::write(&payload, format!("#!/bin/sh\ntouch {}\n", marker.display())).unwrap();
        std::fs::set_permissions(
            &payload,
            std::os::unix::fs::PermissionsExt::from_mode(0o755),
        )
        .unwrap();

        let repo = dir.path().join("repo");
        std::fs::create_dir_all(&repo).unwrap();
        git(&repo, &["init", "-q"]);
        std::fs::write(repo.join("f.txt"), b"committed").unwrap();
        git(&repo, &["add", "-A"]);
        git(
            &repo,
            &[
                "-c",
                "user.email=dev@fluidattacks.com",
                "-c",
                "user.name=dev",
                "commit",
                "-q",
                "-m",
                "files",
            ],
        );
        git(
            &repo,
            &["config", "core.fsmonitor", &payload.to_string_lossy()],
        );

        reset_repo(&repo).await.unwrap();
        assert!(!marker.exists());
    }

    #[tokio::test]
    #[cfg(unix)]
    async fn a_symlink_that_cannot_be_removed_fails_the_reset() {
        use std::os::unix::fs::PermissionsExt;

        let dir = tempfile::tempdir().unwrap();
        let repo = dir.path().join("repo");
        let sealed = repo.join("sealed");
        std::fs::create_dir_all(&sealed).unwrap();
        std::os::unix::fs::symlink("target.txt", sealed.join("link.lnk")).unwrap();
        git(&repo, &["init", "-q"]);
        git(&repo, &["add", "-A"]);
        git(
            &repo,
            &[
                "-c",
                "user.email=dev@fluidattacks.com",
                "-c",
                "user.name=dev",
                "commit",
                "-q",
                "-m",
                "files",
            ],
        );
        std::fs::set_permissions(&sealed, std::fs::Permissions::from_mode(0o500)).unwrap();

        // A privileged user, as the ci runner is, writes into the directory
        // regardless of its mode, and then there is no failure to report.
        if std::fs::write(sealed.join("probe"), b"").is_ok() {
            std::fs::set_permissions(&sealed, std::fs::Permissions::from_mode(0o755)).unwrap();
            return;
        }

        let result = reset_repo(&repo).await;

        std::fs::set_permissions(&sealed, std::fs::Permissions::from_mode(0o755)).unwrap();
        assert!(matches!(result, Err(ResetError::Symlinks(_))), "{result:?}");
        assert!(sealed.join("link.lnk").is_symlink());
    }
}