Skip to content

Commit f007eb6

Browse files
authored
Merge pull request #9468 from Byron/graph-segmentation
integration-checks (testing)
2 parents 77865ea + 10f36bc commit f007eb6

File tree

11 files changed

+373
-101
lines changed

11 files changed

+373
-101
lines changed

crates/but-graph/src/projection/workspace.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,12 +473,12 @@ impl Graph {
473473
(out, stopped_at)
474474
}
475475

476-
/// Visit the ancestry of `start` along the first parents, itself included, until `stop` returns `true`.
476+
/// Visit the ancestry of `start` along the first parents, itself excluded, until `stop` returns `true`.
477477
/// Also return the segment that we stopped at.
478478
/// **Important**: `stop` is not called with `start`, this is a feature.
479479
///
480480
/// Note that the traversal assumes as well-segmented graph without cycles.
481-
fn visit_segments_along_first_parent_until(
481+
fn visit_segments_downward_along_first_parent_until(
482482
&self,
483483
start: SegmentIndex,
484484
mut stop: impl FnMut(&Segment) -> bool,
@@ -507,7 +507,7 @@ impl Graph {
507507
mut find: impl FnMut(&Segment) -> Option<T>,
508508
) -> Option<T> {
509509
let mut out = None;
510-
self.visit_segments_along_first_parent_until(start, |s| {
510+
self.visit_segments_downward_along_first_parent_until(start, |s| {
511511
if let Some(res) = find(s) {
512512
out = Some(res);
513513
true

crates/but-testing/src/command/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ pub fn operating_mode(args: &super::Args) -> anyhow::Result<()> {
466466
}
467467

468468
pub fn ref_info(args: &super::Args, ref_name: Option<&str>, expensive: bool) -> anyhow::Result<()> {
469-
let (repo, project) = repo_and_maybe_project(args, RepositoryOpenMode::General)?;
469+
let (repo, project) = repo_and_maybe_project(args, RepositoryOpenMode::Merge)?;
470470
let opts = but_workspace::ref_info::Options {
471471
stack_commit_limit: 0,
472472
expensive_commit_info: expensive,

crates/but-workspace/src/changeset.rs

Lines changed: 88 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@
44
//! This property allows changeset IDs to be used to determine if two different commits, or sets of commits,
55
//! represent the same change.
66
7-
use std::{
8-
borrow::Borrow,
9-
collections::{HashMap, HashSet, hash_map::Entry},
10-
};
11-
127
use crate::{
138
RefInfo,
149
ref_info::{
@@ -18,9 +13,15 @@ use crate::{
1813
ui::PushStatus,
1914
};
2015
use bstr::BString;
21-
use but_core::{ChangeState, TreeChange, TreeStatus, commit::TreeKind};
16+
use but_core::{ChangeState, commit::TreeKind};
17+
use gix::diff::tree::recorder::Change;
2218
use gix::{ObjectId, Repository, object::tree::EntryKind, prelude::ObjectIdExt};
2319
use itertools::Itertools;
20+
use std::ops::Deref;
21+
use std::{
22+
borrow::Borrow,
23+
collections::{HashMap, HashSet, hash_map::Entry},
24+
};
2425

2526
/// The ID of a changeset, calculated as Git hash for convenience.
2627
type ChangesetID = gix::ObjectId;
@@ -68,9 +69,12 @@ impl RefInfo {
6869
self.compute_pushstatus();
6970
return Ok(());
7071
};
72+
let lower_bound_generation = self.lower_bound.map(|sidx| graph[sidx].generation);
7173
graph.visit_all_segments_until(target_tip, but_graph::petgraph::Direction::Outgoing, |s| {
7274
let prune = true;
73-
if Some(s.id) == self.lower_bound {
75+
if Some(s.id) == self.lower_bound
76+
|| lower_bound_generation.is_some_and(|generation| s.generation > generation)
77+
{
7478
return prune;
7579
}
7680
for c in &s.commits {
@@ -101,7 +105,17 @@ impl RefInfo {
101105
// top-to-bottom
102106
.commits
103107
.iter_mut()
104-
.take_while(|c| c.relation == LocalCommitRelation::LocalOnly)
108+
.take_while(|c| {
109+
matches!(
110+
c.relation,
111+
// This happens when the identity match with the remote didn't work.
112+
LocalCommitRelation::LocalOnly |
113+
// This would be expected to be a remote-match by identity (we don't check for this),
114+
// something that is determined during graph traversal time. But we want ot see
115+
// if any of these is also integrated.
116+
LocalCommitRelation::LocalAndRemote(_)
117+
)
118+
})
105119
{
106120
let expensive = changeset_identifier(repo, expensive.then_some(local))?;
107121
if let Some(upstream_commit_id) =
@@ -338,15 +352,19 @@ fn id_for_tree_diff(
338352
// TODO(perf): use plumbing directly to avoid resource-cache overhead.
339353
// consider parallelization
340354
// really needs caching to be practical, in-memory might suffice for now.
341-
let changes = repo.diff_tree_to_tree(
342-
lhs_tree.as_ref(),
343-
&rhs_tree,
344-
*gix::diff::Options::default()
345-
.track_path()
346-
// Rewrite tracking isn't needed for unique IDs and doesn't alter the validity,
347-
// but would cost time, making it useless.
348-
.track_rewrites(None),
355+
356+
let empty_tree = repo.empty_tree();
357+
let mut state = Default::default();
358+
let mut recorder = gix::diff::tree::Recorder::default()
359+
.track_location(Some(gix::diff::tree::recorder::Location::Path));
360+
gix::diff::tree(
361+
gix::objs::TreeRefIter::from_bytes(&lhs_tree.unwrap_or(empty_tree).data),
362+
gix::objs::TreeRefIter::from_bytes(&rhs_tree.data),
363+
&mut state,
364+
repo.objects.deref(),
365+
&mut recorder,
349366
)?;
367+
let changes = recorder.records;
350368
if changes.is_empty() {
351369
return Ok(None);
352370
}
@@ -356,41 +374,73 @@ fn id_for_tree_diff(
356374

357375
// We rely on the diff order, it's consistent as rewrites are disabled.
358376
for c in changes {
359-
if c.entry_mode().is_tree() {
377+
let (entry_mode, location) = match &c {
378+
Change::Addition {
379+
entry_mode, path, ..
380+
}
381+
| Change::Deletion {
382+
entry_mode, path, ..
383+
}
384+
| Change::Modification {
385+
entry_mode, path, ..
386+
} => (*entry_mode, path),
387+
};
388+
if entry_mode.is_tree() {
360389
continue;
361390
}
362-
// For simplicity, use this type.
363-
let c = TreeChange::from(c);
364391
// must hash all fields, even if None for unambiguous hashes.
365-
hash.update(&c.path);
366-
match c.status {
367-
TreeStatus::Addition {
368-
state,
369-
// Ignore as untracked files can't happen with tree/tree diffs
370-
is_untracked: _,
392+
hash.update(location);
393+
match c {
394+
Change::Addition {
395+
entry_mode, oid, ..
371396
} => {
372397
hash.update(b"A");
373-
hash_change_state(&mut hash, state)
398+
hash_change_state(
399+
&mut hash,
400+
ChangeState {
401+
id: oid,
402+
kind: entry_mode.kind(),
403+
},
404+
)
374405
}
375-
TreeStatus::Deletion { previous_state } => {
406+
Change::Deletion {
407+
entry_mode, oid, ..
408+
} => {
376409
hash.update(b"D");
377-
hash_change_state(&mut hash, previous_state)
410+
hash_change_state(
411+
&mut hash,
412+
ChangeState {
413+
id: oid,
414+
kind: entry_mode.kind(),
415+
},
416+
);
378417
}
379-
TreeStatus::Modification {
380-
previous_state,
381-
state,
382-
// Ignore as it's derived
383-
flags: _,
418+
Change::Modification {
419+
previous_entry_mode,
420+
previous_oid,
421+
entry_mode,
422+
oid,
423+
..
384424
} => {
385425
hash.update(b"M");
386-
hash_change_state(&mut hash, previous_state);
387-
hash_change_state(&mut hash, state);
388-
}
389-
TreeStatus::Rename { .. } => {
390-
unreachable!("disabled in prior configuration")
426+
hash_change_state(
427+
&mut hash,
428+
ChangeState {
429+
id: previous_oid,
430+
kind: previous_entry_mode.kind(),
431+
},
432+
);
433+
hash_change_state(
434+
&mut hash,
435+
ChangeState {
436+
id: oid,
437+
kind: entry_mode.kind(),
438+
},
439+
);
391440
}
392441
}
393442
}
443+
394444
Ok(Some(hash.try_finalize()?))
395445
}
396446

crates/but-workspace/src/ref_info.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,14 @@ pub mod ui {
160160
/// Convert this relation into something displaying, mainly for debugging.
161161
pub fn display(&self, id: gix::ObjectId) -> Cow<'static, str> {
162162
match self {
163-
LocalCommitRelation::LocalOnly => "local".into(),
164-
LocalCommitRelation::LocalAndRemote(remote_id) => if *remote_id == id {
165-
"local/remote(identity)"
166-
} else {
167-
"local/remote(similarity)"
163+
LocalCommitRelation::LocalOnly => Cow::Borrowed("local"),
164+
LocalCommitRelation::LocalAndRemote(remote_id) => {
165+
if *remote_id == id {
166+
"local/remote(identity)".into()
167+
} else {
168+
format!("local/remote({})", remote_id.to_hex_with_len(7)).into()
169+
}
168170
}
169-
.into(),
170171
LocalCommitRelation::Integrated(id) => {
171172
format!("integrated({})", id.to_hex_with_len(7)).into()
172173
}

crates/but-workspace/tests/fixtures/scenario/journey02.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
### General Description
44

5-
# Various stages through a typical user journey. Ideally complete enough to test everything that matters to us.
5+
# Disjoint scenarios related to remote branches that are merged in.
66
set -eu -o pipefail
77

88
source "${BASH_SOURCE[0]%/*}/shared.sh"
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env bash
2+
3+
### General Description
4+
5+
# Disjoint scenarios related to remote branches that have been rebased and merged.
6+
set -eu -o pipefail
7+
8+
source "${BASH_SOURCE[0]%/*}/shared.sh"
9+
10+
git init 01-one-rewritten-one-local-after-push
11+
(cd 01-one-rewritten-one-local-after-push
12+
echo "two local commits pushed to a remote, then rebased onto target.
13+
14+
The branch should then be considered integrated" >.git/description
15+
commit init && setup_target_to_match_main
16+
git checkout -b A
17+
echo A-new >file && git add file && git commit -m A1
18+
echo A-new >other && git add other && git commit -m A2
19+
git rev-parse @ >.git/refs/remotes/origin/A
20+
21+
git checkout main
22+
commit M1
23+
git cherry-pick :/A1
24+
git cherry-pick :/A2
25+
echo M-change >>file && git commit -am M2
26+
echo M-change >>other && git commit -am M3
27+
git rev-parse @ >.git/refs/remotes/origin/main
28+
29+
git checkout A
30+
create_workspace_commit_once A
31+
)
32+
33+
git init 01-one-rewritten-one-local-after-push-author-date-change
34+
(cd 01-one-rewritten-one-local-after-push-author-date-change
35+
echo "two local commits pushed to a remote, then rebased onto target, but with the author date adjusted.
36+
37+
This prevents quick-checks to work." >.git/description
38+
commit init && setup_target_to_match_main
39+
git checkout -b A
40+
echo A-new >file && git add file && git commit -m A1
41+
echo A-new >other && git add other && git commit -m A2
42+
git rev-parse @ >.git/refs/remotes/origin/A
43+
44+
git checkout main
45+
commit M1
46+
tick
47+
git cherry-pick :/A1
48+
tick
49+
git cherry-pick :/A2
50+
echo M-change >>file && git commit -am M2
51+
echo M-change >>other && git commit -am M3
52+
git rev-parse @ >.git/refs/remotes/origin/main
53+
54+
git checkout A
55+
create_workspace_commit_once A
56+
)

0 commit comments

Comments
 (0)