From 231787d1a508c807aadc5473a4babe75cd1b92db Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Sun, 12 Jan 2025 22:45:10 -0800 Subject: [PATCH] wip: resolve SHA1 without rev-parse --- Cargo.lock | 1 + Cargo.toml | 1 + src/utils.rs | 133 +++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5475938..e8d0a86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1468,6 +1468,7 @@ dependencies = [ "hex_color", "home", "rand", + "regex", "serde", "serde_yaml", "shell-words", diff --git a/Cargo.toml b/Cargo.toml index 4a5e59f..c6e2a3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,4 @@ url = "2.5.4" xdg = "2.5.2" home = "0.5.11" rand = "0.8.5" +regex = "1.7" diff --git a/src/utils.rs b/src/utils.rs index 74705fb..78850e3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,14 +1,15 @@ use crate::config::{Config, ConfigItem, DEFAULT_CONFIG_SHELL}; use crate::constants::{REPO_NAME, SCHEME_EXTENSION}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context, Error, Result}; use home::home_dir; use rand::Rng; use std::fs::{self, File}; -use std::io::Write; +use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::str; use tinted_builder::SchemeSystem; +use regex::bytes::Regex; /// Ensures that a directory exists, creating it if it does not. pub fn ensure_directory_exists>(dir_path: P) -> Result<()> { @@ -136,7 +137,6 @@ pub fn git_update(repo_path: &Path, repo_url: &str, revision: Option<&str>) -> R })?; Command::new("git") .args(vec!["remote", "rm", &tmp_remote_name]) - .current_dir(repo_path) .stdout(Stdio::null()) .status() .with_context(|| { @@ -155,6 +155,132 @@ fn random_remote_name() -> String { format!("tinty-remote-{}", random_number) } +// Resolvees the SHA1 of revision at remote_name. +// revision can be a tag, a branch, or a commit SHA1. +fn git_resolve_revision(repo_path: &Path, remote_name: &str, revision: &str) -> Result { + + // 1.) Check if its a tag. + let expected_tag_ref = format!("refs/tags/{}", revision); + let mut command = safe_command( + format!( + "git ls-remote --quiet --tags \"{}\" \"{}\"", + remote_name, expected_tag_ref + ), + repo_path, + )?; + let mut child = command + .stderr(Stdio::null()) + .stdout(Stdio::piped()) + .spawn() + .with_context(|| format!("Failed to list remote tags from {}", remote_name))?; + let stdout = child.stdout.take().expect("failed to capture stdout"); + let reader = BufReader::new(stdout); + + for line in reader.lines() { + match line { + Ok(line) => { + let parts: Vec<&str> = line.split("\t").collect(); + if parts.len() != 2 { + return Err(anyhow!( + "malformed ls-remote result. Expected tab-delimited tuple, found {} parts", + parts.len() + )); + } + // To hedge against non-exact matches, we'll compare the ref field with + // what we'd expect an exact match would look i.e. refs/tags/ + if parts[1] == expected_tag_ref { + // Found the tag. Return the SHA1 + return Ok(parts[0].to_string()); + } + } + Err(e) => return Err(anyhow!("failed to capture lines: {}", e)), + } + } + + child.wait()?; + + // 2.) Check if its a branch + let expected_branch_ref = format!("refs/heads/{}", revision); + let mut command = safe_command( + format!( + "git ls-remote --quiet --branches \"{}\" \"{}\"", + remote_name, expected_tag_ref + ), + repo_path, + )?; + let mut child = command + .stdout(Stdio::piped()) + .spawn() + .with_context(|| format!("Failed to list branches tags from {}", remote_name))?; + let stdout = child.stdout.take().expect("failed to capture stdout"); + let reader = BufReader::new(stdout); + + for line in reader.lines() { + match line { + Ok(line) => { + let parts: Vec<&str> = line.split("\t").collect(); + if parts.len() != 2 { + return Err(anyhow!( + "malformed ls-remote result. Expected tab-delimited tuple, found {} parts", + parts.len() + )); + } + // To hedge against non-exact matches, we'll compare the ref field with + // what we'd expect an exact match would look i.e. refs/heads/ + if parts[1] == expected_branch_ref { + // Found the tag. Return the SHA1 + return Ok(parts[0].to_string()); + } + } + Err(e) => return Err(anyhow!("failed to capture lines: {}", e)), + } + } + + child.wait()?; + + // We are here because revision isn't a tag or a branch. + // First, we'll check if revision itself *could* be a SHA1. + // If it doesn't look like one, we'll return early. + let pattern = r"^[0-9a-f]{1,40}$"; + let re = Regex::new(pattern).expect("Invalid regex"); + if !re.is_match(revision.as_bytes()) { + return Err(anyhow!("cannot resolve {} into a Git SHA1", revision)); + } + + // 3.) Check if any branch in remote contains the SHA1: + // It seems that the only way to do this is to list the branches that contain the SHA1 + // and check if it belongs in the remote. + let remote_branch_prefix = format!("remotes/{}", remote_name); + let mut command = safe_command(format!( "git branch -a --contains \"{}\"", revision), repo_path)?; + let mut child = command + .stdout(Stdio::piped()) + .spawn() + .with_context(|| format!("Failed to find branches containing commit {} from {}", revision, remote_name))?; + let stdout = child.stdout.take().expect("failed to capture stdout"); + let reader = BufReader::new(stdout); + + for line in reader.lines() { + match line { + Ok(line) => { + if line.starts_with(&remote_branch_prefix) { + // Found a branch1 + return Ok(revision.to_string()); + } + } + Err(e) => return Err(anyhow!("failed to capture lines: {}", e)), + } + } + + return Err(anyhow!("cannot find revision {} in remote {}", revision, remote_name)); +} + +fn safe_command(command: String, cwd: &Path) -> Result { + let command_vec = shell_words::split(&command).map_err(anyhow::Error::new)?; + let mut command = Command::new(&command_vec[0]); + command.args(&command_vec[1..]).current_dir(cwd); + Ok(command) +} + fn git_to_revision(repo_path: &Path, remote_name: &str, revision: &str) -> Result<()> { let command = format!("git fetch --quiet \"{}\" \"{}\"", remote_name, revision); let command_vec = shell_words::split(&command).map_err(anyhow::Error::new)?; @@ -180,6 +306,7 @@ fn git_to_revision(repo_path: &Path, remote_name: &str, revision: &str) -> Resul let parse_out = Command::new(&command_vec[0]) .args(&command_vec[1..]) .current_dir(repo_path) + .stderr(Stdio::null()) .output() .with_context(|| { format!(