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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! 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;
/// Narrows the plan to the objects the push introduces.
use super::lfs_object_plan::narrow_plan;
/// The upload itself.
use super::lfs_upload::{run_upload_interactive, submodule_notice};
/// The repository found from the files.
use super::git_system_config::with_real_git;
use super::repo_read_layout::read_layout_after_success_for;
/// 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)) => {
// The layout is read from the files; a repository the readers decline keeps the plan, which `git-lfs` scans.
// The plan came from Git queries that exited with status 0 (the settings may be held from an earlier query),
// so Git had its say about the values it consults;
// the files are read again,
// and a plan that cannot be narrowed is still pushed by `git-lfs` and Git.
let layout = read_layout_after_success_for(request.prefix, with_real_git(request.overlay, request.real_git).as_slice());
let submodules: bool = plan.submodules;
let Some(narrowed) = narrow_plan(plan, layout.as_ref()) else {
// Nothing of this repository to upload, but Git still pushes the submodules, whose objects are not covered.
if submodules {
// Terminal output error: the notice is advisory.
let _written = std::io::stderr().write_all(submodule_notice().as_bytes());
}
return None;
};
return run_upload_interactive(
&narrowed,
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);
}
}
}