Skip to content

Commit e02261e

Browse files
committed
feat: Stupid version tests
Introduce a mechanism to gather, cache, and test the git executable version when running subordinate git commands. This used to determine whether the --3way option to `git apply` may be used in conjunction with `--cached`. Getting the git version is relatively inexpensive compared to other git commands, but does involve the overhead of running a subordinate process. This overhead is mitigated by caching the git executable version in the StupidContext. The runtime performance impact from this approach appears minimal. Fixes: #225
1 parent d846e22 commit e02261e

File tree

2 files changed

+148
-4
lines changed

2 files changed

+148
-4
lines changed

src/stupid/mod.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ mod command;
2020
pub(crate) mod diff;
2121
mod oid;
2222
pub(crate) mod status;
23+
mod version;
2324

2425
use std::{
26+
cell::RefCell,
2527
ffi::{OsStr, OsString},
2628
io::Write,
2729
path::Path,
@@ -36,6 +38,7 @@ use self::{
3638
diff::DiffFiles,
3739
oid::parse_oid,
3840
status::{StatusOptions, Statuses},
41+
version::StupidVersion,
3942
};
4043
use crate::signature::TimeExtended;
4144

@@ -50,6 +53,7 @@ impl<'repo, 'index> Stupid<'repo, 'index> for git2::Repository {
5053
git_dir: Some(self.path()),
5154
work_dir: self.workdir(),
5255
index_path: None,
56+
git_version: RefCell::new(None::<StupidVersion>),
5357
}
5458
}
5559
}
@@ -60,6 +64,7 @@ pub(crate) struct StupidContext<'repo, 'index> {
6064
pub git_dir: Option<&'repo Path>,
6165
pub index_path: Option<&'index Path>,
6266
pub work_dir: Option<&'repo Path>,
67+
git_version: RefCell<Option<StupidVersion>>,
6368
}
6469

6570
impl<'repo, 'index> StupidContext<'repo, 'index> {
@@ -85,6 +90,7 @@ impl<'repo, 'index> StupidContext<'repo, 'index> {
8590
git_dir: self.git_dir,
8691
index_path: Some(index_tempfile.path()),
8792
work_dir: self.work_dir,
93+
git_version: RefCell::new(None),
8894
};
8995

9096
f(&stupid_temp)
@@ -107,6 +113,18 @@ impl<'repo, 'index> StupidContext<'repo, 'index> {
107113
command
108114
}
109115

116+
fn at_least_version(&self, version: &StupidVersion) -> Result<bool> {
117+
let mut git_version = self.git_version.borrow_mut();
118+
if let Some(git_version) = git_version.as_ref() {
119+
Ok(git_version >= version)
120+
} else {
121+
let interrogated_version = self.version()?.parse::<StupidVersion>()?;
122+
let is_at_least = &interrogated_version >= version;
123+
git_version.replace(interrogated_version);
124+
Ok(is_at_least)
125+
}
126+
}
127+
110128
/// Apply a patch (diff) to the specified index using `git apply --cached`.
111129
pub(crate) fn apply_to_index(&self, diff: &[u8]) -> Result<()> {
112130
self.git_in_work_root()
@@ -151,7 +169,7 @@ impl<'repo, 'index> StupidContext<'repo, 'index> {
151169
&self,
152170
tree1: git2::Oid,
153171
tree2: git2::Oid,
154-
do_3way: bool,
172+
want_3way: bool,
155173
) -> Result<bool> {
156174
if tree1 == tree2 {
157175
return Ok(true);
@@ -168,7 +186,7 @@ impl<'repo, 'index> StupidContext<'repo, 'index> {
168186

169187
let mut apply_cmd = self.git_in_work_root();
170188
apply_cmd.args(["apply", "--cached"]);
171-
if do_3way {
189+
if want_3way && self.at_least_version(&StupidVersion::new(2, 32, 0))? {
172190
apply_cmd.arg("--3way");
173191
}
174192
let apply_output = apply_cmd
@@ -186,7 +204,7 @@ impl<'repo, 'index> StupidContext<'repo, 'index> {
186204
&self,
187205
tree1: git2::Oid,
188206
tree2: git2::Oid,
189-
do_3way: bool,
207+
want_3way: bool,
190208
pathspecs: SpecIter,
191209
) -> Result<bool>
192210
where
@@ -217,7 +235,7 @@ impl<'repo, 'index> StupidContext<'repo, 'index> {
217235

218236
let mut apply_cmd = self.git_in_work_root();
219237
apply_cmd.args(["apply", "--cached"]);
220-
if do_3way {
238+
if want_3way && self.at_least_version(&StupidVersion::new(2, 32, 0))? {
221239
apply_cmd.arg("--3way");
222240
}
223241
let apply_output = apply_cmd

src/stupid/version.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use std::str::FromStr;
2+
3+
use anyhow::{anyhow, Context};
4+
5+
#[derive(Clone, Debug, PartialEq, PartialOrd)]
6+
pub(crate) struct StupidVersion {
7+
major: u16,
8+
minor: u16,
9+
micro: u16,
10+
extra: Option<String>,
11+
}
12+
13+
impl StupidVersion {
14+
pub(crate) fn new(major: u16, minor: u16, micro: u16) -> StupidVersion {
15+
StupidVersion {
16+
major,
17+
minor,
18+
micro,
19+
extra: None,
20+
}
21+
}
22+
}
23+
24+
impl FromStr for StupidVersion {
25+
type Err = anyhow::Error;
26+
27+
fn from_str(s: &str) -> Result<Self, Self::Err> {
28+
let ver_str = s
29+
.strip_prefix("git version ")
30+
.ok_or_else(|| anyhow!("failed to parse git version"))?;
31+
32+
let (dotted, extra) = if let Some((dotted, extra)) = ver_str.split_once('-') {
33+
(dotted, Some(extra.to_string()))
34+
} else {
35+
(ver_str, None)
36+
};
37+
38+
let mut components = dotted.splitn(4, '.');
39+
40+
let major = components
41+
.next()
42+
.ok_or_else(|| anyhow!("no major git version"))
43+
.and_then(|major_str| u16::from_str(major_str).context("parsing git major version"))?;
44+
45+
let minor = components
46+
.next()
47+
.ok_or_else(|| anyhow!("no minor git version"))
48+
.and_then(|minor_str| u16::from_str(minor_str).context("parsing git minor version"))?;
49+
50+
let micro = components
51+
.next()
52+
.ok_or_else(|| anyhow!("no micro git version"))
53+
.and_then(|micro_str| u16::from_str(micro_str).context("parsing git micro version"))?;
54+
55+
// Ignore possible nano version component only found in pre-2.0 versions.
56+
57+
Ok(StupidVersion {
58+
major,
59+
minor,
60+
micro,
61+
extra,
62+
})
63+
}
64+
}
65+
66+
#[cfg(test)]
67+
mod tests {
68+
use super::StupidVersion;
69+
70+
#[test]
71+
fn parse_good_versions() {
72+
for (version, version_str) in [
73+
(StupidVersion::new(2, 38, 1), "2.38.1"),
74+
(StupidVersion::new(1, 8, 5), "1.8.5.6"),
75+
(
76+
StupidVersion {
77+
major: 2,
78+
minor: 17,
79+
micro: 123,
80+
extra: Some("rc0".to_string()),
81+
},
82+
"2.17.123-rc0",
83+
),
84+
] {
85+
let version_str = format!("git version {version_str}");
86+
assert_eq!(version, version_str.parse::<StupidVersion>().unwrap());
87+
}
88+
}
89+
90+
#[test]
91+
fn parse_bad_versions() {
92+
assert!("git version something".parse::<StupidVersion>().is_err());
93+
assert!("git version 2.3-rc0".parse::<StupidVersion>().is_err());
94+
}
95+
96+
#[test]
97+
fn version_comparisons() {
98+
let v2_38_1 = StupidVersion::new(2, 38, 1);
99+
let v2_38_0 = StupidVersion::new(2, 38, 0);
100+
let v2_3_15 = StupidVersion::new(2, 3, 15);
101+
let v3_0_0_rc0 = StupidVersion {
102+
major: 3,
103+
minor: 0,
104+
micro: 0,
105+
extra: Some("rc0".to_string()),
106+
};
107+
let v3_0_0_rc1 = StupidVersion {
108+
major: 3,
109+
minor: 0,
110+
micro: 0,
111+
extra: Some("rc1".to_string()),
112+
};
113+
let v3_0_1_rc0 = StupidVersion {
114+
major: 3,
115+
minor: 0,
116+
micro: 1,
117+
extra: Some("rc0".to_string()),
118+
};
119+
assert!(v2_38_1 > v2_38_0);
120+
assert_eq!(v2_38_1, v2_38_1);
121+
assert!(v2_3_15 < v2_38_0);
122+
assert!(v3_0_0_rc0 > v2_38_1);
123+
assert!(v3_0_0_rc0 < v3_0_0_rc1);
124+
assert!(v3_0_1_rc0 > v3_0_0_rc1);
125+
}
126+
}

0 commit comments

Comments
 (0)