aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src/vcs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-core/src/vcs')
-rw-r--r--crates/mozart-core/src/vcs/downloader/git.rs271
-rw-r--r--crates/mozart-core/src/vcs/downloader/hg.rs84
-rw-r--r--crates/mozart-core/src/vcs/downloader/mod.rs56
-rw-r--r--crates/mozart-core/src/vcs/downloader/svn.rs84
4 files changed, 0 insertions, 495 deletions
diff --git a/crates/mozart-core/src/vcs/downloader/git.rs b/crates/mozart-core/src/vcs/downloader/git.rs
deleted file mode 100644
index eb7a649..0000000
--- a/crates/mozart-core/src/vcs/downloader/git.rs
+++ /dev/null
@@ -1,271 +0,0 @@
-use super::super::process::ProcessExecutor;
-use super::super::util::git::GitUtil;
-use super::VcsDownloader;
-use anyhow::Result;
-use regex::Regex;
-use std::path::Path;
-use std::sync::LazyLock;
-
-/// Match `<hex> HEAD` lines in `git show-ref --head -d` output.
-static HEAD_REF_RE: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(r"(?im)^([a-f0-9]+) HEAD$").unwrap());
-
-/// 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 get_local_changes(&self, target: &Path) -> Result<Option<String>> {
- if !target.join(".git").exists() {
- return Ok(None);
- }
- let process = ProcessExecutor::new();
- let output = process.execute(
- &["git", "status", "--porcelain", "--untracked-files=no"],
- Some(target),
- )?;
- let trimmed = output.stdout.trim();
- if trimmed.is_empty() {
- Ok(None)
- } else {
- Ok(Some(trimmed.to_string()))
- }
- }
-
- fn vcs_reference(&self, target: &Path) -> Result<Option<String>> {
- if !target.join(".git").exists() {
- return Ok(None);
- }
- let process = ProcessExecutor::new();
- let output = process.execute(&["git", "rev-parse", "HEAD"], Some(target))?;
- if output.status != 0 {
- return Ok(None);
- }
- let trimmed = output.stdout.trim();
- if trimmed.is_empty() {
- Ok(None)
- } else {
- Ok(Some(trimmed.to_string()))
- }
- }
-
- fn unpushed_changes(&self, target: &Path) -> Result<Option<String>> {
- if !target.join(".git").exists() {
- return Ok(None);
- }
- let process = ProcessExecutor::new();
-
- let mut refs = match collect_show_ref(&process, target)? {
- Some(r) => r,
- None => return Ok(None),
- };
-
- let head_ref = match HEAD_REF_RE
- .captures(&refs)
- .and_then(|c| c.get(1))
- .map(|m| m.as_str().to_string())
- {
- Some(h) => h,
- None => return Ok(None),
- };
-
- let candidate_branches = collect_local_branches(&refs, &head_ref);
- if candidate_branches.is_empty() {
- // not on a branch (detached / tag) — skip
- return Ok(None);
- }
-
- let mut branch = candidate_branches[0].clone();
- let mut unpushed_changes: Option<String> = None;
- let mut branch_not_found_error = false;
-
- for i in 0..=1 {
- let mut remote_branches: Vec<String> = Vec::new();
-
- for candidate in &candidate_branches {
- let matches = collect_remote_branches(&refs, candidate);
- if !matches.is_empty() {
- branch = candidate.clone();
- remote_branches = matches;
- break;
- }
- }
-
- if remote_branches.is_empty() {
- unpushed_changes = Some(format!(
- "Branch {branch} could not be found on any remote and appears to be unpushed"
- ));
- branch_not_found_error = true;
- } else {
- if branch_not_found_error {
- unpushed_changes = None;
- }
- for remote_branch in &remote_branches {
- let range = format!("{remote_branch}...{branch}");
- let output = process.execute_checked(
- &["git", "diff", "--name-status", &range, "--"],
- Some(target),
- )?;
- let trimmed = output.stdout.trim().to_string();
- match unpushed_changes {
- None => unpushed_changes = Some(trimmed),
- Some(ref existing) if trimmed.len() < existing.len() => {
- unpushed_changes = Some(trimmed);
- }
- _ => {}
- }
- }
- }
-
- if unpushed_changes.as_deref().is_some_and(|s| !s.is_empty()) && i == 0 {
- let _ = process.execute(&["git", "fetch", "--all"], Some(target))?;
- refs = match collect_show_ref(&process, target)? {
- Some(r) => r,
- None => return Ok(unpushed_changes),
- };
- }
-
- if unpushed_changes.as_deref().is_none_or(str::is_empty) {
- break;
- }
- }
-
- Ok(unpushed_changes.filter(|s| !s.is_empty()))
- }
-
- 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)
- }
-
- fn is_change_report(&self) -> bool {
- true
- }
-
- fn is_vcs_capable_downloader(&self) -> bool {
- true
- }
-
- fn is_dvcs_downloader(&self) -> bool {
- true
- }
-}
-
-fn collect_show_ref(process: &ProcessExecutor, target: &Path) -> Result<Option<String>> {
- let output = process.execute(&["git", "show-ref", "--head", "-d"], Some(target))?;
- if output.status != 0 {
- anyhow::bail!(
- "Failed to execute git show-ref --head -d\n\n{}",
- output.stderr.trim()
- );
- }
- Ok(Some(output.stdout.trim().to_string()))
-}
-
-fn collect_local_branches(refs: &str, head_ref: &str) -> Vec<String> {
- let pattern = format!(r"(?im)^{} refs/heads/(.+)$", regex::escape(head_ref));
- let re = match Regex::new(&pattern) {
- Ok(r) => r,
- Err(_) => return Vec::new(),
- };
- re.captures_iter(refs)
- .filter_map(|c| c.get(1).map(|m| m.as_str().to_string()))
- .collect()
-}
-
-fn collect_remote_branches(refs: &str, candidate: &str) -> Vec<String> {
- let pattern = format!(
- r"(?im)^[a-f0-9]+ refs/remotes/((?:[^/]+)/{})$",
- regex::escape(candidate)
- );
- let re = match Regex::new(&pattern) {
- Ok(r) => r,
- Err(_) => return Vec::new(),
- };
- re.captures_iter(refs)
- .filter_map(|c| c.get(1).map(|m| m.as_str().to_string()))
- .collect()
-}
diff --git a/crates/mozart-core/src/vcs/downloader/hg.rs b/crates/mozart-core/src/vcs/downloader/hg.rs
deleted file mode 100644
index 33650f8..0000000
--- a/crates/mozart-core/src/vcs/downloader/hg.rs
+++ /dev/null
@@ -1,84 +0,0 @@
-use super::super::util::hg::HgUtil;
-use super::VcsDownloader;
-use anyhow::Result;
-use std::path::Path;
-
-/// Mercurial downloader using clone/pull/update.
-pub struct HgDownloader {
- hg_util: HgUtil,
-}
-
-impl HgDownloader {
- pub fn new(hg_util: HgUtil) -> Self {
- Self { hg_util }
- }
-}
-
-impl VcsDownloader for HgDownloader {
- fn download(&self, _url: &str, _reference: &str, _target: &Path) -> Result<()> {
- Ok(())
- }
-
- fn install(&self, url: &str, reference: &str, target: &Path) -> Result<()> {
- let target_str = target.to_string_lossy().to_string();
- self.hg_util
- .execute(&["clone", "--", url, &target_str], None)?;
- self.hg_util
- .execute(&["update", "-r", reference], Some(target))?;
- Ok(())
- }
-
- fn update(&self, url: &str, _old_ref: &str, new_ref: &str, target: &Path) -> Result<()> {
- self.hg_util.execute(&["pull", url], Some(target))?;
- self.hg_util
- .execute(&["update", "-r", new_ref], Some(target))?;
- Ok(())
- }
-
- fn remove(&self, target: &Path) -> Result<()> {
- if target.exists() {
- std::fs::remove_dir_all(target)?;
- }
- Ok(())
- }
-
- fn get_local_changes(&self, target: &Path) -> Result<Option<String>> {
- if !target.join(".hg").is_dir() {
- return Ok(None);
- }
- let output = self.hg_util.execute(&["st"], Some(target))?;
- let trimmed = output.stdout.trim();
- if trimmed.is_empty() {
- Ok(None)
- } else {
- Ok(Some(trimmed.to_string()))
- }
- }
-
- fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result<String> {
- let range = format!("{from}:{to}");
- let output = self.hg_util.execute(
- &[
- "log",
- "-r",
- &range,
- "--template",
- "{rev}:{node|short} {desc|firstline}\\n",
- ],
- Some(target),
- )?;
- Ok(output.stdout)
- }
-
- fn is_change_report(&self) -> bool {
- true
- }
-
- fn is_vcs_capable_downloader(&self) -> bool {
- true
- }
-
- fn is_dvcs_downloader(&self) -> bool {
- false
- }
-}
diff --git a/crates/mozart-core/src/vcs/downloader/mod.rs b/crates/mozart-core/src/vcs/downloader/mod.rs
deleted file mode 100644
index 352f330..0000000
--- a/crates/mozart-core/src/vcs/downloader/mod.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-pub mod git;
-pub mod hg;
-pub mod svn;
-
-use std::path::Path;
-
-use anyhow::Result;
-
-/// The VCS downloader interface.
-///
-/// Corresponds to Composer's `VcsDownloader` hierarchy.
-pub trait VcsDownloader {
- /// Prepare for installation (e.g., sync mirror cache).
- fn download(&self, url: &str, reference: &str, target: &Path) -> Result<()>;
-
- /// Install (clone/checkout) the source to the target directory.
- fn install(&self, url: &str, reference: &str, target: &Path) -> Result<()>;
-
- /// Update the source at target to a new reference.
- fn update(&self, url: &str, old_ref: &str, new_ref: &str, target: &Path) -> Result<()>;
-
- /// Remove the source from the target directory.
- fn remove(&self, target: &Path) -> Result<()>;
-
- /// Detect local changes in the working copy.
- /// Returns `None` if clean, `Some(diff)` if modified.
- /// Mirrors `Composer\Downloader\ChangeReportInterface::getLocalChanges`.
- fn get_local_changes(&self, target: &Path) -> Result<Option<String>>;
-
- /// Detect commits present locally but not on the tracking remote.
- /// Returns `None` if there are no unpushed commits or the concept does
- /// not apply (only `GitDownloader` implements this in Composer's
- /// `DvcsDownloaderInterface`).
- fn unpushed_changes(&self, _target: &Path) -> Result<Option<String>> {
- Ok(None)
- }
-
- /// Resolve the working copy's current VCS reference (e.g. commit hash).
- /// Returns `None` if no reference can be determined. Mirrors
- /// `Composer\Downloader\VcsCapableDownloaderInterface::getVcsReference`.
- fn vcs_reference(&self, _target: &Path) -> Result<Option<String>> {
- Ok(None)
- }
-
- /// Get commit log between two references.
- fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result<String>;
-
- /// instanceof ChangeReportInterface
- fn is_change_report(&self) -> bool;
-
- /// instanceof VcsCapableDownloaderInterface
- fn is_vcs_capable_downloader(&self) -> bool;
-
- /// instanceof DvcsDownloaderInterface
- fn is_dvcs_downloader(&self) -> bool;
-}
diff --git a/crates/mozart-core/src/vcs/downloader/svn.rs b/crates/mozart-core/src/vcs/downloader/svn.rs
deleted file mode 100644
index ea885ed..0000000
--- a/crates/mozart-core/src/vcs/downloader/svn.rs
+++ /dev/null
@@ -1,84 +0,0 @@
-use super::super::util::svn::SvnUtil;
-use super::VcsDownloader;
-use anyhow::Result;
-use regex::Regex;
-use std::path::Path;
-use std::sync::LazyLock;
-
-/// Match any non-`X` status line (mirror of Composer's
-/// `{^ *[^X ] +}m`). Ignores externals (`X` prefix).
-static SVN_STATUS_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?m)^ *[^X ] +").unwrap());
-
-/// SVN downloader using checkout/switch.
-pub struct SvnDownloader {
- svn_util: SvnUtil,
-}
-
-impl SvnDownloader {
- pub fn new(svn_util: SvnUtil) -> Self {
- Self { svn_util }
- }
-}
-
-impl VcsDownloader for SvnDownloader {
- fn download(&self, _url: &str, _reference: &str, _target: &Path) -> Result<()> {
- // SVN doesn't need a pre-download step
- Ok(())
- }
-
- fn install(&self, url: &str, reference: &str, target: &Path) -> Result<()> {
- let target_str = target.to_string_lossy().to_string();
- let svn_url = format!("{url}@{reference}");
- self.svn_util
- .execute(&["checkout", &svn_url, &target_str], None)?;
- Ok(())
- }
-
- fn update(&self, url: &str, _old_ref: &str, new_ref: &str, target: &Path) -> Result<()> {
- let svn_url = format!("{url}@{new_ref}");
- self.svn_util
- .execute(&["switch", "--ignore-ancestry", &svn_url], Some(target))?;
- Ok(())
- }
-
- fn remove(&self, target: &Path) -> Result<()> {
- if target.exists() {
- std::fs::remove_dir_all(target)?;
- }
- Ok(())
- }
-
- fn get_local_changes(&self, target: &Path) -> Result<Option<String>> {
- if !target.join(".svn").is_dir() {
- return Ok(None);
- }
- let output = self
- .svn_util
- .execute(&["status", "--ignore-externals"], Some(target))?;
- if SVN_STATUS_RE.is_match(&output.stdout) {
- Ok(Some(output.stdout))
- } else {
- Ok(None)
- }
- }
-
- fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result<String> {
- let range = format!("{from}:{to}");
- let output = self
- .svn_util
- .execute(&["log", "-r", &range], Some(target))?;
- Ok(output.stdout)
- }
-
- fn is_change_report(&self) -> bool {
- true
- }
-
- fn is_vcs_capable_downloader(&self) -> bool {
- true
- }
-
- fn is_dvcs_downloader(&self) -> bool {
- false
- }
-}