use std::ffi::OsStr;
use std::process::Command;
use eyre::{OptionExt, Result, bail};
use gix::bstr::ByteSlice;
use gix::state::InProgress;
use crate::manage::State;
#[macro_export]
macro_rules! cmd {
($bin:literal $(, $($rest:tt)*)?) => {{
let bin_str: &str = $bin;
let parts: Vec<&str> = bin_str.split_whitespace().collect();
let (bin, pre_args) = match parts.as_slice() {
[bin, args @ ..] => (bin, args),
[] => panic!("Command cannot be empty"),
};
#[allow(unused_mut)]
let mut args: Vec<String> = pre_args.iter().map(|s| s.to_string()).collect();
cmd!(@inner args $(, $($rest)*)?);
log::debug!("exec: {} {}", bin, args.iter().map(|s| if s.contains(" ") {
format!("'{}'", s)
} else {
s.clone()
}).collect::<Vec<_>>().join(" "));
$crate::util::cmd(bin, &args)
}};
(@inner $vec:ident, $l:literal $(, $($rest:tt)*)?) => {
$vec.push(format!($l));
cmd!(@inner $vec $(, $($rest)*)?);
};
(@inner $vec:ident, $e:expr $(, $($rest:tt)*)?) => {
$vec.push($e.to_string());
cmd!(@inner $vec $(, $($rest)*)?);
};
(@inner $vec:ident $(,)?) => {};
}
#[macro_export]
macro_rules! re {
($name:ident, $re:literal) => {
fn $name() -> &'static regex::Regex {
$crate::re!(@inner $re)
}
};
($re:literal) => {
$crate::re!(@inner $re)
};
(@inner $re:literal) => {{
static RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| regex::Regex::new($re).unwrap());
&*RE
}};
}
pub fn cmd<I: AsRef<OsStr>>(name: &str, args: impl IntoIterator<Item = I>) -> Command {
let mut c = Command::new(name);
c.args(args);
c
}
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HeadState {
Attached(String),
Pending(String),
Detached,
}
impl HeadState {
pub fn name(&self) -> Option<&str> {
match self {
HeadState::Attached(name) | HeadState::Pending(name) => Some(name),
HeadState::Detached => None,
}
}
}
pub struct Repo {
inner: gix::Repository,
current_branch: HeadState,
}
impl Repo {
pub fn open(path: &str) -> Result<Self> {
let inner = gix::discover(path)?;
let current_branch = get_current_branch(&inner)?;
Ok(Self {
inner,
current_branch,
})
}
pub fn current_branch(&self) -> &HeadState {
&self.current_branch
}
pub fn config_string(&self, key: &str) -> Result<Option<String>> {
let Some(cow) = self.inner.config_snapshot().string(key) else {
return Ok(None);
};
let s = std::str::from_utf8(cow.as_ref())?;
Ok(Some(s.trim().to_string()))
}
pub fn config_path(&self, key: &str) -> Result<Option<PathBuf>> {
let snapshot = self.inner.config_snapshot();
let Some(path_val) = snapshot.path(key) else {
return Ok(None);
};
let bstr: &gix::bstr::BStr = path_val.as_ref();
let raw_path = bstr.to_path()?.to_path_buf();
if raw_path.is_absolute() {
Ok(Some(raw_path))
} else {
let root = self.workdir().unwrap_or(self.path()).canonicalize()?;
Ok(Some(root.join(raw_path)))
}
}
pub fn is_newly_created_branch(&self, branch_name: &str) -> Result<bool> {
let reference = match self.inner.find_reference(branch_name) {
Ok(r) => r,
Err(_) => return Ok(true),
};
let latest_log = reference
.log_iter()
.rev()? .ok_or_eyre("No reflog entries found")?
.next()
.transpose()?;
Ok(latest_log.is_some_and(|log| log.previous_oid.is_null()))
}
pub fn is_ancestor(&self, ancestor: gix::ObjectId, descendant: gix::ObjectId) -> Result<bool> {
match self.inner.merge_base(ancestor, descendant) {
Ok(merge_base) => Ok(merge_base.detach() == ancestor),
Err(_) => Ok(false),
}
}
pub fn default_remote_name(&self) -> String {
self.config_string("gherrit.remote")
.unwrap_or_default()
.unwrap_or_else(|| "origin".to_string())
}
fn find_default_branches(&self, remote_name: &str) -> Vec<String> {
let mut branches = Vec::new();
let remote_head_ref = format!("refs/remotes/{}/HEAD", remote_name);
if let Ok(head_ref) = self.inner.find_reference(&remote_head_ref) {
let target_name = head_ref
.target()
.try_name()
.map(|n| n.as_bstr().to_string());
if let Some(target) = target_name {
let prefix = format!("refs/remotes/{}/", remote_name);
if let Some(stripped) = target.strip_prefix(&prefix) {
branches.push(stripped.to_string());
}
}
}
if let Some(default_branch) = self.config_string("init.defaultBranch").ok().flatten() {
branches.push(default_branch);
}
let locals = ["main", "master", "trunk"]
.into_iter()
.filter(|b| self.find_reference(&format!("refs/heads/{b}")).is_ok())
.map(String::from);
branches.extend(locals);
branches.push("main".to_string());
branches
}
pub fn find_default_branch_on_default_remote(&self) -> String {
let branches = self.find_default_branches(&self.default_remote_name());
branches
.first()
.cloned()
.unwrap_or_else(|| "main".to_string())
}
pub fn is_a_default_branch_on_default_remote(&self, branch_name: &str) -> bool {
let branches = self.find_default_branches(&self.default_remote_name());
branches.iter().any(|b| b == branch_name)
}
pub fn is_managed(&self, branch_name: &str) -> Result<bool> {
match State::read_from(self, branch_name)? {
Some(State::Unmanaged) => Ok(false),
Some(State::Private | State::Public) => Ok(true),
None => {
bail!(
"It is unclear whether branch '{branch_name}' should be managed by GHerrit.\n\
Run 'gherrit manage' to sync it as a GHerrit stack.\n\
Run 'gherrit unmanage' to push it as a standard Git branch."
);
}
}
}
pub fn read_current_branch_and_state(&self) -> Result<(String, Option<State>)> {
let branch_name = self.current_branch();
let branch_name = match branch_name {
HeadState::Attached(bn) | HeadState::Pending(bn) => bn,
HeadState::Detached => {
bail!("Cannot get management state in detached HEAD");
}
};
let state = State::read_from(self, branch_name)?;
Ok((branch_name.clone(), state))
}
}
impl std::ops::Deref for Repo {
type Target = gix::Repository;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
fn get_current_branch(repo: &gix::Repository) -> Result<HeadState> {
if let Some(name) = repo.head()?.referent_name() {
let name = name.shorten().to_string();
return Ok(HeadState::Attached(name));
}
if let Some(InProgress::Rebase) | Some(InProgress::RebaseInteractive) = repo.state() {
let git_dir = repo.path();
let try_read_ref = |path: PathBuf| -> Option<String> {
std::fs::read_to_string(path).ok().map(|content| {
content
.trim()
.strip_prefix("refs/heads/")
.unwrap_or(content.trim())
.to_string()
})
};
if let Some(name) = try_read_ref(git_dir.join("rebase-merge/head-name")) {
return Ok(HeadState::Pending(name));
}
if let Some(name) = try_read_ref(git_dir.join("rebase-apply/head-name")) {
return Ok(HeadState::Pending(name));
}
}
Ok(HeadState::Detached)
}
pub trait CommandExt {
fn success(&mut self) -> Result<()>;
fn checked_output(&mut self) -> Result<std::process::Output>;
}
impl CommandExt for Command {
fn success(&mut self) -> Result<()> {
let status = self.status()?;
if !status.success() {
bail!("Command failed with status: {}", status);
}
Ok(())
}
fn checked_output(&mut self) -> Result<std::process::Output> {
let output = self.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"Command {self:?} failed with status: {}. Stderr: {stderr}",
output.status,
);
}
Ok(output)
}
}
#[cfg(test)]
mod tests {
#[test]
#[should_panic(expected = "Command cannot be empty")]
fn test_cmd_macro_empty_panic() {
cmd!("");
}
#[test]
#[should_panic(expected = "Command cannot be empty")]
fn test_cmd_macro_whitespace_panic() {
cmd!(" ");
}
}