Skip to main content

gitversion_rs/
app.rs

1//! Application entry logic (called from the binary). Lives inside the lib so the `t!` macro is available.
2
3use crate::cli::{Cli, OutputFormat};
4use crate::{buildagent, cache, cli, config, exec, git, i18n, output, remote, tui, version};
5use anyhow::{Context, Result};
6use clap::FromArgMatches;
7use rust_i18n::t;
8use std::io::Write;
9use std::path::PathBuf;
10
11/// Detect the locale from `--lang` or environment variables before clap parsing, so that
12/// `--help` and `--version` output also respects the locale. Recognises both `--lang ko` and `--lang=ko`.
13fn pre_detect_lang(raw: &[String]) -> Option<String> {
14    let mut it = raw.iter();
15    while let Some(a) = it.next() {
16        if let Some(v) = a.strip_prefix("--lang=") {
17            return Some(v.to_string());
18        }
19        if a == "--lang" {
20            return it.next().cloned();
21        }
22    }
23    None
24}
25
26/// Binary entry point: run the application and print an error message then exit on failure.
27pub fn main() {
28    if let Err(e) = run() {
29        eprintln!("{}", t!("error.generic", error = format!("{e:#}")));
30        std::process::exit(1);
31    }
32}
33
34fn run() -> Result<()> {
35    // Set the locale before parsing so --help/--version output also uses it.
36    let raw: Vec<String> = std::env::args().collect();
37    i18n::init(pre_detect_lang(&raw).as_deref());
38
39    // Parse with the localised help/about text (--help/--version exits here).
40    let matches = cli::localized_command().get_matches();
41    let args = Cli::from_arg_matches(&matches).unwrap_or_else(|e| e.exit());
42
43    // Logging: RUST_LOG takes priority; otherwise derive the level from --verbosity / --diag.
44    let level = if args.diag {
45        log::LevelFilter::Trace
46    } else {
47        args.verbosity.to_level()
48    };
49    // Log destination: file (append) with --log <FILE>, stderr with --log console,
50    // or stderr by default. stdout is always reserved for the version output so that
51    // `$(gitversion ...)` captures stay clean.
52    let mut builder = env_logger::Builder::new();
53    builder.filter_level(level).parse_default_env();
54    match &args.log_file {
55        // Mirrors the original GitVersion `/l console`: log to the console (stderr) rather than a file.
56        Some(path) if path.as_os_str().eq_ignore_ascii_case("console") => {
57            builder
58                .format_timestamp(None)
59                .target(env_logger::Target::Stderr);
60        }
61        Some(path) => {
62            let file = std::fs::OpenOptions::new()
63                .create(true)
64                .append(true)
65                .open(path)
66                .with_context(|| t!("error.log_open", path = path.display()))?;
67            // Include timestamps in file logs (matching the character of the original GitVersion log files).
68            builder.target(env_logger::Target::Pipe(Box::new(file)));
69        }
70        None => {
71            builder
72                .format_timestamp(None)
73                .target(env_logger::Target::Stderr);
74        }
75    }
76    builder.init();
77
78    // If --url is given, clone the remote repository dynamically and use the clone path as the target.
79    let target = if let Some(url) = &args.url {
80        let opts = remote::DynamicRepoOptions {
81            url: url.clone(),
82            branch: args.branch.clone(),
83            username: args.username.clone(),
84            password: args.password.clone(),
85            commit: args.commit.clone(),
86            location: args.dynamic_repo_location.clone(),
87        };
88        remote::prepare(&opts).with_context(|| t!("error.dynamic_repo").to_string())?
89    } else {
90        args.target_path
91            .clone()
92            .unwrap_or_else(|| args.path.clone())
93    };
94    log::debug!("target path: {}", target.display());
95
96    let repo = git::GitRepo::discover(&target).with_context(|| t!("error.git_open").to_string())?;
97    let work_dir = target.canonicalize().unwrap_or(target);
98    let repo_root: Option<PathBuf> = repo.workdir().map(|p| p.to_path_buf());
99
100    let mut configuration =
101        config::loader::load(args.config.as_deref(), &work_dir, repo_root.as_deref())?;
102    cli::apply_overrides(&mut configuration, &args.override_config);
103
104    if args.show_config {
105        println!("{}", serde_yaml::to_string(&configuration)?);
106        return Ok(());
107    }
108
109    if args.tui {
110        return tui::run(repo, configuration, work_dir);
111    }
112
113    // Cache key inputs: overrideconfig values + branch override.
114    let mut key_inputs = args.override_config.clone();
115    if let Some(b) = &args.branch {
116        key_inputs.push(format!("branch={b}"));
117    }
118    let config_path = args
119        .config
120        .clone()
121        .or_else(|| config::loader::locate(&work_dir, repo_root.as_deref()));
122    let cache_key = if args.nocache {
123        None
124    } else {
125        Some(cache::compute_key(
126            &repo,
127            config_path.as_deref(),
128            &key_inputs,
129        ))
130    };
131
132    let mut variables = match cache_key.as_deref().and_then(|k| cache::load(&repo, k)) {
133        Some(v) => v,
134        None => {
135            let v = version::calculation::calculate(&repo, &configuration, args.branch.clone())
136                .with_context(|| t!("error.calc_failed").to_string())?;
137            if let Some(k) = &cache_key {
138                cache::store(&repo, k, &v);
139            }
140            v
141        }
142    };
143
144    // version hook: modify the version via external command output and recalculate.
145    let version_cmd = args
146        .exec_version
147        .clone()
148        .or_else(|| configuration.exec.get("version").cloned());
149    if let Some(cmd) = version_cmd {
150        if let Some(new_ver) = exec::run_version_hook(&cmd, &variables, &work_dir, args.dry_run)? {
151            log::info!("{}", t!("log.version_hook_modified", ver = new_ver));
152            configuration.next_version = Some(new_ver);
153            variables = version::calculation::calculate(&repo, &configuration, args.branch.clone())
154                .with_context(|| t!("error.version_hook_recalc").to_string())?;
155        }
156    }
157
158    // File output.
159    if let Some(files) = &args.update_assembly_info {
160        for p in output::files::update_assembly_info(
161            &variables,
162            &work_dir,
163            files,
164            args.ensure_assembly_info,
165        )? {
166            log::info!("{}", t!("log.assemblyinfo_updated", path = p.display()));
167        }
168    }
169    if let Some(files) = &args.update_project_files {
170        for p in output::files::update_project_files(&variables, &work_dir, files)? {
171            log::info!("{}", t!("log.projectfile_updated", path = p.display()));
172        }
173    }
174    if args.update_wix_version_file {
175        let p = output::files::write_wix(&variables, &work_dir)?;
176        log::info!("{}", t!("log.wix_created", path = p.display()));
177    }
178    if let Some(files) = &args.update_package_files {
179        for p in output::files::update_package_files(&variables, &work_dir, files)? {
180            log::info!("{}", t!("log.package_updated", path = p.display()));
181        }
182    }
183
184    // External command hooks.
185    if !configuration.exec.is_empty() || args.exec.is_some() {
186        exec::run_hooks(
187            &configuration.exec,
188            args.exec.as_deref(),
189            &variables,
190            &work_dir,
191            args.dry_run,
192        )?;
193    }
194
195    // Single variable / format string.
196    if let Some(name) = &args.show_variable {
197        return emit(&args, output::generator::show_variable(&variables, name)?);
198    }
199    if let Some(template) = &args.format {
200        return emit(
201            &args,
202            output::generator::format_template(&variables, template)?,
203        );
204    }
205
206    // Output format rendering.
207    let mut rendered = String::new();
208    for (i, fmt) in args.output.iter().enumerate() {
209        if i > 0 {
210            rendered.push('\n');
211        }
212        match fmt {
213            // `File` mirrors the original `/output file`: rendered as JSON, then written to --outputfile by `emit`.
214            OutputFormat::Json | OutputFormat::File => {
215                rendered.push_str(&output::generator::to_json(&variables)?)
216            }
217            OutputFormat::DotEnv => rendered.push_str(&output::generator::to_dotenv(&variables)),
218            OutputFormat::BuildServer => match buildagent::detect() {
219                Some(agent) => {
220                    log::info!("{}", t!("log.agent_detected", name = agent.name()));
221                    let ubn = configuration.update_build_number.unwrap_or(true);
222                    rendered.push_str(&agent.write_integration(&variables, ubn).join("\n"));
223                }
224                None => rendered.push_str(&output::generator::to_buildserver_env(&variables)),
225            },
226        }
227    }
228    emit(&args, rendered)
229}
230
231/// Write the result to a file or to stdout.
232fn emit(args: &Cli, content: String) -> Result<()> {
233    if let Some(path) = &args.output_file {
234        let mut f = std::fs::File::create(path)
235            .with_context(|| t!("error.output_file", path = path.display()).to_string())?;
236        f.write_all(content.as_bytes())?;
237        if !content.ends_with('\n') {
238            f.write_all(b"\n")?;
239        }
240        log::info!("{}", t!("log.result_written", path = path.display()));
241    } else {
242        println!("{content}");
243    }
244    Ok(())
245}