git_checks/
fast_forward.rs1use derive_builder::Builder;
10use git_checks_core::impl_prelude::*;
11use thiserror::Error;
12
13#[derive(Debug, Error)]
14enum FastForwardError {
15 #[error(
16 "failed to get the merge-base for {} against a target branch {} ({:?}): {}",
17 commit,
18 base,
19 code,
20 output
21 )]
22 MergeBase {
23 commit: CommitId,
24 base: CommitId,
25 code: Option<i32>,
26 output: String,
27 },
28}
29
30impl FastForwardError {
31 fn merge_base(commit: CommitId, base: CommitId, code: Option<i32>, output: &[u8]) -> Self {
32 Self::MergeBase {
33 commit,
34 base,
35 code,
36 output: String::from_utf8_lossy(output).into(),
37 }
38 }
39}
40
41#[derive(Builder, Debug, Clone)]
48#[builder(field(private))]
49pub struct FastForward {
50 #[builder(setter(into))]
54 branch: CommitId,
55 #[builder(default = "false")]
60 required: bool,
61}
62
63impl FastForward {
64 pub fn builder() -> FastForwardBuilder {
66 Default::default()
67 }
68}
69
70impl BranchCheck for FastForward {
71 fn name(&self) -> &str {
72 "fast-forward"
73 }
74
75 fn check(
76 &self,
77 ctx: &CheckGitContext,
78 commit: &CommitId,
79 ) -> Result<CheckResult, Box<dyn Error>> {
80 let merge_base = ctx
81 .git()
82 .arg("merge-base")
83 .arg("--is-ancestor")
84 .arg(self.branch.as_str())
85 .arg(commit.as_str())
86 .output()
87 .map_err(|err| GitError::subcommand("merge-base", err))?;
88 let ok = match merge_base.status.code() {
89 Some(0) => true,
90 Some(1) => false,
91 code => {
92 return Err(FastForwardError::merge_base(
93 commit.clone(),
94 self.branch.clone(),
95 code,
96 &merge_base.stderr,
97 )
98 .into());
99 },
100 };
101
102 let mut result = CheckResult::new();
103
104 if !ok {
106 if self.required {
107 result.add_error(format!(
108 "This branch is ineligible for the fast-forward merging into the `{}` branch; \
109 it needs to be rebased.",
110 self.branch,
111 ));
112 } else {
113 result.add_warning(format!(
114 "Not eligible for fast-forward merging into `{}`.",
115 self.branch,
116 ));
117 }
118 }
119
120 Ok(result)
121 }
122}
123
124#[cfg(feature = "config")]
125pub(crate) mod config {
126 use git_checks_config::{register_checks, BranchCheckConfig, IntoCheck};
127 use git_workarea::CommitId;
128 use serde::Deserialize;
129 #[cfg(test)]
130 use serde_json::json;
131
132 #[cfg(test)]
133 use crate::test;
134 use crate::FastForward;
135
136 #[derive(Deserialize, Debug)]
153 pub struct FastForwardConfig {
154 branch: String,
155 #[serde(default)]
156 required: Option<bool>,
157 }
158
159 impl IntoCheck for FastForwardConfig {
160 type Check = FastForward;
161
162 fn into_check(self) -> Self::Check {
163 let mut builder = FastForward::builder();
164
165 builder.branch(CommitId::new(self.branch));
166
167 if let Some(required) = self.required {
168 builder.required(required);
169 }
170
171 builder
172 .build()
173 .expect("configuration mismatch for `FastForward`")
174 }
175 }
176
177 register_checks! {
178 FastForwardConfig {
179 "fast_forward" => BranchCheckConfig,
180 },
181 }
182
183 #[test]
184 fn test_fast_forward_config_empty() {
185 let json = json!({});
186 let err = serde_json::from_value::<FastForwardConfig>(json).unwrap_err();
187 test::check_missing_json_field(err, "branch");
188 }
189
190 #[test]
191 fn test_fast_forward_config_branch_is_required() {
192 let json = json!({});
193 let err = serde_json::from_value::<FastForwardConfig>(json).unwrap_err();
194 test::check_missing_json_field(err, "branch");
195 }
196
197 #[test]
198 fn test_fast_forward_config_minimum_fields() {
199 let exp_branch = CommitId::new("v1.x");
200 let json = json!({
201 "branch": exp_branch.as_str(),
202 });
203 let check: FastForwardConfig = serde_json::from_value(json).unwrap();
204
205 assert_eq!(check.branch, exp_branch.as_str());
206 assert_eq!(check.required, None);
207
208 let check = check.into_check();
209
210 assert_eq!(check.branch, exp_branch);
211 assert!(!check.required);
212 }
213
214 #[test]
215 fn test_fast_forward_config_all_fields() {
216 let exp_branch = CommitId::new("v1.x");
217 let json = json!({
218 "branch": exp_branch.as_str(),
219 "required": true,
220 });
221 let check: FastForwardConfig = serde_json::from_value(json).unwrap();
222
223 assert_eq!(check.branch, exp_branch.as_str());
224 assert_eq!(check.required, Some(true));
225
226 let check = check.into_check();
227
228 assert_eq!(check.branch, exp_branch);
229 assert!(check.required);
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use git_checks_core::BranchCheck;
236 use git_workarea::CommitId;
237
238 use crate::builders::FastForwardBuilder;
239 use crate::test::*;
240 use crate::FastForward;
241
242 const RELEASE_BRANCH: &str = "3a22ca19fda09183da2faab60819ff6807568acd";
243 const NON_FF_TOPIC: &str = "a61fd3759b61a4a1f740f3fe656bc42151cefbdd";
244 const FF_TOPIC: &str = "969b794ea82ce51d1555852de33bfcb63dfec969";
245
246 #[test]
247 fn test_fast_forward_builder_default() {
248 assert!(FastForward::builder().build().is_err());
249 }
250
251 #[test]
252 fn test_fast_forward_builder_branch_is_required() {
253 assert!(FastForward::builder().build().is_err());
254 }
255
256 #[test]
257 fn test_fast_forward_builder_minimum_fields() {
258 assert!(FastForward::builder()
259 .branch(CommitId::new("release"))
260 .build()
261 .is_ok());
262 }
263
264 #[test]
265 fn test_fast_forward_name_branch() {
266 let check = FastForward::builder()
267 .branch(CommitId::new("release"))
268 .build()
269 .unwrap();
270 assert_eq!(BranchCheck::name(&check), "fast-forward");
271 }
272
273 fn make_fast_forward_check() -> FastForwardBuilder {
274 let mut builder = FastForward::builder();
275 builder.branch(CommitId::new(RELEASE_BRANCH));
276 builder
277 }
278
279 #[test]
280 fn test_fast_forward_ok() {
281 let check = make_fast_forward_check().build().unwrap();
282 run_branch_check_ok("test_fast_forward_ok", FF_TOPIC, check);
283 }
284
285 #[test]
286 fn test_fast_forward_ok_required() {
287 let check = make_fast_forward_check().required(true).build().unwrap();
288 run_branch_check_ok("test_fast_forward_ok_required", FF_TOPIC, check);
289 }
290
291 #[test]
292 fn test_fast_forward_bad() {
293 let check = make_fast_forward_check().build().unwrap();
294 let result = run_branch_check("test_fast_forward_bad", NON_FF_TOPIC, check);
295 test_result_warnings(
296 result,
297 &["Not eligible for fast-forward merging into \
298 `3a22ca19fda09183da2faab60819ff6807568acd`."],
299 );
300 }
301
302 #[test]
303 fn test_fast_forward_bad_required() {
304 let check = make_fast_forward_check().required(true).build().unwrap();
305 let result = run_branch_check("test_fast_forward_bad_required", NON_FF_TOPIC, check);
306 test_result_errors(
307 result,
308 &[
309 "This branch is ineligible for the fast-forward merging into the \
310 `3a22ca19fda09183da2faab60819ff6807568acd` branch; it needs to be rebased.",
311 ],
312 );
313 }
314}