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