use std::ffi::OsString;
use std::fmt::Write;
use std::path::PathBuf;
use cursive::theme::{BaseColor, Effect, Style};
use cursive::utils::markup::StyledString;
use eyre::Context;
use tracing::{instrument, warn};
use crate::core::formatting::StyledStringBuilder;
use crate::git::{ConfigRead, GitRunInfo, GitRunOpts, Repo};
use super::effects::Effects;
use super::eventlog::EventTransactionId;
#[instrument]
pub fn get_default_hooks_dir(repo: &Repo) -> PathBuf {
repo.get_path().join("hooks")
}
#[instrument]
pub fn get_hooks_dir(
git_run_info: &GitRunInfo,
repo: &Repo,
event_tx_id: Option<EventTransactionId>,
) -> eyre::Result<PathBuf> {
let result = git_run_info
.run_silent(
repo,
event_tx_id,
&["config", "--type", "path", "core.hooksPath"],
GitRunOpts {
treat_git_failure_as_error: false,
..Default::default()
},
)
.context("Reading core.hooksPath")?;
let hooks_path = if result.exit_code.is_success() {
let path = String::from_utf8(result.stdout)
.context("Decoding git config output for hooks path")?;
PathBuf::from(path.strip_suffix('\n').unwrap_or(&path))
} else {
get_default_hooks_dir(repo)
};
Ok(hooks_path)
}
#[instrument]
pub fn get_main_branch_name(repo: &Repo) -> eyre::Result<String> {
let config = repo.get_readonly_config()?;
if let Some(branch_name) = config.get("branchless.core.mainBranch")? {
return Ok(branch_name);
}
if let Some(branch_name) = config.get("branchless.mainBranch")? {
return Ok(branch_name);
}
if let Some(branch_name) = get_default_branch_name(repo)? {
return Ok(branch_name);
}
Ok("master".to_string())
}
#[instrument]
pub fn get_auto_switch_branches(repo: &Repo) -> eyre::Result<bool> {
repo.get_readonly_config()?
.get_or("branchless.navigation.autoSwitchBranches", true)
}
#[instrument]
pub fn get_smartlog_default_revset(repo: &Repo) -> eyre::Result<String> {
repo.get_readonly_config()?
.get_or_else("branchless.smartlog.defaultRevset", || {
"((draft() | branches() | @) % main()) | branches() | @".to_string()
})
}
#[instrument]
pub fn get_comment_char(repo: &Repo) -> eyre::Result<char> {
let from_config: Option<String> = repo.get_readonly_config()?.get("core.commentChar")?;
let comment_char = match from_config {
Some(comment_char) => comment_char.chars().next().unwrap(),
None => char::from(git2::DEFAULT_COMMENT_CHAR.unwrap()),
};
Ok(comment_char)
}
#[instrument]
pub fn get_commit_template(repo: &Repo) -> eyre::Result<Option<String>> {
let commit_template_path: Option<String> =
repo.get_readonly_config()?.get("commit.template")?;
let commit_template_path = match commit_template_path {
Some(commit_template_path) => PathBuf::from(commit_template_path),
None => return Ok(None),
};
let commit_template_path = if commit_template_path.is_relative() {
match repo.get_working_copy_path() {
Some(root) => root.join(commit_template_path),
None => {
warn!(?commit_template_path, "Commit template path was relative, but this repository does not have a working copy");
return Ok(None);
}
}
} else {
commit_template_path
};
match std::fs::read_to_string(&commit_template_path) {
Ok(contents) => Ok(Some(contents)),
Err(e) => {
warn!(?e, ?commit_template_path, "Could not read commit template");
Ok(None)
}
}
}
#[instrument]
pub fn get_default_branch_name(repo: &Repo) -> eyre::Result<Option<String>> {
let config = repo.get_readonly_config()?;
let default_branch_name: Option<String> = config.get("init.defaultBranch")?;
Ok(default_branch_name)
}
#[instrument]
pub fn get_editor(git_run_info: &GitRunInfo, repo: &Repo) -> eyre::Result<Option<OsString>> {
if let Ok(result) =
git_run_info.run_silent(repo, None, &["var", "GIT_EDITOR"], GitRunOpts::default())
{
if result.exit_code.is_success() {
let editor =
std::str::from_utf8(&result.stdout).context("Decoding git var output as UTF-8")?;
let editor = editor.trim_end();
let editor = OsString::from(editor);
return Ok(Some(editor));
} else {
warn!(?result, "`git var` invocation failed");
}
}
let editor = std::env::var_os("GIT_EDITOR");
if editor.is_some() {
return Ok(editor);
}
let config = repo.get_readonly_config()?;
let editor: Option<String> = config.get("core.editor")?;
match editor {
Some(editor) => Ok(Some(editor.into())),
None => Ok(None),
}
}
#[instrument]
pub fn get_undo_create_snapshots(repo: &Repo) -> eyre::Result<bool> {
repo.get_readonly_config()?
.get_or("branchless.undo.createSnapshots", true)
}
#[instrument]
pub fn get_restack_preserve_timestamps(repo: &Repo) -> eyre::Result<bool> {
repo.get_readonly_config()?
.get_or("branchless.restack.preserveTimestamps", false)
}
#[instrument]
pub fn get_next_interactive(repo: &Repo) -> eyre::Result<bool> {
repo.get_readonly_config()?
.get_or("branchless.next.interactive", false)
}
#[instrument]
pub fn get_commit_descriptors_branches(repo: &Repo) -> eyre::Result<bool> {
repo.get_readonly_config()?
.get_or("branchless.commitDescriptors.branches", true)
}
#[instrument]
pub fn get_commit_descriptors_differential_revision(repo: &Repo) -> eyre::Result<bool> {
repo.get_readonly_config()?
.get_or("branchless.commitDescriptors.differentialRevision", true)
}
#[instrument]
pub fn get_commit_descriptors_relative_time(repo: &Repo) -> eyre::Result<bool> {
repo.get_readonly_config()?
.get_or("branchless.commitDescriptors.relativeTime", true)
}
pub const RESTACK_WARN_ABANDONED_CONFIG_KEY: &str = "branchless.restack.warnAbandoned";
#[derive(Clone, Debug)]
pub enum Hint {
CleanCachedTestResults,
MoveImplicitHeadArgument,
RestackWarnAbandoned,
SmartlogFixAbandoned,
TestShowVerbose,
}
impl Hint {
fn get_config_key(&self) -> &'static str {
match self {
Hint::CleanCachedTestResults => "branchless.hint.cleanCachedTestResults",
Hint::MoveImplicitHeadArgument => "branchless.hint.moveImplicitHeadArgument",
Hint::RestackWarnAbandoned => "branchless.hint.restackWarnAbandoned",
Hint::SmartlogFixAbandoned => "branchless.hint.smartlogFixAbandoned",
Hint::TestShowVerbose => "branchless.hint.testShowVerbose",
}
}
}
pub fn get_hint_enabled(repo: &Repo, hint: Hint) -> eyre::Result<bool> {
repo.get_readonly_config()?
.get_or(hint.get_config_key(), true)
}
pub fn get_hint_string() -> StyledString {
StyledStringBuilder::new()
.append_styled(
"hint",
Style::merge(&[BaseColor::Blue.dark().into(), Effect::Bold.into()]),
)
.build()
}
pub fn print_hint_suppression_notice(effects: &Effects, hint: Hint) -> eyre::Result<()> {
writeln!(
effects.get_output_stream(),
"{}: disable this hint by running: git config --global {} false",
effects.get_glyphs().render(get_hint_string())?,
hint.get_config_key(),
)?;
Ok(())
}
pub mod env_vars {
use std::path::PathBuf;
use tracing::instrument;
pub const TEST_GIT: &str = "TEST_GIT";
pub const TEST_GIT_EXEC_PATH: &str = "TEST_GIT_EXEC_PATH";
pub const TEST_SEPARATE_COMMAND_BINARIES: &str = "TEST_SEPARATE_COMMAND_BINARIES";
#[instrument]
pub fn get_path_to_git() -> eyre::Result<PathBuf> {
let path_to_git = std::env::var_os(TEST_GIT).ok_or_else(|| {
eyre::eyre!(
"No path to Git executable was set. \
Try running as: `{0}=$(which git) cargo test ...` \
or set `env.{0}` in your `config.toml` \
(see https://doc.rust-lang.org/cargo/reference/config.html)",
TEST_GIT,
)
})?;
let path_to_git = PathBuf::from(&path_to_git);
Ok(path_to_git)
}
#[instrument]
pub fn get_git_exec_path() -> eyre::Result<PathBuf> {
let git_exec_path = std::env::var_os(TEST_GIT_EXEC_PATH).ok_or_else(|| {
eyre::eyre!(
"No Git exec path was set. \
Try running as: `{0}=$(git --exec-path) cargo test ...` \
or set `env.{0}` in your `config.toml` \
(see https://doc.rust-lang.org/cargo/reference/config.html)",
TEST_GIT_EXEC_PATH,
)
})?;
let git_exec_path = PathBuf::from(&git_exec_path);
Ok(git_exec_path)
}
#[instrument]
pub fn should_use_separate_command_binary(program: &str) -> bool {
let values = match std::env::var("TEST_SEPARATE_COMMAND_BINARIES") {
Ok(value) => value,
Err(_) => return false,
};
let program = program.strip_prefix("git-branchless-").unwrap_or(program);
values
.split_ascii_whitespace()
.any(|value| value.strip_prefix("git-branchless-").unwrap_or(value) == program)
}
}