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
use std::io::IsTerminal;
use std::io::Read;
use crate::hook_options::HookOptions;
use crate::{
Result,
git::{Git, is_zero_sha},
};
#[derive(clap::Args)]
#[clap(visible_alias = "pp")]
pub struct PrePush {
/// Remote name
remote: Option<String>,
/// Remote URL
url: Option<String>,
#[clap(flatten)]
hook: HookOptions,
}
#[derive(Debug)]
struct PrePushRefs {
to: (String, String),
from: (String, String),
}
impl From<&str> for PrePushRefs {
fn from(line: &str) -> Self {
let parts: Vec<&str> = line.split_whitespace().collect();
PrePushRefs {
to: (parts[0].to_string(), parts[1].to_string()),
from: (parts[2].to_string(), parts[3].to_string()),
}
}
}
impl PrePush {
pub async fn run(mut self) -> Result<()> {
self.hook.tctx.insert(
"hook_args",
&format!(
"{} {}",
self.remote.as_deref().unwrap_or(""),
self.url.as_deref().unwrap_or("")
),
);
let to_be_updated_refs = if std::io::stdin().is_terminal() {
self.hook.tctx.insert("hook_stdin", "");
vec![]
} else {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input)?;
self.hook.tctx.insert("hook_stdin", &input);
// Note: we deliberately keep deletions (local sha all-zeros) in
// the list. The downstream EMPTY_REF guard in hook.rs detects
// `to_ref` of all-zeros and short-circuits to an empty file set
// — dropping deletions here would route them through
// `files_between_refs(default_branch, "HEAD")` and lint
// unrelated files.
input
.lines()
.filter(|line| !line.is_empty())
.map(PrePushRefs::from)
.collect::<Vec<_>>()
};
trace!("to_be_updated_refs: {to_be_updated_refs:?}");
self.hook.from_ref = Some(match &self.hook.from_ref {
Some(to_ref) => to_ref.clone(),
None if !to_be_updated_refs.is_empty()
&& !is_zero_sha(&to_be_updated_refs[0].from.1) =>
{
to_be_updated_refs[0].from.1.clone()
}
None => {
// Either no refs were provided on stdin, or the first ref is
// a new-branch push (remote sha is all-zeros). Fall back to
// the remote-tracking branch if it exists, then to the
// repository's default branch on the target remote.
let remote = self.remote.as_deref().unwrap_or("origin");
let repo = Git::new()?; // TODO: remove this extra repo creation
if let Some(rb) = repo.matching_remote_branch(remote)? {
rb
} else if remote == "origin" {
repo.resolve_default_branch()
} else {
// resolve_default_branch is internally hardcoded to
// origin, so for a non-origin remote strip the known
// origin prefix and rebind onto the actual remote.
// Use strip_prefix (not rsplit) so multi-segment branch
// names like "release/v1" are preserved intact.
let default = repo.resolve_default_branch();
let bare = default
.strip_prefix("refs/remotes/origin/")
.or_else(|| default.strip_prefix("origin/"))
.unwrap_or(&default);
format!("refs/remotes/{remote}/{bare}")
}
}
});
self.hook.to_ref = Some(
self.hook
.to_ref
.clone()
.or(if !to_be_updated_refs.is_empty() {
Some(to_be_updated_refs[0].to.1.clone())
} else {
None
})
.unwrap_or("HEAD".to_string()),
);
debug!(
"from_ref: {}, to_ref: {}",
self.hook.from_ref.as_ref().unwrap(),
self.hook.to_ref.as_ref().unwrap()
);
self.hook.run("pre-push").await
}
}