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)
}
}
|