git_policy_cli/lfs/lfs_interactive.rs
1//! What:
2//! The Git LFS step before a push the caller typed: the pre-check, the plan and the upload, with the caller's streams.
3//! Why:
4//! The parent owns the whole step, so a refusal, a plan that cannot be worked out and a failed upload are all explained
5//! by the process that saw them, and the push is not started after any of them. The configuration the plan reads comes
6//! from the snapshot the wrapper already took when it planned the invocation, so a push whose repository is known costs
7//! no Git start for it; without a snapshot one `config --get-regexp` start reads it.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! const stop = await uploadBeforePush({ prefix, region, realGit, environment, overlay, settings }); if (stop !== undefined) return stop;
12//! ```
13
14/// The invocation's directory option.
15use crate::commit_command::effective_directory;
16/// The builder of real-Git commands.
17use crate::forwarding::git_command;
18/// The refusal texts.
19use crate::lfs_pre_push::plan_failed_message;
20/// The pre-check that the program can start.
21use crate::lfs_push_precheck::lfs_refusal;
22/// Narrows the plan to the objects the push introduces.
23use crate::lfs_object_plan::narrow_plan;
24/// The upload itself.
25use crate::lfs_upload::{run_upload_interactive, submodule_notice};
26/// The repository found from the files.
27use crate::git_system_config::with_real_git;
28use crate::repo_read_layout::read_layout_after_success_for;
29/// What to upload.
30use crate::lfs_upload_plan::{Query, Settings, plan_upload, read_settings};
31/// Owned OS strings for Git arguments.
32use std::ffi::OsString;
33/// Standard error as a writable stream.
34use std::io::Write;
35/// The real Git.
36use std::path::Path;
37
38/// What the step needs from the invocation.
39///
40/// In TS you'd write (pseudocode):
41/// ```ts
42/// type InteractiveUpload = { prefix: string[]; region: string[]; realGit: string; environment; overlay; settings?: Settings };
43/// ```
44pub struct InteractiveUpload<'a> {
45 /// The global options before `push`.
46 pub prefix: &'a [OsString],
47 /// The arguments after `push`.
48 pub region: &'a [OsString],
49 /// The real Git, whose queries fill in what the invocation does not hold.
50 pub real_git: &'a Path,
51 /// The invocation's environment.
52 pub environment: &'a [(OsString, OsString)],
53 /// Variables added to the environment of every Git the wrapper starts.
54 pub overlay: &'a [(OsString, OsString)],
55 /// The configuration entries the plan reads, when the invocation already holds them.
56 pub settings: Option<&'a [(String, String)]>,
57}
58
59/// What:
60/// Run the step; the status to stop with when the push must not start, or nothing when it may.
61/// Why:
62/// A missing `git-lfs`, a plan that cannot be worked out and a failed upload each end the push before it starts.
63///
64/// In TS you'd write (pseudocode):
65/// ```ts
66/// function uploadFirst(request: InteractiveUpload): number | undefined;
67/// ```
68pub fn upload_first(request: &InteractiveUpload<'_>) -> Option<i32> {
69 if let Some(refusal) = lfs_refusal(request.prefix, request.region, request.environment) {
70 // Terminal output error: the exit status already carries the refusal.
71 let _written = std::io::stderr().write_all(refusal.as_bytes());
72 return Some(1);
73 }
74 let mut query = |query_arguments: &[&str]| -> Result<String, String> {
75 let mut arguments: Vec<OsString> = request.prefix.to_vec();
76 arguments.extend(query_arguments.iter().map(OsString::from));
77 let output = git_command(request.real_git, arguments.as_slice(), request.overlay)
78 .output()
79 .map_err(|error| {
80 return error.to_string();
81 })?;
82 let text: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_owned();
83 return match output.status.code() {
84 Some(0) => Ok(text),
85 Some(1) if text.is_empty() => Ok(text),
86 code => Err(format!("exited with {code:?}: {}", String::from_utf8_lossy(output.stderr.as_slice()).trim())),
87 };
88 };
89 let directory: std::path::PathBuf =
90 effective_directory(std::env::current_dir().unwrap_or_default().as_path(), request.prefix);
91 let read: Result<Settings, _> = match request.settings {
92 Some(held) => Ok(held.to_vec()),
93 None => read_settings(&mut query as Query<'_>),
94 };
95 let planned = read.and_then(|settings| {
96 return plan_upload(request.prefix, request.region, request.environment, directory.as_path(), settings.as_slice(), &mut query);
97 });
98 match planned {
99 Ok(Some(plan)) => {
100 // The layout is read from the files; a repository the readers decline keeps the plan, which `git-lfs` scans.
101 // The plan came from Git queries that exited with status 0 (the settings may be held from an earlier query),
102 // so Git had its say about the values it consults;
103 // the files are read again,
104 // and a plan that cannot be narrowed is still pushed by `git-lfs` and Git.
105 let layout = read_layout_after_success_for(request.prefix, with_real_git(request.overlay, request.real_git).as_slice());
106 let submodules: bool = plan.submodules;
107 let Some(narrowed) = narrow_plan(plan, layout.as_ref()) else {
108 // Nothing of this repository to upload, but Git still pushes the submodules, whose objects are not covered.
109 if submodules {
110 // Terminal output error: the notice is advisory.
111 let _written = std::io::stderr().write_all(submodule_notice().as_bytes());
112 }
113 return None;
114 };
115 return run_upload_interactive(
116 &narrowed,
117 request.prefix,
118 directory.as_path(),
119 request.real_git,
120 request.environment,
121 request.overlay,
122 &mut std::io::stderr(),
123 );
124 }
125 Ok(None) => return None,
126 Err(error) => {
127 // Terminal output error: the exit status already carries the refusal.
128 let _written = std::io::stderr().write_all(plan_failed_message(error.0.as_str()).as_bytes());
129 return Some(1);
130 }
131 }
132}