Skip to main content

git_policy_cli/
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 super::commit_command::effective_directory;
16/// The builder of real-Git commands.
17use super::forwarding::git_command;
18/// The refusal texts.
19use super::lfs_pre_push::plan_failed_message;
20/// The pre-check that the program can start.
21use super::lfs_push_precheck::lfs_refusal;
22/// Narrows the plan to the objects the push introduces.
23use super::lfs_object_plan::narrow_plan;
24/// The upload itself.
25use super::lfs_upload::run_upload_interactive;
26/// The repository found from the files.
27use super::repo_read_layout::read_layout;
28/// What to upload.
29use super::lfs_upload_plan::{Query, Settings, plan_upload, read_settings};
30/// Owned OS strings for Git arguments.
31use std::ffi::OsString;
32/// Standard error as a writable stream.
33use std::io::Write;
34/// The real Git.
35use std::path::Path;
36
37/// What the step needs from the invocation.
38///
39/// In TS you'd write (pseudocode):
40/// ```ts
41/// type InteractiveUpload = { prefix: string[]; region: string[]; realGit: string; environment; overlay; settings?: Settings };
42/// ```
43pub struct InteractiveUpload<'a> {
44    /// The global options before `push`.
45    pub prefix: &'a [OsString],
46    /// The arguments after `push`.
47    pub region: &'a [OsString],
48    /// The real Git, whose queries fill in what the invocation does not hold.
49    pub real_git: &'a Path,
50    /// The invocation's environment.
51    pub environment: &'a [(OsString, OsString)],
52    /// Variables added to the environment of every Git the wrapper starts.
53    pub overlay: &'a [(OsString, OsString)],
54    /// The configuration entries the plan reads, when the invocation already holds them.
55    pub settings: Option<&'a [(String, String)]>,
56}
57
58/// What:
59///  Run the step; the status to stop with when the push must not start, or nothing when it may.
60/// Why:
61///   A missing `git-lfs`, a plan that cannot be worked out and a failed upload each end the push before it starts.
62///
63/// In TS you'd write (pseudocode):
64/// ```ts
65/// function uploadFirst(request: InteractiveUpload): number | undefined;
66/// ```
67pub fn upload_first(request: &InteractiveUpload<'_>) -> Option<i32> {
68    if let Some(refusal) = lfs_refusal(request.prefix, request.region, request.environment) {
69        // Terminal output error: the exit status already carries the refusal.
70        let _written = std::io::stderr().write_all(refusal.as_bytes());
71        return Some(1);
72    }
73    let mut query = |query_arguments: &[&str]| -> Result<String, String> {
74        let mut arguments: Vec<OsString> = request.prefix.to_vec();
75        arguments.extend(query_arguments.iter().map(OsString::from));
76        let output = git_command(request.real_git, arguments.as_slice(), request.overlay)
77            .output()
78            .map_err(|error| {
79                return error.to_string();
80            })?;
81        let text: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_owned();
82        return match output.status.code() {
83            Some(0) => Ok(text),
84            Some(1) if text.is_empty() => Ok(text),
85            code => Err(format!("exited with {code:?}: {}", String::from_utf8_lossy(output.stderr.as_slice()).trim())),
86        };
87    };
88    let directory: std::path::PathBuf =
89        effective_directory(std::env::current_dir().unwrap_or_default().as_path(), request.prefix);
90    let read: Result<Settings, _> = match request.settings {
91        Some(held) => Ok(held.to_vec()),
92        None => read_settings(&mut query as Query<'_>),
93    };
94    let planned = read.and_then(|settings| {
95        return plan_upload(request.prefix, request.region, request.environment, directory.as_path(), settings.as_slice(), &mut query);
96    });
97    match planned {
98        Ok(Some(plan)) => {
99            // The layout is read from the files; a repository the readers decline keeps the plan, which `git-lfs` scans.
100            let layout = read_layout(request.prefix, request.overlay);
101            let narrowed = narrow_plan(plan, layout.as_ref())?;
102            return run_upload_interactive(
103                &narrowed,
104                request.prefix,
105                directory.as_path(),
106                request.real_git,
107                request.environment,
108                request.overlay,
109                &mut std::io::stderr(),
110            );
111        }
112        Ok(None) => return None,
113        Err(error) => {
114            // Terminal output error: the exit status already carries the refusal.
115            let _written = std::io::stderr().write_all(plan_failed_message(error.0.as_str()).as_bytes());
116            return Some(1);
117        }
118    }
119}