branchless/core/
config.rs1use std::ffi::OsString;
4use std::fmt::Write;
5use std::path::PathBuf;
6
7use cursive::theme::{BaseColor, Effect, Style};
8use cursive::utils::markup::StyledString;
9use eyre::Context;
10use tracing::{instrument, warn};
11
12use crate::core::formatting::StyledStringBuilder;
13use crate::git::{ConfigRead, GitRunInfo, GitRunOpts, Repo};
14
15use super::effects::Effects;
16use super::eventlog::EventTransactionId;
17
18#[instrument]
21pub fn get_default_hooks_dir(repo: &Repo) -> eyre::Result<PathBuf> {
22 let parent_repo = repo.open_worktree_parent_repo()?;
23 let repo = parent_repo.as_ref().unwrap_or(repo);
24 Ok(repo.get_path().join("hooks"))
25}
26
27#[instrument]
38pub fn get_main_worktree_hooks_dir(
39 git_run_info: &GitRunInfo,
40 repo: &Repo,
41 event_tx_id: Option<EventTransactionId>,
42) -> eyre::Result<PathBuf> {
43 let result = git_run_info
44 .run_silent(
45 repo,
46 event_tx_id,
47 &["config", "--type", "path", "core.hooksPath"],
48 GitRunOpts {
49 treat_git_failure_as_error: false,
50 ..Default::default()
51 },
52 )
53 .context("Reading core.hooksPath")?;
54 let hooks_path = if result.exit_code.is_success() {
55 let path = String::from_utf8(result.stdout)
56 .context("Decoding git config output for hooks path")?;
57 PathBuf::from(path.strip_suffix('\n').unwrap_or(&path))
58 } else {
59 get_default_hooks_dir(repo)?
60 };
61 Ok(hooks_path)
62}
63
64#[instrument]
72pub fn get_main_branch_name(repo: &Repo) -> eyre::Result<String> {
73 let config = repo.get_readonly_config()?;
74
75 if let Some(branch_name) = config.get("branchless.core.mainBranch")? {
76 return Ok(branch_name);
77 }
78
79 if let Some(branch_name) = config.get("branchless.mainBranch")? {
80 return Ok(branch_name);
81 }
82
83 if let Some(branch_name) = get_default_branch_name(repo)? {
84 return Ok(branch_name);
85 }
86
87 Ok("master".to_string())
88}
89
90#[instrument]
95pub fn get_auto_switch_branches(repo: &Repo) -> eyre::Result<bool> {
96 repo.get_readonly_config()?
97 .get_or("branchless.navigation.autoSwitchBranches", true)
98}
99
100#[instrument]
104pub fn get_smartlog_default_revset(repo: &Repo) -> eyre::Result<String> {
105 repo.get_readonly_config()?
106 .get_or_else("branchless.smartlog.defaultRevset", || {
107 "((draft() | branches() | @) % main()) | branches() | @".to_string()
108 })
109}
110
111#[instrument]
113pub fn get_comment_char(repo: &Repo) -> eyre::Result<char> {
114 let from_config: Option<String> = repo.get_readonly_config()?.get("core.commentChar")?;
115 let comment_char = match from_config {
116 Some(comment_char) => comment_char.chars().next().unwrap(),
118 None => char::from(git2::DEFAULT_COMMENT_CHAR.unwrap()),
119 };
120 Ok(comment_char)
121}
122
123#[instrument]
125pub fn get_commit_template(repo: &Repo) -> eyre::Result<Option<String>> {
126 let commit_template_path: Option<String> =
127 repo.get_readonly_config()?.get("commit.template")?;
128 let commit_template_path = match commit_template_path {
129 Some(commit_template_path) => PathBuf::from(commit_template_path),
130 None => return Ok(None),
131 };
132
133 let commit_template_path = if commit_template_path.is_relative() {
134 match repo.get_working_copy_path() {
135 Some(root) => root.join(commit_template_path),
136 None => {
137 warn!(?commit_template_path, "Commit template path was relative, but this repository does not have a working copy");
138 return Ok(None);
139 }
140 }
141 } else {
142 commit_template_path
143 };
144
145 match std::fs::read_to_string(&commit_template_path) {
146 Ok(contents) => Ok(Some(contents)),
147 Err(e) => {
148 warn!(?e, ?commit_template_path, "Could not read commit template");
149 Ok(None)
150 }
151 }
152}
153
154#[instrument]
156pub fn get_default_branch_name(repo: &Repo) -> eyre::Result<Option<String>> {
157 let config = repo.get_readonly_config()?;
158 let default_branch_name: Option<String> = config.get("init.defaultBranch")?;
159 Ok(default_branch_name)
160}
161
162#[instrument]
171pub fn get_editor(git_run_info: &GitRunInfo, repo: &Repo) -> eyre::Result<Option<OsString>> {
172 if let Ok(result) =
173 git_run_info.run_silent(repo, None, &["var", "GIT_EDITOR"], GitRunOpts::default())
174 {
175 if result.exit_code.is_success() {
176 let editor =
177 std::str::from_utf8(&result.stdout).context("Decoding git var output as UTF-8")?;
178 let editor = editor.trim_end();
179 let editor = OsString::from(editor);
180 return Ok(Some(editor));
181 } else {
182 warn!(?result, "`git var` invocation failed");
183 }
184 }
185
186 let editor = std::env::var_os("GIT_EDITOR");
187 if editor.is_some() {
188 return Ok(editor);
189 }
190
191 let config = repo.get_readonly_config()?;
192 let editor: Option<String> = config.get("core.editor")?;
193 match editor {
194 Some(editor) => Ok(Some(editor.into())),
195 None => Ok(None),
196 }
197}
198
199#[instrument]
202pub fn get_undo_create_snapshots(repo: &Repo) -> eyre::Result<bool> {
203 repo.get_readonly_config()?
204 .get_or("branchless.undo.createSnapshots", true)
205}
206
207#[instrument]
210pub fn get_restack_preserve_timestamps(repo: &Repo) -> eyre::Result<bool> {
211 repo.get_readonly_config()?
212 .get_or("branchless.restack.preserveTimestamps", false)
213}
214
215#[instrument]
218pub fn get_next_interactive(repo: &Repo) -> eyre::Result<bool> {
219 repo.get_readonly_config()?
220 .get_or("branchless.next.interactive", false)
221}
222
223#[instrument]
225pub fn get_commit_descriptors_branches(repo: &Repo) -> eyre::Result<bool> {
226 repo.get_readonly_config()?
227 .get_or("branchless.commitDescriptors.branches", true)
228}
229
230#[instrument]
232pub fn get_commit_descriptors_differential_revision(repo: &Repo) -> eyre::Result<bool> {
233 repo.get_readonly_config()?
234 .get_or("branchless.commitDescriptors.differentialRevision", true)
235}
236
237#[instrument]
239pub fn get_commit_descriptors_relative_time(repo: &Repo) -> eyre::Result<bool> {
240 repo.get_readonly_config()?
241 .get_or("branchless.commitDescriptors.relativeTime", true)
242}
243
244pub const RESTACK_WARN_ABANDONED_CONFIG_KEY: &str = "branchless.restack.warnAbandoned";
246
247#[derive(Clone, Debug)]
249pub enum Hint {
250 CleanCachedTestResults,
252
253 MoveImplicitHeadArgument,
255
256 RestackWarnAbandoned,
258
259 SmartlogFixAbandoned,
261
262 TestShowVerbose,
264}
265
266impl Hint {
267 fn get_config_key(&self) -> &'static str {
268 match self {
269 Hint::CleanCachedTestResults => "branchless.hint.cleanCachedTestResults",
270 Hint::MoveImplicitHeadArgument => "branchless.hint.moveImplicitHeadArgument",
271 Hint::RestackWarnAbandoned => "branchless.hint.restackWarnAbandoned",
272 Hint::SmartlogFixAbandoned => "branchless.hint.smartlogFixAbandoned",
273 Hint::TestShowVerbose => "branchless.hint.testShowVerbose",
274 }
275 }
276}
277
278pub fn get_hint_enabled(repo: &Repo, hint: Hint) -> eyre::Result<bool> {
280 repo.get_readonly_config()?
281 .get_or(hint.get_config_key(), true)
282}
283
284pub fn get_hint_string() -> StyledString {
286 StyledStringBuilder::new()
287 .append_styled(
288 "hint",
289 Style::merge(&[BaseColor::Blue.dark().into(), Effect::Bold.into()]),
290 )
291 .build()
292}
293
294pub fn print_hint_suppression_notice(effects: &Effects, hint: Hint) -> eyre::Result<()> {
296 writeln!(
297 effects.get_output_stream(),
298 "{}: disable this hint by running: git config --global {} false",
299 effects.get_glyphs().render(get_hint_string())?,
300 hint.get_config_key(),
301 )?;
302 Ok(())
303}
304
305pub mod env_vars {
307 use std::path::PathBuf;
308
309 use tracing::instrument;
310
311 pub const TEST_GIT: &str = "TEST_GIT";
314
315 pub const TEST_GIT_EXEC_PATH: &str = "TEST_GIT_EXEC_PATH";
320
321 pub const TEST_SEPARATE_COMMAND_BINARIES: &str = "TEST_SEPARATE_COMMAND_BINARIES";
351
352 #[instrument]
354 pub fn get_path_to_git() -> eyre::Result<PathBuf> {
355 let path_to_git = std::env::var_os(TEST_GIT).ok_or_else(|| {
356 eyre::eyre!(
357 "No path to Git executable was set. \
358Try running as: `{0}=$(which git) cargo test ...` \
359or set `env.{0}` in your `config.toml` \
360(see https://doc.rust-lang.org/cargo/reference/config.html)",
361 TEST_GIT,
362 )
363 })?;
364 let path_to_git = PathBuf::from(&path_to_git);
365 Ok(path_to_git)
366 }
367
368 #[instrument]
370 pub fn get_git_exec_path() -> eyre::Result<PathBuf> {
371 let git_exec_path = std::env::var_os(TEST_GIT_EXEC_PATH).ok_or_else(|| {
372 eyre::eyre!(
373 "No Git exec path was set. \
374Try running as: `{0}=$(git --exec-path) cargo test ...` \
375or set `env.{0}` in your `config.toml` \
376(see https://doc.rust-lang.org/cargo/reference/config.html)",
377 TEST_GIT_EXEC_PATH,
378 )
379 })?;
380 let git_exec_path = PathBuf::from(&git_exec_path);
381 Ok(git_exec_path)
382 }
383
384 #[instrument]
387 pub fn should_use_separate_command_binary(program: &str) -> bool {
388 let values = match std::env::var("TEST_SEPARATE_COMMAND_BINARIES") {
389 Ok(value) => value,
390 Err(_) => return false,
391 };
392 let program = program.strip_prefix("git-branchless-").unwrap_or(program);
393 values
394 .split_ascii_whitespace()
395 .any(|value| value.strip_prefix("git-branchless-").unwrap_or(value) == program)
396 }
397}