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_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::EnvFilter;
39use tracing_subscriber::fmt as tracing_fmt;
40use tracing_subscriber::prelude::*;
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 unsafe {
75 std::env::set_var(NESTING_LEVEL_KEY, (nesting_level + 1).to_string());
76 }
77
78 let should_include_function_args = match std::env::var("RUST_PROFILE_INCLUDE_ARGS") {
79 Ok(value) if !value.is_empty() => true,
80 Ok(_) | Err(_) => false,
81 };
82
83 let filename = match std::env::var("RUST_PROFILE") {
84 Ok(value) if value == "1" || value == "true" => {
85 let filename = format!(
86 "trace-{}.json-{}",
87 SystemTime::now()
88 .duration_since(SystemTime::UNIX_EPOCH)?
89 .as_secs(),
90 nesting_level,
91 );
92 Some(filename)
93 }
94 Ok(value) if !value.is_empty() => Some(format!("{value}-{nesting_level}")),
95 Ok(_) | Err(_) => None,
96 };
97
98 match filename {
99 Some(filename) => {
100 let (layer, flush_guard) = ChromeLayerBuilder::new()
101 .file(filename)
102 .include_args(should_include_function_args)
103 .build();
104 (Some(layer), Box::new(flush_guard))
105 }
106 None => {
107 struct TrivialDrop;
108 (None, Box::new(TrivialDrop))
109 }
110 }
111 };
112
113 tracing_subscriber::registry()
114 .with(ErrorLayer::default())
115 .with(fmt_layer.with_filter(env_filter))
116 .with(profile_layer)
117 .try_init()?;
118
119 Ok(flush_guard)
120}
121
122#[instrument]
123fn install_libgit2_tracing() {
124 fn git_trace(level: git2::TraceLevel, msg: &[u8]) {
125 info!("[{:?}]: {}", level, String::from_utf8_lossy(msg));
126 }
127
128 if let Err(err) = git2::trace_set(git2::TraceLevel::Trace, git_trace) {
129 warn!("Failed to install libgit2 tracing: {err}");
130 }
131}
132
133#[instrument]
134fn check_unsupported_config_options(effects: &Effects) -> eyre::Result<Option<ExitCode>> {
135 let _repo = match Repo::from_current_dir() {
136 Ok(repo) => repo,
137 Err(RepoError::UnsupportedExtensionWorktreeConfig(_)) => {
138 writeln!(
139 effects.get_output_stream(),
140 "\
141{error}
142
143Usually, this configuration setting is enabled when initializing a sparse
144checkout. See https://github.com/arxanas/git-branchless/issues/278 for more
145information.
146
147Here are some options:
148
149- To unset the configuration option, run: git config --unset extensions.worktreeConfig
150 - This is safe unless you created another worktree also using a sparse checkout.
151- Try upgrading to Git v2.36+ and reinitializing your sparse checkout.",
152 error = effects.get_glyphs().render(StyledString::styled(
153 "\
154Error: the Git configuration setting `extensions.worktreeConfig` is enabled in
155this repository. Due to upstream libgit2 limitations, git-branchless does not
156support repositories with this configuration option enabled.",
157 BaseColor::Red.light()
158 ))?,
159 )?;
160 return Ok(Some(ExitCode(1)));
161 }
162 Err(_) => return Ok(None),
163 };
164
165 Ok(None)
166}
167
168#[instrument(skip(f))]
172pub fn do_main_and_drop_locals<T: Parser>(
173 f: impl Fn(CommandContext, T) -> EyreExitOr<()>,
174 args: Vec<OsString>,
175) -> eyre::Result<i32> {
176 let command = GlobalArgs::command();
177 let command_args = T::parse_from(&args);
178 let matches = command.ignore_errors(true).get_matches_from(&args);
179 let GlobalArgs {
180 working_directory,
181 color,
182 } = GlobalArgs::from_arg_matches(&matches)
183 .map_err(|err| eyre::eyre!("Could not parse global arguments: {err}"))?;
184
185 if let Some(working_directory) = working_directory {
186 std::env::set_current_dir(&working_directory).wrap_err_with(|| {
187 format!(
188 "Could not set working directory to: {:?}",
189 &working_directory
190 )
191 })?;
192 }
193
194 let path_to_git = get_path_to_git().unwrap_or_else(|_| PathBuf::from("git"));
195 let path_to_git = PathBuf::from(&path_to_git);
196 let git_run_info = GitRunInfo {
197 path_to_git,
198 working_directory: std::env::current_dir()?,
199 env: {
200 let mut env: HashMap<OsString, OsString> = std::env::vars_os().collect();
201 if let Ok(git_exec_path) = get_git_exec_path() {
202 env.entry("GIT_EXEC_PATH".into())
203 .or_insert(git_exec_path.into());
204 }
205 env
206 },
207 };
208
209 let color = match color {
210 Some(ColorSetting::Always) => Glyphs::pretty(),
211 Some(ColorSetting::Never) => Glyphs::text(),
212 Some(ColorSetting::Auto) | None => Glyphs::detect(),
213 };
214 let effects = Effects::new(color);
215
216 let _tracing_guard = install_tracing(effects.clone());
217 install_libgit2_tracing();
218
219 if let Some(ExitCode(exit_code)) = check_unsupported_config_options(&effects)? {
220 let exit_code: i32 = exit_code.try_into()?;
221 return Ok(exit_code);
222 }
223
224 let ctx = CommandContext {
225 effects,
226 git_run_info,
227 };
228 let exit_code = match f(ctx, command_args)? {
229 Ok(()) => 0,
230 Err(ExitCode(exit_code)) => {
231 let exit_code: i32 = exit_code.try_into()?;
232 exit_code
233 }
234 };
235 Ok(exit_code)
236}
237
238#[instrument(skip(f))]
247pub fn invoke_subcommand_main<T: Parser>(f: impl Fn(CommandContext, T) -> EyreExitOr<()>) {
248 color_eyre::install().expect("Could not install panic handler");
250 let args = std::env::args_os().collect();
251 let exit_code = do_main_and_drop_locals(f, args).expect("A fatal error occurred");
252 std::process::exit(exit_code);
253}