Skip to content

Commit 2217a82

Browse files
committed
streamline status API
1 parent 2683e64 commit 2217a82

File tree

18 files changed

+480
-408
lines changed

18 files changed

+480
-408
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-hash/src/object_id.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ impl ObjectId {
127127
}
128128
}
129129

130+
/// Returns true if this hash is equal to an empty blob
131+
#[inline]
132+
pub fn is_empty_blob(&self) -> bool {
133+
self == &Self::empty_blob(self.kind())
134+
}
135+
130136
/// Returns an Digest representing a hash with whose memory is zeroed.
131137
#[inline]
132138
pub const fn null(kind: crate::Kind) -> ObjectId {

gix-index/src/access/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ impl State {
2121
self.timestamp
2222
}
2323

24+
/// Updates the timestamp of this state, indicating its freshness compared
25+
/// to other files on disk. Be careful about using this as setting a
26+
/// timestamp without correctly updating the index **will cause (file
27+
/// system) race conditions** see racy-git.txt (in the git documentation)
28+
/// for more details.
29+
pub fn set_timestamp(&mut self, timestamp: FileTime) {
30+
self.timestamp = timestamp
31+
}
32+
2433
/// Return the kind of hashes used in this instance.
2534
pub fn object_hash(&self) -> gix_hash::Kind {
2635
self.object_hash

gix-index/src/entry/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/// The stage of an entry, one of 0 = base, 1 = ours, 2 = theirs
22
pub type Stage = u32;
33

4-
mod mode;
4+
///
5+
pub mod mode;
56
pub use mode::Mode;
67

78
mod flags;

gix-index/src/entry/mode.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
use bitflags::bitflags;
2+
3+
// TODO: we essentially treat this as an enum withj the only exception being
4+
// that `FILE_EXECUTABLE.contains(FILE)` works might want to turn this into an
5+
// enum proper
26
bitflags! {
37
/// The kind of file of an entry.
48
pub struct Mode: u32 {
@@ -16,9 +20,74 @@ bitflags! {
1620
}
1721
}
1822

23+
#[cfg(unix)]
24+
/// Returns whether a a file has the executable permission set
25+
pub fn is_executable(metadata: &std::fs::Metadata) -> bool {
26+
use std::os::unix::fs::MetadataExt;
27+
(metadata.mode() & 0o100) != 0
28+
}
29+
30+
#[cfg(not(unix))]
31+
/// Returns whether a a file has the executable permission set
32+
pub fn is_executable(_metadata: &std::fs::Metadata) -> bool {
33+
false
34+
}
35+
1936
impl Mode {
2037
/// Return true if this is a sparse entry, as it points to a directory which usually isn't what an unsparse index tracks.
2138
pub fn is_sparse(&self) -> bool {
2239
*self == Self::DIR
2340
}
41+
42+
/// Compares this mode to the file system version ([`std::fs::symlink_metadata`])
43+
/// and returns the change needed to update this mode to match the file if
44+
pub fn change_to_match_fs(
45+
self,
46+
stat: &std::fs::Metadata,
47+
has_symlinks: bool,
48+
executable_bit: bool,
49+
) -> Option<Change> {
50+
match self {
51+
Mode::FILE if !stat.is_file() => (),
52+
Mode::SYMLINK if has_symlinks && !stat.is_symlink() => (),
53+
Mode::SYMLINK if !has_symlinks && !stat.is_file() => (),
54+
Mode::COMMIT | Mode::DIR if !stat.is_dir() => (),
55+
Mode::FILE if executable_bit && is_executable(stat) => return Some(Change::ExecutableBit),
56+
Mode::FILE_EXECUTABLE if executable_bit && !is_executable(stat) => return Some(Change::ExecutableBit),
57+
_ => return None,
58+
};
59+
let new_mode = if stat.is_dir() {
60+
Mode::DIR
61+
} else if executable_bit && is_executable(stat) {
62+
Mode::FILE_EXECUTABLE
63+
} else {
64+
Mode::FILE
65+
};
66+
Some(Change::Type { new_mode })
67+
}
68+
}
69+
70+
/// A change of a [`Mode`]
71+
pub enum Change {
72+
/// The type of mode changed (like symlink => file)
73+
Type {
74+
/// The mode representing the new index type
75+
new_mode: Mode,
76+
},
77+
/// The executable permission of this file has changed
78+
ExecutableBit,
79+
}
80+
81+
impl Change {
82+
/// Applies this change to a `Mode`
83+
pub fn apply(self, mode: &mut Mode) {
84+
*mode = match self {
85+
Change::Type { new_mode } => new_mode,
86+
Change::ExecutableBit => match *mode {
87+
Mode::FILE => Mode::FILE_EXECUTABLE,
88+
Mode::FILE_EXECUTABLE => Mode::FILE,
89+
_ => unreachable!("invalid mode change: can't flip executable bit of {mode:?}"),
90+
},
91+
}
92+
}
2493
}

gix-worktree/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ gix-features = { version = "^0.28.0", path = "../gix-features" }
4141
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}
4242

4343
thiserror = "1.0.26"
44+
filetime = "0.2.15"
4445
bstr = { version = "1.3.0", default-features = false }
4546

4647
document-features = { version = "0.2.0", optional = true }

gix-worktree/src/index/status.rs

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,60 @@
11
//!
22
3+
use bstr::BStr;
4+
35
///
46
pub mod worktree;
57

6-
mod index;
7-
88
///
9-
pub mod visit;
9+
pub mod index;
1010

1111
///
1212
pub mod recorder;
1313

1414
///
15-
pub struct IndexStatus<'index> {
16-
index: &'index gix_index::State,
17-
}
15+
pub mod diff;
1816

19-
impl<'index> From<&'index gix_index::File> for IndexStatus<'index> {
20-
fn from(file: &'index gix_index::File) -> Self {
21-
Self { index: file }
22-
}
17+
/// The status of an index entry in a worktree
18+
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
19+
pub enum Status<T = ()> {
20+
#[default]
21+
/// The file in the worktree is identical to the index entry
22+
Unchanged,
23+
/// An index entry has no corresponding file in the worktree.
24+
Removed,
25+
/// The type of file changed (symlink <=> file) performing a
26+
/// diff is usually not necessary/desired
27+
TypeChange,
28+
/// The worktree file that belongs to this index has a changed stat
29+
/// therefore could have been modified.
30+
///
31+
/// Note that this doesn't necessarily mean that the *content* of the file changed.
32+
/// A modified even is emitted whenever a change could have occurred. It is up
33+
/// to API consumers to check the file for content changes
34+
Modified {
35+
/// Indicates that one of the stat changes was an executable bit change
36+
/// which is a significant change itself (for git status)
37+
/// these files don't need to be rechanged
38+
executable_bit_changed: bool,
39+
/// The output of the diff run on this entry.
40+
/// if the there is no content change and only the executable bit
41+
/// changed than this is `None`
42+
diff: Option<T>,
43+
},
44+
/// An index entry that correspond to an untracked worktree file marked with `git add`
45+
Added,
2346
}
2447

25-
impl<'index> From<&'index gix_index::State> for IndexStatus<'index> {
26-
fn from(index: &'index gix_index::State) -> Self {
27-
Self { index }
28-
}
48+
///
49+
pub trait Collector<'index> {
50+
/// Data generated by comparing two files/entries
51+
type Diff;
52+
///
53+
fn visit_entry(
54+
&mut self,
55+
entry: &'index gix_index::Entry,
56+
path: &'index BStr,
57+
status: Status<Self::Diff>,
58+
conflict: bool,
59+
);
2960
}

gix-worktree/src/index/status/diff.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use gix_features::hash;
2+
use gix_hash::ObjectId;
3+
use gix_index as index;
4+
use gix_object::encode::loose_header;
5+
use index::Entry;
6+
7+
///
8+
pub trait LazyBlob<'a, E> {
9+
///
10+
fn read(self) -> Result<&'a [u8], E>;
11+
}
12+
13+
///
14+
pub trait Diff: Send + Sync {
15+
///
16+
type Output;
17+
///
18+
fn content_changed<'a, E>(
19+
&self,
20+
entry: &'a Entry,
21+
blob_size: usize,
22+
blob: impl LazyBlob<'a, E>,
23+
resolve_oid: impl FnMut(gix_hash::ObjectId) -> Result<&'a [u8], E>,
24+
) -> Result<Option<Self::Output>, E>;
25+
}
26+
27+
/// compares to blobs by comparing their size and oid very fast
28+
pub struct Fast;
29+
30+
impl Diff for Fast {
31+
type Output = ();
32+
33+
fn content_changed<'a, E>(
34+
&self,
35+
entry: &'a Entry,
36+
blob_size: usize,
37+
blob: impl LazyBlob<'a, E>,
38+
_resolve_oid: impl FnMut(gix_hash::ObjectId) -> Result<&'a [u8], E>,
39+
) -> Result<Option<Self::Output>, E> {
40+
// make sure to account for racily smudged entries here
41+
// so that they don't always keep showing up as modified even
42+
// after their contents have changed again (to a potentially unmodified state)
43+
// that means that we want to ignore stat.size == 0 for non_empty_blobs
44+
if entry.stat.size as usize != blob_size && (entry.id.is_empty_blob() || entry.stat.size != 0) {
45+
return Ok(Some(()));
46+
}
47+
let blob = blob.read()?;
48+
let header = loose_header(gix_object::Kind::Blob, blob.len());
49+
match entry.id {
50+
ObjectId::Sha1(entry_hash) => {
51+
let mut file_hash = hash::Sha1::default();
52+
file_hash.update(&header);
53+
file_hash.update(blob);
54+
let file_hash = file_hash.digest();
55+
Ok((entry_hash != file_hash).then_some(()))
56+
}
57+
}
58+
}
59+
}
60+
61+
/// compares to blobs by comparing their oid
62+
/// Same as [`FastEq`] but always
63+
pub struct Hash;
64+
65+
impl Diff for Hash {
66+
type Output = ObjectId;
67+
68+
fn content_changed<'a, E>(
69+
&self,
70+
entry: &'a Entry,
71+
_blob_size: usize,
72+
blob: impl LazyBlob<'a, E>,
73+
_resolve_oid: impl FnMut(gix_hash::ObjectId) -> Result<&'a [u8], E>,
74+
) -> Result<Option<Self::Output>, E> {
75+
let blob = blob.read()?;
76+
let header = loose_header(gix_object::Kind::Blob, blob.len());
77+
match entry.id {
78+
ObjectId::Sha1(entry_hash) => {
79+
let mut file_hash = hash::Sha1::default();
80+
file_hash.update(&header);
81+
file_hash.update(blob);
82+
let file_hash = file_hash.digest();
83+
Ok((entry_hash != file_hash).then_some(ObjectId::Sha1(file_hash)))
84+
}
85+
}
86+
}
87+
}
Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,28 @@
1+
use bstr::BStr;
2+
3+
use gix_index as index;
4+
5+
use crate::index::status::{Collector, Status};
6+
17
///
2-
pub mod index;
3-
///
4-
pub mod worktree;
8+
#[derive(Debug, Default)]
9+
pub struct Recorder<'index, T = ()> {
10+
/// collected records, unchanged fields are excluded
11+
pub records: Vec<(&'index BStr, Status<T>, bool)>,
12+
}
13+
14+
impl<'index, T> Collector<'index> for Recorder<'index, T> {
15+
type Diff = T;
16+
17+
fn visit_entry(
18+
&mut self,
19+
_entry: &'index index::Entry,
20+
path: &'index BStr,
21+
status: Status<Self::Diff>,
22+
conflict: bool,
23+
) {
24+
if !matches!(status, Status::Unchanged) {
25+
self.records.push((path, status, conflict))
26+
}
27+
}
28+
}

gix-worktree/src/index/status/recorder/index.rs

Lines changed: 0 additions & 1 deletion
This file was deleted.

gix-worktree/src/index/status/recorder/worktree.rs

Lines changed: 0 additions & 62 deletions
This file was deleted.

0 commit comments

Comments
 (0)