use eyre::{Result, WrapErr, bail};
use owo_colors::OwoColorize;
use crate::cmd;
use crate::util::{self, CommandExt as _, HeadState};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
Unmanaged,
Private,
Public,
}
impl State {
const UNMANAGED: &str = "false";
const PRIVATE: &str = "managedPrivate";
const PUBLIC: &str = "managedPublic";
pub fn read_from(repo: &util::Repo, branch_name: &str) -> Result<Option<State>> {
let key = format!("branch.{}.gherritManaged", branch_name);
match repo.config_string(&key)?.as_deref() {
Some(State::PUBLIC) => Ok(Some(State::Public)),
Some(State::PRIVATE) => Ok(Some(State::Private)),
Some(State::UNMANAGED) => Ok(Some(State::Unmanaged)),
None => Ok(None),
Some(unknown) => bail!(
"Invalid gherritManaged value: {}. Expected {}, {}, or {}.",
unknown.yellow(),
State::PUBLIC.yellow(),
State::PRIVATE.yellow(),
State::UNMANAGED.yellow()
),
}
}
}
#[derive(Debug, PartialEq, Eq)]
struct BranchConfig {
push_remote: Option<String>,
remote: Option<String>,
merge: Option<String>,
}
impl BranchConfig {
fn expected(state: Option<State>, branch_name: &str, default_remote: &str) -> BranchConfig {
let self_merge_ref = format!("refs/heads/{branch_name}");
BranchConfig {
push_remote: match state {
Some(State::Unmanaged) | None => None,
Some(State::Private) => Some(".".to_string()),
Some(State::Public) => Some(default_remote.to_string()),
},
remote: match state {
Some(State::Unmanaged) | None => None,
Some(State::Private | State::Public) => Some(".".to_string()),
},
merge: match state {
Some(State::Unmanaged) | None => None,
Some(State::Private | State::Public) => Some(self_merge_ref),
},
}
}
fn read_from(repo: &util::Repo, branch_name: &str) -> Result<BranchConfig> {
let key = |suffix: &str| format!("branch.{branch_name}.{suffix}");
Ok(BranchConfig {
push_remote: repo.config_string(&key("pushRemote"))?,
remote: repo.config_string(&key("remote"))?,
merge: repo.config_string(&key("merge"))?,
})
}
}
pub fn set_state(repo: &util::Repo, new_state: State, force: bool) -> Result<()> {
use State::*;
let (branch_name, old_state) = repo.read_current_branch_and_state()?;
let default_remote = repo.default_remote_name();
let key = |suffix: &str| format!("branch.{branch_name}.{suffix}");
match (old_state, new_state) {
(Some(Unmanaged), Unmanaged) => {
log::debug!(
"Branch {} is already in the desired state ({new_state:?}).",
branch_name.yellow(),
);
return Ok(());
}
(None, Unmanaged) => {}
(Some(Unmanaged) | None, Private | Public)
| (Some(Private), Unmanaged | Public | Private)
| (Some(Public), Unmanaged | Private | Public) => {
let current_config = BranchConfig::read_from(repo, &branch_name)?;
let expected_old_config =
BranchConfig::expected(old_state, &branch_name, &default_remote);
if current_config != expected_old_config {
log::warn!(
"Configuration drift detected for branch {}.",
branch_name.yellow()
);
let (article, state) = match old_state {
Some(State::Unmanaged) | None => ("an", "unmanaged"),
Some(State::Private) => ("a", "private"),
Some(State::Public) => ("a", "public"),
};
log::warn!(
"The current git configuration does not match the expected state for {article} {} branch.",
state.yellow(),
);
let check_diff =
|key: &str, current: &Option<String>, expected: &Option<String>| {
if current != expected {
let curr_s = current.as_deref().unwrap_or("<unset>");
let exp_s = expected.as_deref().unwrap_or("<unset>");
log::warn!(
" - {key}: current='{}', expected='{}'",
curr_s.yellow(),
exp_s.yellow()
);
}
};
let (c, e) = (¤t_config, expected_old_config);
check_diff("pushRemote", &c.push_remote, &e.push_remote);
check_diff("remote", &c.remote, &e.remote);
check_diff("merge", &c.merge, &e.merge);
if force {
log::warn!("Overwriting manual changes (--force).");
} else {
log::warn!("Use --force to overwrite manual changes.");
return Ok(());
}
}
let apply_config =
|k: String, old: Option<String>, new: Option<String>| -> Result<()> {
match (old, new) {
(old, Some(new)) => {
if old.as_ref() != Some(&new) {
cmd!("git config", k, new).success()
} else {
Ok(())
}
}
(Some(_), None) => cmd!("git config --unset", k).success(),
(None, None) => Ok(()),
}
};
let new_config = BranchConfig::expected(Some(new_state), &branch_name, &default_remote);
let (c, n) = (current_config, new_config);
apply_config(key("pushRemote"), c.push_remote, n.push_remote)?;
apply_config(key("remote"), c.remote, n.remote)?;
apply_config(key("merge"), c.merge, n.merge)?;
}
};
let state_val = match new_state {
State::Unmanaged => State::UNMANAGED,
State::Private => State::PRIVATE,
State::Public => State::PUBLIC,
};
cmd!("git config", key("gherritManaged"), state_val).success()?;
let branch_name_y = branch_name.yellow();
match new_state {
State::Unmanaged => {
let unmanaged_r = "unmanaged".red();
log::info!("Branch {branch_name_y} is now {unmanaged_r} by GHerrit.");
}
#[rustfmt::skip]
State::Private => {
let managed_g = "managed".green();
log::info!("Branch {branch_name_y} is now {managed_g} by GHerrit in private mode.");
log::info!(" - 'git push' will sync PRs only, but will not push {branch_name_y} itself.");
}
State::Public => {
let managed_g = "managed".green();
log::info!("Branch {branch_name_y} is now {managed_g} by GHerrit in public mode.");
log::info!(" - 'git push' will sync PRs and will also push {branch_name_y} itself.");
}
}
Ok(())
}
pub fn post_checkout(repo: &util::Repo, _prev: &str, _new: &str, flag: &str) -> Result<()> {
if flag != "1" {
return Ok(());
}
let branch_name = repo.current_branch();
let branch_name = match branch_name {
HeadState::Attached(bn) => bn,
HeadState::Pending(_) | HeadState::Detached => return Ok(()),
};
let current_state =
State::read_from(repo, branch_name).wrap_err("Failed to parse gherritState")?;
let state_str = match current_state {
Some(State::Unmanaged) | None => None,
Some(State::Private) => Some("private"),
Some(State::Public) => Some("public"),
};
if let Some(state) = state_str {
log::debug!(
"Branch {} is already configured as {} by GHerrit.",
branch_name.yellow(),
state.yellow()
);
return Ok(());
}
let is_new = repo
.is_newly_created_branch(branch_name)
.wrap_err("Failed to check if branch is new")?;
if !is_new {
log::debug!(" Branch '{}' is not newly created.", branch_name);
return Ok(());
}
let upstream_remote = repo
.config_string(&format!("branch.{branch_name}.remote"))
.wrap_err("Failed to read config")?;
let upstream_merge = repo
.config_string(&format!("branch.{branch_name}.merge"))
.wrap_err("Failed to read config")?;
let is_default_branch = upstream_merge
.as_deref()
.map(|merge| {
let branch_name = merge.strip_prefix("refs/heads/").unwrap_or(merge);
repo.is_a_default_branch_on_default_remote(branch_name)
})
.unwrap_or(false);
let has_upstream = upstream_remote.is_some() && upstream_merge.is_some();
let branch_name_yellow = branch_name.yellow();
if has_upstream && !is_default_branch {
log::info!("Detected {branch_name_yellow} as a shared branch.");
set_state(repo, State::Unmanaged, false)?;
log::info!("To have GHerrit manage this branch, run: gherrit manage");
} else {
log::info!("Detected {branch_name_yellow} as a new branch.");
set_state(repo, State::Private, false)?;
log::info!("To opt-out, run: gherrit unmanage");
}
Ok(())
}