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_smartlog_reverse(repo: &Repo) -> eyre::Result<bool> {
114 repo.get_readonly_config()?
115 .get_or("branchless.smartlog.reverse", false)
116}
117
118#[instrument]
120pub fn get_comment_char(repo: &Repo) -> eyre::Result<char> {
121 let from_config: Option<String> = repo.get_readonly_config()?.get("core.commentChar")?;
122 let comment_char = match from_config {
123 Some(comment_char) => comment_char.chars().next().unwrap(),
125 None => char::from(git2::DEFAULT_COMMENT_CHAR.unwrap()),
126 };
127 Ok(comment_char)
128}
129
130#[instrument]
132pub fn get_commit_template(repo: &Repo) -> eyre::Result<Option<String>> {
133 let commit_template_path: Option<String> =
134 repo.get_readonly_config()?.get("commit.template")?;
135 let commit_template_path = match commit_template_path {
136 Some(commit_template_path) => PathBuf::from(commit_template_path),
137 None => return Ok(None),
138 };
139
140 let commit_template_path = if commit_template_path.is_relative() {
141 match repo.get_working_copy_path() {
142 Some(root) => root.join(commit_template_path),
143 None => {
144 warn!(
145 ?commit_template_path,
146 "Commit template path was relative, but this repository does not have a working copy"
147 );
148 return Ok(None);
149 }
150 }
151 } else {
152 commit_template_path
153 };
154
155 match std::fs::read_to_string(&commit_template_path) {
156 Ok(contents) => Ok(Some(contents)),
157 Err(e) => {
158 warn!(?e, ?commit_template_path, "Could not read commit template");
159 Ok(None)
160 }
161 }
162}
163
164#[instrument]
166pub fn get_default_branch_name(repo: &Repo) -> eyre::Result<Option<String>> {
167 let config = repo.get_readonly_config()?;
168 let default_branch_name: Option<String> = config.get("init.defaultBranch")?;
169 Ok(default_branch_name)
170}
171
172#[instrument]
181pub fn get_editor(git_run_info: &GitRunInfo, repo: &Repo) -> eyre::Result<Option<OsString>> {
182 if let Ok(result) =
183 git_run_info.run_silent(repo, None, &["var", "GIT_EDITOR"], GitRunOpts::default())
184 {
185 if result.exit_code.is_success() {
186 let editor =
187 std::str::from_utf8(&result.stdout).context("Decoding git var output as UTF-8")?;
188 let editor = editor.trim_end();
189 let editor = OsString::from(editor);
190 return Ok(Some(editor));
191 } else {
192 warn!(?result, "`git var` invocation failed");
193 }
194 }
195
196 let editor = std::env::var_os("GIT_EDITOR");
197 if editor.is_some() {
198 return Ok(editor);
199 }
200
201 let config = repo.get_readonly_config()?;
202 let editor: Option<String> = config.get("core.editor")?;
203 match editor {
204 Some(editor) => Ok(Some(editor.into())),
205 None => Ok(None),
206 }
207}
208
209#[instrument]
212pub fn get_undo_create_snapshots(repo: &Repo) -> eyre::Result<bool> {
213 repo.get_readonly_config()?
214 .get_or("branchless.undo.createSnapshots", true)
215}
216
217#[instrument]
220pub fn get_restack_preserve_timestamps(repo: &Repo) -> eyre::Result<bool> {
221 repo.get_readonly_config()?
222 .get_or("branchless.restack.preserveTimestamps", false)
223}
224
225#[instrument]
228pub fn get_next_interactive(repo: &Repo) -> eyre::Result<bool> {
229 repo.get_readonly_config()?
230 .get_or("branchless.next.interactive", false)
231}
232
233#[instrument]
235pub fn get_commit_descriptors_branches(repo: &Repo) -> eyre::Result<bool> {
236 repo.get_readonly_config()?
237 .get_or("branchless.commitDescriptors.branches", true)
238}
239
240#[instrument]
242pub fn get_commit_descriptors_differential_revision(repo: &Repo) -> eyre::Result<bool> {
243 repo.get_readonly_config()?
244 .get_or("branchless.commitDescriptors.differentialRevision", true)
245}
246
247#[instrument]
249pub fn get_commit_descriptors_relative_time(repo: &Repo) -> eyre::Result<bool> {
250 repo.get_readonly_config()?
251 .get_or("branchless.commitDescriptors.relativeTime", true)
252}
253
254pub const RESTACK_WARN_ABANDONED_CONFIG_KEY: &str = "branchless.restack.warnAbandoned";
256
257#[derive(Clone, Debug)]
259pub enum Hint {
260 AddSkippedFiles,
263
264 CleanCachedTestResults,
266
267 MoveImplicitHeadArgument,
269
270 RestackWarnAbandoned,
272
273 SmartlogFixAbandoned,
275
276 TestShowVerbose,
278}
279
280impl Hint {
281 fn get_config_key(&self) -> &'static str {
282 match self {
283 Hint::AddSkippedFiles => "branchless.hint.addSkippedFiles",
284 Hint::CleanCachedTestResults => "branchless.hint.cleanCachedTestResults",
285 Hint::MoveImplicitHeadArgument => "branchless.hint.moveImplicitHeadArgument",
286 Hint::RestackWarnAbandoned => "branchless.hint.restackWarnAbandoned",
287 Hint::SmartlogFixAbandoned => "branchless.hint.smartlogFixAbandoned",
288 Hint::TestShowVerbose => "branchless.hint.testShowVerbose",
289 }
290 }
291}
292
293pub fn get_hint_enabled(repo: &Repo, hint: Hint) -> eyre::Result<bool> {
295 repo.get_readonly_config()?
296 .get_or(hint.get_config_key(), true)
297}
298
299pub fn get_hint_string() -> StyledString {
301 StyledStringBuilder::new()
302 .append_styled(
303 "hint",
304 Style::merge(&[BaseColor::Blue.dark().into(), Effect::Bold.into()]),
305 )
306 .build()
307}
308
309pub fn print_hint_suppression_notice(effects: &Effects, hint: Hint) -> eyre::Result<()> {
311 writeln!(
312 effects.get_output_stream(),
313 "{}: disable this hint by running: git config --global {} false",
314 effects.get_glyphs().render(get_hint_string())?,
315 hint.get_config_key(),
316 )?;
317 Ok(())
318}
319
320pub mod env_vars {
322 use std::path::PathBuf;
323
324 use tracing::instrument;
325
326 pub const TEST_GIT: &str = "TEST_GIT";
329
330 pub const TEST_GIT_EXEC_PATH: &str = "TEST_GIT_EXEC_PATH";
335
336 pub const TEST_SEPARATE_COMMAND_BINARIES: &str = "TEST_SEPARATE_COMMAND_BINARIES";
366
367 #[instrument]
369 pub fn get_path_to_git() -> eyre::Result<PathBuf> {
370 let path_to_git = std::env::var_os(TEST_GIT).ok_or_else(|| {
371 eyre::eyre!(
372 "No path to Git executable was set. \
373Try running as: `{0}=$(which git) cargo test ...` \
374or set `env.{0}` in your `config.toml` \
375(see https://doc.rust-lang.org/cargo/reference/config.html)",
376 TEST_GIT,
377 )
378 })?;
379 let path_to_git = PathBuf::from(&path_to_git);
380 Ok(path_to_git)
381 }
382
383 #[instrument]
385 pub fn get_git_exec_path() -> eyre::Result<PathBuf> {
386 let git_exec_path = std::env::var_os(TEST_GIT_EXEC_PATH).ok_or_else(|| {
387 eyre::eyre!(
388 "No Git exec path was set. \
389Try running as: `{0}=$(git --exec-path) cargo test ...` \
390or set `env.{0}` in your `config.toml` \
391(see https://doc.rust-lang.org/cargo/reference/config.html)",
392 TEST_GIT_EXEC_PATH,
393 )
394 })?;
395 let git_exec_path = PathBuf::from(&git_exec_path);
396 Ok(git_exec_path)
397 }
398
399 #[instrument]
402 pub fn should_use_separate_command_binary(program: &str) -> bool {
403 let values = match std::env::var("TEST_SEPARATE_COMMAND_BINARIES") {
404 Ok(value) => value,
405 Err(_) => return false,
406 };
407 let program = program.strip_prefix("git-branchless-").unwrap_or(program);
408 values
409 .split_ascii_whitespace()
410 .any(|value| value.strip_prefix("git-branchless-").unwrap_or(value) == program)
411 }
412}