git_branchless_invoke/
lib.rs1#![warn(missing_docs)]
8#![warn(
9 clippy::all,
10 clippy::as_conversions,
11 clippy::clone_on_ref_ptr,
12 clippy::dbg_macro
13)]
14#![allow(clippy::too_many_arguments, clippy::blocks_in_if_conditions)]
15
16use std::any::Any;
17use std::collections::HashMap;
18use std::ffi::OsString;
19use std::fmt::Write;
20use std::path::PathBuf;
21use std::time::SystemTime;
22
23use clap::{CommandFactory, FromArgMatches, Parser};
24use cursive_core::theme::BaseColor;
25use cursive_core::utils::markup::StyledString;
26use eyre::Context;
27use git_branchless_opts::{ColorSetting, GlobalArgs};
28use lib::core::config::env_vars::{get_git_exec_path, get_path_to_git};
29use lib::core::effects::Effects;
30use lib::core::formatting::Glyphs;
31use lib::git::GitRunInfo;
32use lib::git::{Repo, RepoError};
33use lib::util::{ExitCode, EyreExitOr};
34use tracing::level_filters::LevelFilter;
35use tracing::{info, instrument, warn};
36use tracing_chrome::ChromeLayerBuilder;
37use tracing_error::ErrorLayer;
38use tracing_subscriber::fmt as tracing_fmt;
39use tracing_subscriber::prelude::*;
40use tracing_subscriber::EnvFilter;
41
42#[derive(Clone, Debug)]
44pub struct CommandContext {
45 pub effects: Effects,
47
48 pub git_run_info: GitRunInfo,
50}
51
52#[must_use = "This function returns a guard object to flush traces. Dropping it immediately is probably incorrect. Make sure that the returned value lives until tracing has finished."]
53#[instrument]
54fn install_tracing(effects: Effects) -> eyre::Result<impl Drop> {
55 let env_filter = EnvFilter::builder()
56 .with_default_directive(LevelFilter::WARN.into())
57 .parse(std::env::var(EnvFilter::DEFAULT_ENV).unwrap_or_else(|_|
58 "git_branchless=warn".to_string()))?;
62 let fmt_layer = tracing_fmt::layer().with_writer(move || effects.clone().get_error_stream());
63
64 let (profile_layer, flush_guard): (_, Box<dyn Any>) = {
65 const NESTING_LEVEL_KEY: &str = "RUST_LOGGING_NESTING_LEVEL";
68 let nesting_level = match std::env::var(NESTING_LEVEL_KEY) {
69 Ok(nesting_level) => nesting_level.parse::<usize>().unwrap_or_default(),
70 Err(_) => 0,
71 };
72 std::env::set_var(NESTING_LEVEL_KEY, (nesting_level + 1).to_string());
73
74 let should_include_function_args = match std::env::var("RUST_PROFILE_INCLUDE_ARGS") {
75 Ok(value) if !value.is_empty() => true,
76 Ok(_) | Err(_) => false,
77 };
78
79 let filename = match std::env::var("RUST_PROFILE") {
80 Ok(value) if value == "1" || value == "true" => {
81 let filename = format!(
82 "trace-{}.json-{}",
83 SystemTime::now()
84 .duration_since(SystemTime::UNIX_EPOCH)?
85 .as_secs(),
86 nesting_level,
87 );
88 Some(filename)
89 }
90 Ok(value) if !value.is_empty() => Some(format!("{value}-{nesting_level}")),
91 Ok(_) | Err(_) => None,
92 };
93
94 match filename {
95 Some(filename) => {
96 let (layer, flush_guard) = ChromeLayerBuilder::new()
97 .file(filename)
98 .include_args(should_include_function_args)
99 .build();
100 (Some(layer), Box::new(flush_guard))
101 }
102 None => {
103 struct TrivialDrop;
104 (None, Box::new(TrivialDrop))
105 }
106 }
107 };
108
109 tracing_subscriber::registry()
110 .with(ErrorLayer::default())
111 .with(fmt_layer.with_filter(env_filter))
112 .with(profile_layer)
113 .try_init()?;
114
115 Ok(flush_guard)
116}
117
118#[instrument]
119fn install_libgit2_tracing() {
120 fn git_trace(level: git2::TraceLevel, msg: &str) {
121 info!("[{:?}]: {}", level, msg);
122 }
123
124 if !git2::trace_set(git2::TraceLevel::Trace, git_trace) {
125 warn!("Failed to install libgit2 tracing");
126 }
127}
128
129#[instrument]
130fn check_unsupported_config_options(effects: &Effects) -> eyre::Result<Option<ExitCode>> {
131 let _repo = match Repo::from_current_dir() {
132 Ok(repo) => repo,
133 Err(RepoError::UnsupportedExtensionWorktreeConfig(_)) => {
134 writeln!(
135 effects.get_output_stream(),
136 "\
137{error}
138
139Usually, this configuration setting is enabled when initializing a sparse
140checkout. See https://github.com/arxanas/git-branchless/issues/278 for more
141information.
142
143Here are some options:
144
145- To unset the configuration option, run: git config --unset extensions.worktreeConfig
146 - This is safe unless you created another worktree also using a sparse checkout.
147- Try upgrading to Git v2.36+ and reinitializing your sparse checkout.",
148 error = effects.get_glyphs().render(StyledString::styled(
149 "\
150Error: the Git configuration setting `extensions.worktreeConfig` is enabled in
151this repository. Due to upstream libgit2 limitations, git-branchless does not
152support repositories with this configuration option enabled.",
153 BaseColor::Red.light()
154 ))?,
155 )?;
156 return Ok(Some(ExitCode(1)));
157 }
158 Err(_) => return Ok(None),
159 };
160
161 Ok(None)
162}
163
164#[instrument(skip(f))]
168pub fn do_main_and_drop_locals<T: Parser>(
169 f: impl Fn(CommandContext, T) -> EyreExitOr<()>,
170 args: Vec<OsString>,
171) -> eyre::Result<i32> {
172 let command = GlobalArgs::command();
173 let command_args = T::parse_from(&args);
174 let matches = command.ignore_errors(true).get_matches_from(&args);
175 let GlobalArgs {
176 working_directory,
177 color,
178 } = GlobalArgs::from_arg_matches(&matches)
179 .map_err(|err| eyre::eyre!("Could not parse global arguments: {err}"))?;
180
181 if let Some(working_directory) = working_directory {
182 std::env::set_current_dir(&working_directory).wrap_err_with(|| {
183 format!(
184 "Could not set working directory to: {:?}",
185 &working_directory
186 )
187 })?;
188 }
189
190 let path_to_git = get_path_to_git().unwrap_or_else(|_| PathBuf::from("git"));
191 let path_to_git = PathBuf::from(&path_to_git);
192 let git_run_info = GitRunInfo {
193 path_to_git,
194 working_directory: std::env::current_dir()?,
195 env: {
196 let mut env: HashMap<OsString, OsString> = std::env::vars_os().collect();
197 if let Ok(git_exec_path) = get_git_exec_path() {
198 env.entry("GIT_EXEC_PATH".into())
199 .or_insert(git_exec_path.into());
200 }
201 env
202 },
203 };
204
205 let color = match color {
206 Some(ColorSetting::Always) => Glyphs::pretty(),
207 Some(ColorSetting::Never) => Glyphs::text(),
208 Some(ColorSetting::Auto) | None => Glyphs::detect(),
209 };
210 let effects = Effects::new(color);
211
212 let _tracing_guard = install_tracing(effects.clone());
213 install_libgit2_tracing();
214
215 if let Some(ExitCode(exit_code)) = check_unsupported_config_options(&effects)? {
216 let exit_code: i32 = exit_code.try_into()?;
217 return Ok(exit_code);
218 }
219
220 let ctx = CommandContext {
221 effects,
222 git_run_info,
223 };
224 let exit_code = match f(ctx, command_args)? {
225 Ok(()) => 0,
226 Err(ExitCode(exit_code)) => {
227 let exit_code: i32 = exit_code.try_into()?;
228 exit_code
229 }
230 };
231 Ok(exit_code)
232}
233
234#[instrument(skip(f))]
243pub fn invoke_subcommand_main<T: Parser>(f: impl Fn(CommandContext, T) -> EyreExitOr<()>) {
244 color_eyre::install().expect("Could not install panic handler");
246 let args = std::env::args_os().collect();
247 let exit_code = do_main_and_drop_locals(f, args).expect("A fatal error occurred");
248 std::process::exit(exit_code);
249}