Skip to content

Commit cf91e4c

Browse files
authored
Attempt to uniformly handle Windows-style paths. (#549)
1 parent 47d4e6a commit cf91e4c

File tree

9 files changed

+192
-35
lines changed

9 files changed

+192
-35
lines changed

src/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ impl<'cb> CheckoutBuilder<'cb> {
503503
/// If no paths are specified, then all files are checked out. Otherwise
504504
/// only these specified paths are checked out.
505505
pub fn path<T: IntoCString>(&mut self, path: T) -> &mut CheckoutBuilder<'cb> {
506-
let path = path.into_c_string().unwrap();
506+
let path = util::cstring_to_repo_path(path).unwrap();
507507
self.path_ptrs.push(path.as_ptr());
508508
self.paths.push(path);
509509
self

src/index.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ impl Index {
291291
T: IntoCString,
292292
I: IntoIterator<Item = T>,
293293
{
294-
let (_a, _b, raw_strarray) = crate::util::iter2cstrs(pathspecs)?;
294+
let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?;
295295
let ptr = cb.as_mut();
296296
let callback = ptr
297297
.as_ref()
@@ -469,7 +469,7 @@ impl Index {
469469
T: IntoCString,
470470
I: IntoIterator<Item = T>,
471471
{
472-
let (_a, _b, raw_strarray) = crate::util::iter2cstrs(pathspecs)?;
472+
let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?;
473473
let ptr = cb.as_mut();
474474
let callback = ptr
475475
.as_ref()
@@ -507,7 +507,7 @@ impl Index {
507507
T: IntoCString,
508508
I: IntoIterator<Item = T>,
509509
{
510-
let (_a, _b, raw_strarray) = crate::util::iter2cstrs(pathspecs)?;
510+
let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?;
511511
let ptr = cb.as_mut();
512512
let callback = ptr
513513
.as_ref()

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,12 @@ pub use crate::util::IntoCString;
132132

133133
// Create a convinience method on bitflag struct which checks the given flag
134134
macro_rules! is_bit_set {
135-
($name:ident, $flag:expr) => (
135+
($name:ident, $flag:expr) => {
136136
#[allow(missing_docs)]
137137
pub fn $name(&self) -> bool {
138138
self.intersects($flag)
139139
}
140-
)
140+
};
141141
}
142142

143143
/// An enumeration of possible errors that can happen when working with a git

src/oid.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ impl Oid {
9191
pub fn hash_file<P: AsRef<Path>>(kind: ObjectType, path: P) -> Result<Oid, Error> {
9292
crate::init();
9393

94+
// Normal file path OK (does not need Windows conversion).
9495
let rpath = path.as_ref().into_c_string()?;
9596

9697
let mut out = raw::git_oid {

src/pathspec.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ impl Pathspec {
4545
T: IntoCString,
4646
I: IntoIterator<Item = T>,
4747
{
48-
let (_a, _b, arr) = crate::util::iter2cstrs(specs)?;
48+
let (_a, _b, arr) = crate::util::iter2cstrs_paths(specs)?;
4949
unsafe {
5050
let mut ret = ptr::null_mut();
5151
try_call!(raw::git_pathspec_new(&mut ret, &arr));

src/repo.rs

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::diff::{
1414
use crate::oid_array::OidArray;
1515
use crate::stash::{stash_cb, StashApplyOptions, StashCbData};
1616
use crate::string_array::StringArray;
17-
use crate::util::{self, Binding};
17+
use crate::util::{self, path_to_repo_path, Binding};
1818
use crate::CherrypickOptions;
1919
use crate::{
2020
init, raw, AttrCheckFlags, Buf, Error, Object, Remote, RepositoryOpenFlags, RepositoryState,
@@ -676,7 +676,7 @@ impl Repository {
676676
T: IntoCString,
677677
I: IntoIterator<Item = T>,
678678
{
679-
let (_a, _b, mut arr) = crate::util::iter2cstrs(paths)?;
679+
let (_a, _b, mut arr) = crate::util::iter2cstrs_paths(paths)?;
680680
let target = target.map(|t| t.raw());
681681
unsafe {
682682
try_call!(raw::git_reset_default(self.raw, target, &mut arr));
@@ -881,13 +881,7 @@ impl Repository {
881881
/// through looking for the path that you are interested in.
882882
pub fn status_file(&self, path: &Path) -> Result<Status, Error> {
883883
let mut ret = 0 as c_uint;
884-
let path = if cfg!(windows) {
885-
// `git_status_file` does not work with windows path separator
886-
// so we convert \ to /
887-
std::ffi::CString::new(path.to_string_lossy().replace('\\', "/"))?
888-
} else {
889-
path.into_c_string()?
890-
};
884+
let path = path_to_repo_path(path)?;
891885
unsafe {
892886
try_call!(raw::git_status_file(&mut ret, self.raw, path));
893887
}
@@ -2603,13 +2597,7 @@ impl Repository {
26032597

26042598
/// Test if the ignore rules apply to a given path.
26052599
pub fn is_path_ignored<P: AsRef<Path>>(&self, path: P) -> Result<bool, Error> {
2606-
let path = if cfg!(windows) {
2607-
// `git_ignore_path_is_ignored` dose not work with windows path separator
2608-
// so we convert \ to /
2609-
std::ffi::CString::new(path.as_ref().to_string_lossy().replace('\\', "/"))?
2610-
} else {
2611-
path.as_ref().into_c_string()?
2612-
};
2600+
let path = path_to_repo_path(path.as_ref())?;
26132601
let mut ignored: c_int = 0;
26142602
unsafe {
26152603
try_call!(raw::git_ignore_path_is_ignored(
@@ -3358,18 +3346,18 @@ mod tests {
33583346
fn smoke_is_path_ignored() {
33593347
let (_td, repo) = graph_repo_init();
33603348

3361-
assert!(!repo.is_path_ignored(Path::new("/foo")).unwrap());
3349+
assert!(!repo.is_path_ignored(Path::new("foo")).unwrap());
33623350

33633351
let _ = repo.add_ignore_rule("/foo");
3364-
assert!(repo.is_path_ignored(Path::new("/foo")).unwrap());
3352+
assert!(repo.is_path_ignored(Path::new("foo")).unwrap());
33653353
if cfg!(windows) {
3366-
assert!(repo.is_path_ignored(Path::new("\\foo\\thing")).unwrap());
3354+
assert!(repo.is_path_ignored(Path::new("foo\\thing")).unwrap());
33673355
}
33683356

33693357
let _ = repo.clear_ignore_rules();
3370-
assert!(!repo.is_path_ignored(Path::new("/foo")).unwrap());
3358+
assert!(!repo.is_path_ignored(Path::new("foo")).unwrap());
33713359
if cfg!(windows) {
3372-
assert!(!repo.is_path_ignored(Path::new("\\foo\\thing")).unwrap());
3360+
assert!(!repo.is_path_ignored(Path::new("foo\\thing")).unwrap());
33733361
}
33743362
}
33753363

src/status.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::mem;
55
use std::ops::Range;
66
use std::str;
77

8-
use crate::util::Binding;
8+
use crate::util::{self, Binding};
99
use crate::{raw, DiffDelta, IntoCString, Repository, Status};
1010

1111
/// Options that can be provided to `repo.statuses()` to control how the status
@@ -98,7 +98,7 @@ impl StatusOptions {
9898
/// path to match. If this is not called, then there will be no patterns to
9999
/// match and the entire directory will be used.
100100
pub fn pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut StatusOptions {
101-
let s = pathspec.into_c_string().unwrap();
101+
let s = util::cstring_to_repo_path(pathspec).unwrap();
102102
self.ptrs.push(s.as_ptr());
103103
self.pathspec.push(s);
104104
self

src/tree.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::path::Path;
88
use std::ptr;
99
use std::str;
1010

11-
use crate::util::{c_cmp_to_ordering, Binding, IntoCString};
11+
use crate::util::{c_cmp_to_ordering, path_to_repo_path, Binding};
1212
use crate::{panic, raw, Error, Object, ObjectType, Oid, Repository};
1313

1414
/// A structure to represent a git [tree][1]
@@ -179,7 +179,7 @@ impl<'repo> Tree<'repo> {
179179
/// Retrieve a tree entry contained in a tree or in any of its subtrees,
180180
/// given its relative path.
181181
pub fn get_path(&self, path: &Path) -> Result<TreeEntry<'static>, Error> {
182-
let path = path.into_c_string()?;
182+
let path = path_to_repo_path(path)?;
183183
let mut ret = ptr::null_mut();
184184
unsafe {
185185
try_call!(raw::git_tree_entry_bypath(&mut ret, &*self.raw(), path));

src/util.rs

Lines changed: 171 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use libc::{c_char, c_int, size_t};
22
use std::cmp::Ordering;
33
use std::ffi::{CString, OsStr, OsString};
44
use std::iter::IntoIterator;
5-
use std::path::{Path, PathBuf};
5+
use std::path::{Component, Path, PathBuf};
66

77
use crate::{raw, Error};
88

@@ -41,6 +41,29 @@ pub trait Binding: Sized {
4141
}
4242
}
4343

44+
/// Converts an iterator of repo paths into a git2-compatible array of cstrings.
45+
///
46+
/// Only use this for repo-relative paths or pathspecs.
47+
///
48+
/// See `iter2cstrs` for more details.
49+
pub fn iter2cstrs_paths<T, I>(
50+
iter: I,
51+
) -> Result<(Vec<CString>, Vec<*const c_char>, raw::git_strarray), Error>
52+
where
53+
T: IntoCString,
54+
I: IntoIterator<Item = T>,
55+
{
56+
let cstrs = iter
57+
.into_iter()
58+
.map(|i| fixup_windows_path(i.into_c_string()?))
59+
.collect::<Result<Vec<CString>, _>>()?;
60+
iter2cstrs(cstrs)
61+
}
62+
63+
/// Converts an iterator of things into a git array of c-strings.
64+
///
65+
/// Returns a tuple `(cstrings, pointers, git_strarray)`. The first two values
66+
/// should not be dropped before `git_strarray`.
4467
pub fn iter2cstrs<T, I>(
4568
iter: I,
4669
) -> Result<(Vec<CString>, Vec<*const c_char>, raw::git_strarray), Error>
@@ -136,8 +159,7 @@ impl IntoCString for OsString {
136159
match self.to_str() {
137160
Some(s) => s.into_c_string(),
138161
None => Err(Error::from_str(
139-
"only valid unicode paths are accepted \
140-
on windows",
162+
"only valid unicode paths are accepted on windows",
141163
)),
142164
}
143165
}
@@ -172,3 +194,149 @@ pub fn c_cmp_to_ordering(cmp: c_int) -> Ordering {
172194
_ => Ordering::Greater,
173195
}
174196
}
197+
198+
/// Converts a path to a CString that is usable by the libgit2 API.
199+
///
200+
/// Checks if it is a relative path.
201+
///
202+
/// On Windows, this also requires the path to be valid unicode, and translates
203+
/// back slashes to forward slashes.
204+
pub fn path_to_repo_path(path: &Path) -> Result<CString, Error> {
205+
macro_rules! err {
206+
($msg:literal, $path:expr) => {
207+
return Err(Error::from_str(&format!($msg, $path.display())));
208+
};
209+
}
210+
match path.components().next() {
211+
None => return Err(Error::from_str("repo path should not be empty")),
212+
Some(Component::Prefix(_)) => err!(
213+
"repo path `{}` should be relative, not a windows prefix",
214+
path
215+
),
216+
Some(Component::RootDir) => err!("repo path `{}` should be relative", path),
217+
Some(Component::CurDir) => err!("repo path `{}` should not start with `.`", path),
218+
Some(Component::ParentDir) => err!("repo path `{}` should not start with `..`", path),
219+
Some(Component::Normal(_)) => {}
220+
}
221+
#[cfg(windows)]
222+
{
223+
match path.to_str() {
224+
None => {
225+
return Err(Error::from_str(
226+
"only valid unicode paths are accepted on windows",
227+
))
228+
}
229+
Some(s) => return fixup_windows_path(s),
230+
}
231+
}
232+
#[cfg(not(windows))]
233+
{
234+
path.into_c_string()
235+
}
236+
}
237+
238+
pub fn cstring_to_repo_path<T: IntoCString>(path: T) -> Result<CString, Error> {
239+
fixup_windows_path(path.into_c_string()?)
240+
}
241+
242+
#[cfg(windows)]
243+
fn fixup_windows_path<P: Into<Vec<u8>>>(path: P) -> Result<CString, Error> {
244+
let mut bytes: Vec<u8> = path.into();
245+
for i in 0..bytes.len() {
246+
if bytes[i] == b'\\' {
247+
bytes[i] = b'/';
248+
}
249+
}
250+
Ok(CString::new(bytes)?)
251+
}
252+
253+
#[cfg(not(windows))]
254+
fn fixup_windows_path(path: CString) -> Result<CString, Error> {
255+
Ok(path)
256+
}
257+
258+
#[cfg(test)]
259+
mod tests {
260+
use super::*;
261+
262+
macro_rules! assert_err {
263+
($path:expr, $msg:expr) => {
264+
match path_to_repo_path(Path::new($path)) {
265+
Ok(_) => panic!("expected `{}` to err", $path),
266+
Err(e) => assert_eq!(e.message(), $msg),
267+
}
268+
};
269+
}
270+
271+
macro_rules! assert_repo_path_ok {
272+
($path:expr) => {
273+
assert_repo_path_ok!($path, $path)
274+
};
275+
($path:expr, $expect:expr) => {
276+
assert_eq!(
277+
path_to_repo_path(Path::new($path)),
278+
Ok(CString::new($expect).unwrap())
279+
);
280+
};
281+
}
282+
283+
#[test]
284+
#[cfg(windows)]
285+
fn path_to_repo_path_translate() {
286+
assert_repo_path_ok!("foo");
287+
assert_repo_path_ok!("foo/bar");
288+
assert_repo_path_ok!(r"foo\bar", "foo/bar");
289+
assert_repo_path_ok!(r"foo\bar\", "foo/bar/");
290+
}
291+
292+
#[test]
293+
fn path_to_repo_path_no_weird() {
294+
assert_err!("", "repo path should not be empty");
295+
assert_err!("./foo", "repo path `./foo` should not start with `.`");
296+
assert_err!("../foo", "repo path `../foo` should not start with `..`");
297+
}
298+
299+
#[test]
300+
#[cfg(not(windows))]
301+
fn path_to_repo_path_no_absolute() {
302+
assert_err!("/", "repo path `/` should be relative");
303+
assert_repo_path_ok!("foo/bar");
304+
}
305+
306+
#[test]
307+
#[cfg(windows)]
308+
fn path_to_repo_path_no_absolute() {
309+
assert_err!(
310+
r"c:",
311+
r"repo path `c:` should be relative, not a windows prefix"
312+
);
313+
assert_err!(
314+
r"c:\",
315+
r"repo path `c:\` should be relative, not a windows prefix"
316+
);
317+
assert_err!(
318+
r"c:temp",
319+
r"repo path `c:temp` should be relative, not a windows prefix"
320+
);
321+
assert_err!(
322+
r"\\?\UNC\a\b\c",
323+
r"repo path `\\?\UNC\a\b\c` should be relative, not a windows prefix"
324+
);
325+
assert_err!(
326+
r"\\?\c:\foo",
327+
r"repo path `\\?\c:\foo` should be relative, not a windows prefix"
328+
);
329+
assert_err!(
330+
r"\\.\COM42",
331+
r"repo path `\\.\COM42` should be relative, not a windows prefix"
332+
);
333+
assert_err!(
334+
r"\\a\b",
335+
r"repo path `\\a\b` should be relative, not a windows prefix"
336+
);
337+
assert_err!(r"\", r"repo path `\` should be relative");
338+
assert_err!(r"/", r"repo path `/` should be relative");
339+
assert_err!(r"\foo", r"repo path `\foo` should be relative");
340+
assert_err!(r"/foo", r"repo path `/foo` should be relative");
341+
}
342+
}

0 commit comments

Comments
 (0)