Skip to content

Commit 32516af

Browse files
committed
Avoid case-insensitive patch name collisions
On platforms using case-insensitive filesystems, having two patch names in the same stack that only differ by case will cause patch reference collisions. E.g. `p0` and `P0` would use the same reference file: `.git/refs/patches/<branch>/p0`. To avoid this hazard, StGit is modified to disallow case-insensitive collisions of patch names within a stack. When new patch names are generated with PatchName::uniquify(), a case-insensitive comparision is performed to ensure the uniqified name differs by more than just case from all other patch names in the stack. And the new Stack::collides() method is used instead of Stack::has_patch() to test whether user-provided patch names collide with any existing patch names.
1 parent 9374817 commit 32516af

File tree

9 files changed

+54
-23
lines changed

9 files changed

+54
-23
lines changed

src/cmd/new.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,14 @@ fn run(matches: &ArgMatches) -> Result<()> {
131131

132132
let patchname = if let Some(name) = matches.value_of("patchname") {
133133
let patchname = name.parse::<PatchName>()?;
134-
if stack.has_patch(&patchname) {
135-
return Err(anyhow!("Patch `{patchname}` already exists"));
134+
if let Some(colliding_patchname) = stack.collides(&patchname) {
135+
Err(anyhow!("Patch `{colliding_patchname}` already exists"))
136136
} else {
137-
Some(patchname)
137+
Ok(Some(patchname))
138138
}
139139
} else {
140-
None
141-
};
140+
Ok(None)
141+
}?;
142142

143143
let config = repo.config()?;
144144

src/cmd/pick.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ fn pick_picks(
270270
let mut new_patches: Vec<(PatchName, git2::Oid)> = Vec::with_capacity(picks.len());
271271

272272
for (patchname, commit) in picks {
273-
let disallow: Vec<&PatchName> = stack.all_patches().collect();
273+
let mut disallow: Vec<&PatchName> = stack.all_patches().collect();
274274

275275
let patchname = if let Some(name) = matches.value_of("name") {
276276
PatchName::from_str(name)?
@@ -334,6 +334,7 @@ fn pick_picks(
334334
.repo
335335
.commit_ex(&author, &committer, message, top.tree_id(), [bottom.id()])?;
336336
new_patches.push((patchname, new_commit_id));
337+
disallow.push(&new_patches[new_patches.len() - 1].0);
337338
}
338339

339340
stack

src/cmd/rename.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ fn run(matches: &ArgMatches) -> Result<()> {
6060
return Err(Error::NoAppliedPatches.into());
6161
};
6262

63-
if old_patchname == new_patchname {
64-
return Err(anyhow!("Patch `{new_patchname}` already exists"));
63+
if old_patchname.collides(&new_patchname) {
64+
return Err(anyhow!("Patch `{old_patchname}` already exists"));
6565
}
6666

6767
stack

src/cmd/squash.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,10 @@ fn run(matches: &ArgMatches) -> Result<()> {
9191
.map(|s| PatchName::from_str(s).expect("clap already validated name"));
9292

9393
if let Some(patchname) = patchname.as_ref() {
94-
if !squash_patchnames.contains(patchname) && stack.has_patch(patchname) {
95-
return Err(anyhow!("Patch name `{patchname}` already taken"));
94+
if !squash_patchnames.contains(patchname) {
95+
if let Some(colliding_patchname) = stack.collides(patchname) {
96+
return Err(anyhow!("Patch name `{colliding_patchname}` already taken"));
97+
}
9698
}
9799
}
98100

src/cmd/uncommit.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! `stg uncommit` implementation.
22
3-
use std::{collections::HashSet, str::FromStr};
3+
use std::str::FromStr;
44

55
use anyhow::{anyhow, Result};
66
use clap::{Arg, ArgMatches};
@@ -239,13 +239,20 @@ fn make_patchnames(
239239
}
240240

241241
fn check_patchnames(stack: &Stack, patchnames: &[PatchName]) -> Result<()> {
242-
let mut taken_names: HashSet<_> = stack.all_patches().cloned().collect();
242+
let mut taken_names: Vec<&PatchName> = Vec::new();
243243
for patchname in patchnames {
244-
if taken_names.contains(patchname) {
245-
return Err(anyhow!("Patch `{patchname}` already exists"));
244+
if let Some(colliding_patchname) = stack.collides(patchname) {
245+
return Err(anyhow!("Patch `{colliding_patchname}` already exists"));
246+
} else if let Some(colliding_patchname) =
247+
taken_names.iter().find(|pn| patchname.collides(pn))
248+
{
249+
return Err(anyhow!(
250+
"Patch name `{patchname}` collides with `{colliding_patchname}`"
251+
));
246252
} else {
247-
taken_names.insert(patchname.clone());
253+
taken_names.push(patchname);
248254
}
249255
}
256+
250257
Ok(())
251258
}

src/patchname.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ impl PatchName {
173173
let mut candidate = self;
174174
loop {
175175
if allow.iter().any(|pn| pn.as_ref() == &candidate)
176-
|| disallow.iter().all(|pn| pn.as_ref() != &candidate)
176+
|| disallow.iter().all(|pn| !candidate.collides(pn.as_ref()))
177177
{
178178
break candidate;
179179
} else {
@@ -191,6 +191,15 @@ impl PatchName {
191191
}
192192
}
193193

194+
/// Test if another patch name is the same as self, ignoring case.
195+
///
196+
/// Having two patch names in the same stack that only differ by case will lead to
197+
/// ref collisions in case-insensitive filesystems. E.g. `refs/patches/<branch>/p0`
198+
/// would collide with `refs/patches/<branch>/P0`.
199+
pub(crate) fn collides(&self, other: &PatchName) -> bool {
200+
self.0.eq_ignore_ascii_case(&other.0)
201+
}
202+
194203
/// Validate a patch name `str`.
195204
fn validate(name: &str) -> Result<(), Error> {
196205
if name.is_empty() {

src/stack/state.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,19 @@ pub(crate) trait StackStateAccess<'repo> {
7474
fn get_patch(&self, patchname: &PatchName) -> &PatchState<'repo>;
7575

7676
/// Test whether given patch name exists in the stack.
77+
///
78+
/// N.B. use [`StackStateAccess::collides()`] to test for potential patch name
79+
/// collisions.
7780
fn has_patch(&self, patchname: &PatchName) -> bool;
7881

82+
/// Test whether given patch name collides with an existing patch name.
83+
///
84+
/// A patch name collides if it is different from only by case from a patch in the
85+
/// stack.
86+
fn collides(&self, patchname: &PatchName) -> Option<&PatchName> {
87+
self.all_patches().find(|pn| patchname.collides(pn))
88+
}
89+
7990
/// Get stack's top commit, or base if no applied patches.
8091
fn top(&self) -> &Commit<'repo>;
8192

src/stack/transaction.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -877,13 +877,14 @@ impl<'repo> StackTransaction<'repo> {
877877
) -> Result<()> {
878878
if new_patchname == old_patchname {
879879
return Ok(());
880-
} else if self.stack.has_patch(new_patchname)
881-
&& self
880+
} else if let Some(colliding_patchname) = self.stack.collides(new_patchname) {
881+
if self
882882
.patch_updates
883-
.get(new_patchname)
883+
.get(colliding_patchname)
884884
.map_or(true, |maybe_patch| maybe_patch.is_some())
885-
{
886-
return Err(anyhow!("Patch `{new_patchname}` already exists"));
885+
{
886+
return Err(anyhow!("Patch `{colliding_patchname}` already exists"));
887+
}
887888
} else if !self.stack.has_patch(old_patchname) {
888889
return Err(anyhow!("Patch `{old_patchname}` does not exist"));
889890
}

t/t3400-pick.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,9 +276,9 @@ test_expect_success \
276276
'Pick too many commits' \
277277
'
278278
stg pick --ref-branch foo $(cat C-id) D-foo &&
279-
test "$(echo $(stg series --applied --noprefix))" = "C2 c d" &&
279+
test "$(echo $(stg series --applied --noprefix))" = "C2 c-1 d-1" &&
280280
test "$(echo $(stg series --unapplied --noprefix))" = "A B C D" &&
281-
stg delete c d
281+
stg delete c-1 d-1
282282
'
283283
fi
284284

0 commit comments

Comments
 (0)