aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-vcs/src/downloader/git.rs
blob: 3bdb9cad5045991bfa90ee53c213f6fabfc8b262 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
use std::path::Path;

use anyhow::Result;

use crate::process::ProcessExecutor;
use crate::util::git::GitUtil;

use super::VcsDownloader;

/// Git downloader using clone/checkout with optional mirror cache.
///
/// Corresponds to Composer's `Downloader\GitDownloader`.
pub struct GitDownloader {
    git_util: GitUtil,
}

impl GitDownloader {
    pub fn new(git_util: GitUtil) -> Self {
        Self { git_util }
    }
}

impl VcsDownloader for GitDownloader {
    fn download(&self, url: &str, _reference: &str, _target: &Path) -> Result<()> {
        // Pre-sync the mirror so install can use --reference
        self.git_util.sync_mirror(url)?;
        Ok(())
    }

    fn install(&self, url: &str, reference: &str, target: &Path) -> Result<()> {
        let target_str = target.to_string_lossy();
        let mirror_path = self.git_util.mirror_path(url);

        if mirror_path.join("HEAD").exists() {
            // Clone with mirror reference for efficiency
            let mirror_str = mirror_path.to_string_lossy().to_string();
            self.git_util.run_command(
                &[
                    "git",
                    "clone",
                    "--no-checkout",
                    "--dissociate",
                    "--reference",
                    &mirror_str,
                    "--",
                    url,
                    &target_str,
                ],
                url,
                None,
            )?;
        } else {
            self.git_util.run_command(
                &["git", "clone", "--no-checkout", "--", url, &target_str],
                url,
                None,
            )?;
        }

        // Checkout the specific reference
        let process = ProcessExecutor::new();
        process.execute_checked(&["git", "checkout", reference, "--force"], Some(target))?;

        Ok(())
    }

    fn update(&self, url: &str, _old_ref: &str, new_ref: &str, target: &Path) -> Result<()> {
        let process = ProcessExecutor::new();

        // Update remote URL
        process.execute_checked(
            &["git", "remote", "set-url", "origin", "--", url],
            Some(target),
        )?;

        // Fetch latest
        self.git_util
            .run_command(&["git", "fetch", "origin"], url, Some(target))?;

        // Checkout new reference
        process.execute_checked(&["git", "checkout", new_ref, "--force"], Some(target))?;

        Ok(())
    }

    fn remove(&self, target: &Path) -> Result<()> {
        if target.exists() {
            std::fs::remove_dir_all(target)?;
        }
        Ok(())
    }

    fn local_changes(&self, target: &Path) -> Result<Option<String>> {
        let process = ProcessExecutor::new();
        let output = process.execute(&["git", "status", "--porcelain"], Some(target))?;
        if output.stdout.trim().is_empty() {
            Ok(None)
        } else {
            Ok(Some(output.stdout))
        }
    }

    fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result<String> {
        let process = ProcessExecutor::new();
        let range = format!("{from}..{to}");
        let output = process.execute(
            &["git", "log", &range, "--oneline", "--no-decorate"],
            Some(target),
        )?;
        Ok(output.stdout)
    }
}