Skip to content

Commit 41cd53e

Browse files
committed
Merge branch 'improvements-for-cargo'
2 parents 4ccf39b + ab21083 commit 41cd53e

File tree

31 files changed

+821
-151
lines changed

31 files changed

+821
-151
lines changed

gitoxide-core/src/repository/clean.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ pub(crate) mod function {
8686
} else {
8787
Vec::new()
8888
},
89+
&gix::interrupt::IS_INTERRUPTED,
8990
options,
9091
&mut collect,
9192
)?;

gix-actor/src/signature/decode.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ pub(crate) mod function {
3636
take_while(1..=2, AsChar::is_dec_digit)
3737
.verify_map(|v| to_signed::<OffsetInSeconds>(v).ok())
3838
.context(StrContext::Expected("MM".into())),
39+
take_while(0.., AsChar::is_dec_digit).map(|v: &[u8]| v),
3940
)
40-
.map(|(time, sign, hours, minutes)| {
41-
let offset = (hours * 3600 + minutes * 60) * if sign == Sign::Minus { -1 } else { 1 };
41+
.map(|(time, sign, hours, minutes, trailing_digits)| {
42+
let offset = if trailing_digits.is_empty() {
43+
(hours * 3600 + minutes * 60) * if sign == Sign::Minus { -1 } else { 1 }
44+
} else {
45+
0
46+
};
4247
Time {
4348
seconds: time,
4449
offset,

gix-actor/tests/signature/mod.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ mod write_to {
5353
}
5454

5555
use bstr::ByteSlice;
56-
use gix_actor::Signature;
56+
use gix_actor::{Signature, SignatureRef};
5757

5858
#[test]
5959
fn trim() {
@@ -80,3 +80,28 @@ fn round_trip() -> Result<(), Box<dyn std::error::Error>> {
8080
}
8181
Ok(())
8282
}
83+
84+
#[test]
85+
fn parse_timestamp_with_trailing_digits() {
86+
let signature = gix_actor::SignatureRef::from_bytes::<()>(b"first last <name@example.com> 1312735823 +051800")
87+
.expect("deal with trailing zeroes in timestamp by discarding it");
88+
assert_eq!(
89+
signature,
90+
SignatureRef {
91+
name: "first last".into(),
92+
email: "name@example.com".into(),
93+
time: gix_actor::date::Time::new(1312735823, 0),
94+
}
95+
);
96+
97+
let signature = gix_actor::SignatureRef::from_bytes::<()>(b"first last <name@example.com> 1312735823 +0518")
98+
.expect("this naturally works as the timestamp does not have trailing zeroes");
99+
assert_eq!(
100+
signature,
101+
SignatureRef {
102+
name: "first last".into(),
103+
email: "name@example.com".into(),
104+
time: gix_actor::date::Time::new(1312735823, 19080),
105+
}
106+
);
107+
}

gix-dir/src/walk/classify.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ pub fn path(
134134
emit_ignored,
135135
for_deletion,
136136
classify_untracked_bare_repositories,
137+
symlinks_to_directories_are_ignored_like_directories,
137138
..
138139
}: Options,
139140
ctx: &mut Context<'_>,
@@ -199,6 +200,15 @@ pub fn path(
199200
.pattern_matching_relative_path(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.pathspec_attributes)
200201
.map(Into::into);
201202

203+
let is_dir = if symlinks_to_directories_are_ignored_like_directories
204+
&& ctx.excludes.is_some()
205+
&& kind.map_or(false, |ft| ft == entry::Kind::Symlink)
206+
{
207+
path.metadata().ok().map(|md| md.is_dir()).or(Some(false))
208+
} else {
209+
kind.map(|ft| ft.is_dir())
210+
};
211+
202212
let mut maybe_upgrade_to_repository = |current_kind, find_harder: bool| {
203213
if recurse_repositories {
204214
return current_kind;
@@ -245,7 +255,7 @@ pub fn path(
245255
.as_mut()
246256
.map_or(Ok(None), |stack| {
247257
stack
248-
.at_entry(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.objects)
258+
.at_entry(rela_path.as_bstr(), is_dir, ctx.objects)
249259
.map(|platform| platform.excluded_kind())
250260
})
251261
.map_err(Error::ExcludesAccess)?

gix-dir/src/walk/function.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,16 @@ pub fn walk(
6969
)?;
7070
if !can_recurse(
7171
buf.as_bstr(),
72-
root_info,
72+
if root == worktree_root && root_info.disk_kind == Some(entry::Kind::Symlink) && current.is_dir() {
73+
classify::Outcome {
74+
disk_kind: Some(entry::Kind::Directory),
75+
..root_info
76+
}
77+
} else {
78+
root_info
79+
},
7380
options.for_deletion,
74-
worktree_root_is_repository, /* is root */
81+
worktree_root_is_repository,
7582
delegate,
7683
) {
7784
if buf.is_empty() && !root_info.disk_kind.map_or(false, |kind| kind.is_dir()) {
@@ -147,16 +154,17 @@ pub(super) fn can_recurse(
147154
rela_path: &BStr,
148155
info: classify::Outcome,
149156
for_deletion: Option<ForDeletionMode>,
150-
is_root: bool,
157+
worktree_root_is_repository: bool,
151158
delegate: &mut dyn Delegate,
152159
) -> bool {
153-
if info.disk_kind.map_or(true, |k| !k.is_dir()) {
160+
let is_dir = info.disk_kind.map_or(false, |k| k.is_dir());
161+
if !is_dir {
154162
return false;
155163
}
156164
delegate.can_recurse(
157165
EntryRef::from_outcome(Cow::Borrowed(rela_path), info),
158166
for_deletion,
159-
is_root,
167+
worktree_root_is_repository,
160168
)
161169
}
162170

gix-dir/src/walk/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::{entry, EntryRef};
22
use bstr::BStr;
33
use std::path::PathBuf;
4+
use std::sync::atomic::AtomicBool;
45

56
/// A type returned by the [`Delegate::emit()`] as passed to [`walk()`](function::walk()).
67
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
@@ -183,10 +184,22 @@ pub struct Options {
183184
pub emit_empty_directories: bool,
184185
/// If `None`, no entries inside of collapsed directories are emitted. Otherwise, act as specified by `Some(mode)`.
185186
pub emit_collapsed: Option<CollapsedEntriesEmissionMode>,
187+
/// This is a `libgit2` compatibility flag, and if enabled, symlinks that point to directories will be considered a directory
188+
/// when checking for exclusion.
189+
///
190+
/// This is relevant if `src2` points to `src`, and is excluded with `src2/`. If `false`, `src2` will not be excluded,
191+
/// if `true` it will be excluded as the symlink is considered a directory.
192+
///
193+
/// In other words, for Git compatibility this flag should be `false`, the default, for `git2` compatibility it should be `true`.
194+
pub symlinks_to_directories_are_ignored_like_directories: bool,
186195
}
187196

188197
/// All information that is required to perform a dirwalk, and classify paths properly.
189198
pub struct Context<'a> {
199+
/// If not `None`, it will be checked before entering any directory to trigger early interruption.
200+
///
201+
/// If this flag is `true` at any point in the iteration, it will abort with an error.
202+
pub should_interrupt: Option<&'a AtomicBool>,
190203
/// The `git_dir` of the parent repository, after a call to [`gix_path::realpath()`].
191204
///
192205
/// It's used to help us differentiate our own `.git` directory from nested unrelated repositories,
@@ -261,6 +274,8 @@ pub struct Outcome {
261274
#[derive(Debug, thiserror::Error)]
262275
#[allow(missing_docs)]
263276
pub enum Error {
277+
#[error("Interrupted")]
278+
Interrupted,
264279
#[error("Worktree root at '{}' is not a directory", root.display())]
265280
WorktreeRootIsFile { root: PathBuf },
266281
#[error("Traversal root '{}' contains relative path components and could not be normalized", root.display())]

gix-dir/src/walk/readdir.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use bstr::{BStr, BString, ByteSlice};
22
use std::borrow::Cow;
33
use std::path::{Path, PathBuf};
4+
use std::sync::atomic::Ordering;
45

56
use crate::entry::{PathspecMatch, Status};
67
use crate::walk::function::{can_recurse, emit_entry};
@@ -23,6 +24,9 @@ pub(super) fn recursive(
2324
out: &mut Outcome,
2425
state: &mut State,
2526
) -> Result<(Action, bool), Error> {
27+
if ctx.should_interrupt.map_or(false, |flag| flag.load(Ordering::Relaxed)) {
28+
return Err(Error::Interrupted);
29+
}
2630
out.read_dir_calls += 1;
2731
let entries = gix_fs::read_dir(current, opts.precompose_unicode).map_err(|err| Error::ReadDir {
2832
path: current.to_owned(),

gix-dir/tests/fixtures/many-symlinks.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,29 @@ git init immediate-breakout-symlink
1717
(cd immediate-breakout-symlink
1818
ln -s .. breakout
1919
)
20+
21+
git init excluded-symlinks-to-dir
22+
(cd excluded-symlinks-to-dir
23+
cat <<EOF >.gitignore
24+
src1
25+
src2/
26+
file1
27+
file2/
28+
ignored
29+
ignored-must-be-dir/
30+
EOF
31+
git add .gitignore && git commit -m "init"
32+
33+
mkdir src
34+
>src/file
35+
36+
mkdir ignored-must-be-dir ignored
37+
touch ignored-must-be-dir/file ignored/file
38+
39+
ln -s src src1
40+
ln -s src src2
41+
ln -s src/file file1
42+
ln -s src/file file2
43+
)
44+
45+
ln -s excluded-symlinks-to-dir worktree-root-is-symlink

gix-dir/tests/walk/mod.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use gix_dir::{walk, EntryRef};
22
use pretty_assertions::assert_eq;
3+
use std::sync::atomic::AtomicBool;
34

45
use crate::walk_utils::{
56
collect, collect_filtered, collect_filtered_with_cwd, entry, entry_dirstat, entry_nokind, entry_nomatch, entryps,
@@ -16,6 +17,81 @@ use gix_dir::walk::EmissionMode::*;
1617
use gix_dir::walk::ForDeletionMode;
1718
use gix_ignore::Kind::*;
1819

20+
#[test]
21+
#[cfg_attr(windows, ignore = "symlinks the way they are organized don't yet work on windows")]
22+
fn symlink_to_dir_can_be_excluded() -> crate::Result {
23+
let root = fixture_in("many-symlinks", "excluded-symlinks-to-dir");
24+
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
25+
walk(
26+
&root,
27+
ctx,
28+
gix_dir::walk::Options {
29+
emit_ignored: Some(Matching),
30+
..options()
31+
},
32+
keep,
33+
)
34+
});
35+
assert_eq!(
36+
out,
37+
walk::Outcome {
38+
read_dir_calls: 2,
39+
returned_entries: entries.len(),
40+
seen_entries: 9,
41+
}
42+
);
43+
44+
assert_eq!(
45+
entries,
46+
&[
47+
entry("file1", Ignored(Expendable), Symlink),
48+
entry("file2", Untracked, Symlink),
49+
entry("ignored", Ignored(Expendable), Directory),
50+
entry("ignored-must-be-dir", Ignored(Expendable), Directory),
51+
entry("src/file", Untracked, File),
52+
entry("src1", Ignored(Expendable), Symlink),
53+
entry("src2", Untracked, Symlink), /* marked as src2/ in .gitignore */
54+
],
55+
"by default, symlinks are counted as files only, even if they point to a directory, when handled by the exclude machinery"
56+
);
57+
58+
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
59+
walk(
60+
&root,
61+
ctx,
62+
gix_dir::walk::Options {
63+
emit_ignored: Some(Matching),
64+
symlinks_to_directories_are_ignored_like_directories: true,
65+
..options()
66+
},
67+
keep,
68+
)
69+
});
70+
assert_eq!(
71+
out,
72+
walk::Outcome {
73+
read_dir_calls: 2,
74+
returned_entries: entries.len(),
75+
seen_entries: 9,
76+
}
77+
);
78+
79+
assert_eq!(
80+
entries,
81+
&[
82+
entry("file1", Ignored(Expendable), Symlink),
83+
entry("file2", Untracked, Symlink),
84+
entry("ignored", Ignored(Expendable), Directory),
85+
entry("ignored-must-be-dir", Ignored(Expendable), Directory),
86+
entry("src/file", Untracked, File),
87+
entry("src1", Ignored(Expendable), Symlink),
88+
entry("src2", Ignored(Expendable), Symlink), /* marked as src2/ in .gitignore */
89+
],
90+
"with libgit2 compatibility enabled, symlinks to directories are treated like a directory, not symlink"
91+
);
92+
Ok(())
93+
}
94+
1995
#[test]
2096
#[cfg_attr(windows, ignore = "symlinks the way they are organized don't yet work on windows")]
2197
fn root_may_not_lead_through_symlinks() -> crate::Result {
@@ -43,6 +119,57 @@ fn root_may_not_lead_through_symlinks() -> crate::Result {
43119
Ok(())
44120
}
45121

122+
#[test]
123+
#[cfg_attr(windows, ignore = "symlinks the way they are organized don't yet work on windows")]
124+
fn root_may_be_a_symlink_if_it_is_the_worktree() -> crate::Result {
125+
let root = fixture_in("many-symlinks", "worktree-root-is-symlink");
126+
let ((_out, _root), entries) = collect(&root, None, |keep, ctx| {
127+
walk(
128+
&root,
129+
ctx,
130+
gix_dir::walk::Options {
131+
emit_ignored: Some(Matching),
132+
symlinks_to_directories_are_ignored_like_directories: true,
133+
..options()
134+
},
135+
keep,
136+
)
137+
});
138+
139+
assert_eq!(
140+
entries,
141+
&[
142+
entry("file1", Ignored(Expendable), Symlink),
143+
entry("file2", Untracked, Symlink),
144+
entry("ignored", Ignored(Expendable), Directory),
145+
entry("ignored-must-be-dir", Ignored(Expendable), Directory),
146+
entry("src/file", Untracked, File),
147+
entry("src1", Ignored(Expendable), Symlink),
148+
entry("src2", Ignored(Expendable), Symlink), /* marked as src2/ in .gitignore */
149+
],
150+
"it traversed the directory normally - without this capability, symlinked repos can't be traversed"
151+
);
152+
Ok(())
153+
}
154+
155+
#[test]
156+
fn should_interrupt_works_even_in_empty_directories() {
157+
let root = fixture("empty");
158+
let should_interrupt = AtomicBool::new(true);
159+
let err = try_collect_filtered_opts_collect(
160+
&root,
161+
None,
162+
|keep, ctx| walk(&root, ctx, gix_dir::walk::Options { ..options() }, keep),
163+
None::<&str>,
164+
Options {
165+
should_interrupt: Some(&should_interrupt),
166+
..Default::default()
167+
},
168+
)
169+
.unwrap_err();
170+
assert!(matches!(err, gix_dir::walk::Error::Interrupted));
171+
}
172+
46173
#[test]
47174
fn empty_root() -> crate::Result {
48175
let root = fixture("empty");

0 commit comments

Comments
 (0)