Skip to content

Commit cebef0b

Browse files
committed
feat!: better handling and tracking of directory renames
Deletations that are caused by deletions higher up will now carry IDs to associate them with each other. This allows to bundle them at a later stage to re-obtain directory deletions.
1 parent b25fe4d commit cebef0b

File tree

8 files changed

+409
-145
lines changed

8 files changed

+409
-145
lines changed

gix-diff/src/tree/changes.rs

Lines changed: 136 additions & 46 deletions
Large diffs are not rendered by default.

gix-diff/src/tree/mod.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,43 @@
1-
use std::collections::VecDeque;
2-
1+
use crate::tree::visit::Relation;
2+
use bstr::BStr;
33
use gix_hash::ObjectId;
44
use gix_object::{bstr::BString, TreeRefIter};
5+
use std::collections::VecDeque;
6+
7+
/// A trait to allow responding to a traversal designed to figure out the [changes](visit::Change)
8+
/// to turn tree A into tree B.
9+
pub trait Visit {
10+
/// Sets the full path in front of the queue so future calls to push and pop components affect it instead.
11+
fn pop_front_tracked_path_and_set_current(&mut self);
12+
/// Append a `component` to the end of a path, which may be empty.
13+
fn push_back_tracked_path_component(&mut self, component: &BStr);
14+
/// Append a `component` to the end of a path, which may be empty.
15+
fn push_path_component(&mut self, component: &BStr);
16+
/// Removes the last component from the path, which may leave it empty.
17+
fn pop_path_component(&mut self);
18+
/// Record a `change` and return an instruction whether to continue or not.
19+
///
20+
/// The implementation may use the current path to lean where in the tree the change is located.
21+
fn visit(&mut self, change: visit::Change) -> visit::Action;
22+
}
523

624
/// The state required to visit [Changes] to be instantiated with `State::default()`.
725
#[derive(Default, Clone)]
826
pub struct State {
927
buf1: Vec<u8>,
1028
buf2: Vec<u8>,
11-
trees: VecDeque<TreeInfoPair>,
29+
trees: VecDeque<TreeInfoTuple>,
30+
change_id: visit::ChangeId,
1231
}
1332

14-
type TreeInfoPair = (Option<ObjectId>, Option<ObjectId>);
33+
type TreeInfoTuple = (Option<ObjectId>, Option<ObjectId>, Option<Relation>);
1534

1635
impl State {
1736
fn clear(&mut self) {
1837
self.trees.clear();
1938
self.buf1.clear();
2039
self.buf2.clear();
40+
self.change_id = 0;
2141
}
2242
}
2343

@@ -38,8 +58,6 @@ pub mod changes;
3858

3959
///
4060
pub mod visit;
41-
#[doc(inline)]
42-
pub use visit::Visit;
4361

4462
/// A [Visit] implementation to record every observed change and keep track of the changed paths.
4563
#[derive(Clone, Debug)]

gix-diff/src/tree/recorder.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use gix_object::{
44
tree,
55
};
66

7-
use crate::tree::{visit, Recorder};
7+
use crate::tree::visit::Relation;
8+
use crate::tree::{visit, Recorder, Visit};
89

910
/// Describe how to track the location of a change.
1011
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
@@ -17,7 +18,7 @@ pub enum Location {
1718
FileName,
1819
}
1920

20-
/// A Change as observed by a call to [`visit(…)`][visit::Visit::visit()], enhanced with the path affected by the change.
21+
/// A Change as observed by a call to [`visit(…)`](Visit::visit()), enhanced with the path affected by the change.
2122
/// Its similar to [`visit::Change`] but includes the path that changed.
2223
#[derive(Clone, Debug, PartialEq, Eq)]
2324
#[allow(missing_docs)]
@@ -26,11 +27,13 @@ pub enum Change {
2627
entry_mode: tree::EntryMode,
2728
oid: ObjectId,
2829
path: BString,
30+
relation: Option<Relation>,
2931
},
3032
Deletion {
3133
entry_mode: tree::EntryMode,
3234
oid: ObjectId,
3335
path: BString,
36+
relation: Option<Relation>,
3437
},
3538
Modification {
3639
previous_entry_mode: tree::EntryMode,
@@ -93,7 +96,7 @@ impl Recorder {
9396
}
9497
}
9598

96-
impl visit::Visit for Recorder {
99+
impl Visit for Recorder {
97100
fn pop_front_tracked_path_and_set_current(&mut self) {
98101
if let Some(Location::Path) = self.location {
99102
self.path = self.path_deque.pop_front().expect("every parent is set only once");
@@ -136,15 +139,25 @@ impl visit::Visit for Recorder {
136139
fn visit(&mut self, change: visit::Change) -> visit::Action {
137140
use visit::Change::*;
138141
self.records.push(match change {
139-
Deletion { entry_mode, oid } => Change::Deletion {
142+
Deletion {
143+
entry_mode,
144+
oid,
145+
relation,
146+
} => Change::Deletion {
140147
entry_mode,
141148
oid,
142149
path: self.path_clone(),
150+
relation,
143151
},
144-
Addition { entry_mode, oid } => Change::Addition {
152+
Addition {
153+
entry_mode,
154+
oid,
155+
relation,
156+
} => Change::Addition {
145157
entry_mode,
146158
oid,
147159
path: self.path_clone(),
160+
relation,
148161
},
149162
Modification {
150163
previous_entry_mode,

gix-diff/src/tree/visit.rs

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
use gix_hash::ObjectId;
2-
use gix_object::{bstr::BStr, tree, tree::EntryMode};
2+
use gix_object::{tree, tree::EntryMode};
3+
4+
/// A way to recognize and associate different [`Change`] instances.
5+
///
6+
/// These are unique only within one diff operation.
7+
pub type ChangeId = u32;
8+
9+
/// Identifies a relationship between this instance and another one.
10+
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Hash)]
11+
pub enum Relation {
12+
/// This is a parent with the given ID, which will always have at least one child
13+
/// assuming that empty directories are not allowed in valid trees.
14+
/// It's also always a tree which is the start of a recursive deletion or addition.
15+
///
16+
/// The change with this relation is always emitted first.
17+
Parent(ChangeId),
18+
/// This is a direct or indirect child, tree or not tree, of the parent with the given ID.
19+
ChildOfParent(ChangeId),
20+
}
321

422
/// Represents any possible change in order to turn one tree into another.
523
#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq, Hash)]
@@ -10,13 +28,17 @@ pub enum Change {
1028
entry_mode: tree::EntryMode,
1129
/// The object id of the added entry.
1230
oid: ObjectId,
31+
/// Possibly associate this change with another for hierarchical rename tracking.
32+
relation: Option<Relation>,
1333
},
1434
/// An entry was deleted, like the deletion of a file or directory.
1535
Deletion {
1636
/// The mode of the deleted entry.
1737
entry_mode: tree::EntryMode,
1838
/// The object id of the deleted entry.
1939
oid: ObjectId,
40+
/// Possibly associate this change with another for hierarchical rename tracking.
41+
relation: Option<Relation>,
2042
},
2143
/// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning
2244
/// a file into a symbolic link adjusts its mode.
@@ -51,20 +73,28 @@ impl Change {
5173
/// Return the current object id and tree entry mode of a change.
5274
pub fn oid_and_entry_mode(&self) -> (&gix_hash::oid, EntryMode) {
5375
match self {
54-
Change::Addition { oid, entry_mode }
55-
| Change::Deletion { oid, entry_mode }
76+
Change::Addition {
77+
oid,
78+
entry_mode,
79+
relation: _,
80+
}
81+
| Change::Deletion {
82+
oid,
83+
entry_mode,
84+
relation: _,
85+
}
5686
| Change::Modification { oid, entry_mode, .. } => (oid, *entry_mode),
5787
}
5888
}
5989
}
6090

61-
/// What to do after a [Change] was [recorded][Visit::visit()].
91+
/// What to do after a [Change] was [recorded](super::Visit::visit()).
6292
#[derive(Default, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)]
6393
pub enum Action {
6494
/// Continue the traversal of changes.
6595
#[default]
6696
Continue,
67-
/// Stop the traversal of changes, making this the last call to [visit(…)][Visit::visit()].
97+
/// Stop the traversal of changes, making this the last call to [visit(…)](super::Visit::visit()).
6898
Cancel,
6999
}
70100

@@ -75,23 +105,6 @@ impl Action {
75105
}
76106
}
77107

78-
/// A trait to allow responding to a traversal designed to figure out the [changes][Change]
79-
/// to turn tree A into tree B.
80-
pub trait Visit {
81-
/// Sets the full path path in front of the queue so future calls to push and pop components affect it instead.
82-
fn pop_front_tracked_path_and_set_current(&mut self);
83-
/// Append a `component` to the end of a path, which may be empty.
84-
fn push_back_tracked_path_component(&mut self, component: &BStr);
85-
/// Append a `component` to the end of a path, which may be empty.
86-
fn push_path_component(&mut self, component: &BStr);
87-
/// Removes the last component from the path, which may leave it empty.
88-
fn pop_path_component(&mut self);
89-
/// Record a `change` and return an instruction whether to continue or not.
90-
///
91-
/// The implementation may use the current path to lean where in the tree the change is located.
92-
fn visit(&mut self, change: Change) -> Action;
93-
}
94-
95108
#[cfg(feature = "blob")]
96109
mod change_impls {
97110
use gix_hash::oid;
@@ -140,8 +153,8 @@ mod tests {
140153
fn size_of_change() {
141154
let actual = std::mem::size_of::<Change>();
142155
assert!(
143-
actual <= 46,
144-
"{actual} <= 46: this type shouldn't grow without us knowing"
156+
actual <= 48,
157+
"{actual} <= 48: this type shouldn't grow without us knowing"
145158
);
146159
}
147160
}
Binary file not shown.
Binary file not shown.

gix-diff/tests/fixtures/make_diff_repo.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ git commit -qam 'rm g/aa, add g/a'
9999
rm -Rf ./* && mkdir git-sec gix && touch a git-sec/2 git-sequencer h gix/5 && git add .
100100
git commit -am "clear slate"
101101

102-
git mv git-sec gix-sec && git commit -m "interesting rename 1"
102+
mkdir git-sec/subdir && touch git-sec/subdir/6 git-sec/7
103+
git add . && git commit -m "add files to git-sec"
103104

104-
git mv gix-sec git-sec && git commit -m "interesting rename 2"
105+
git mv git-sec gix-sec && git commit -m "rename git-sec to gix-sec"
106+
107+
git mv gix-sec git-sec && git commit -m "rename gix-sec to git-sec"

0 commit comments

Comments
 (0)