Skip to content

Commit 9cc6300

Browse files
committed
integration: Intial steps and display information
1 parent 8f33869 commit 9cc6300

2 files changed

Lines changed: 210 additions & 15 deletions

File tree

crates/but-workspace/src/branch/integrate_branch_upstream.rs

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -806,25 +806,61 @@ pub fn get_initial_integration_steps_for_branch(
806806
}
807807

808808
for upstream_commit in remote_only_commits {
809+
divergence_upstream_only.push(divergence_commit(repo, upstream_commit)?);
809810
initial_steps.push(InteractiveIntegrationStep::PickUpstream {
810811
commit_id: upstream_commit,
811812
});
812813
}
813814

814815
initial_steps.reverse();
815816

816-
Ok(InteractiveIntegration {
817+
let integration = InteractiveIntegration {
817818
steps: initial_steps,
818819
merge_base,
820+
};
821+
let mut divergence = IntegrationDivergenceDisplay {
822+
branch_ref_name: ref_name.to_owned(),
823+
upstream_ref_name: upstream_ref_name.into_owned(),
824+
local_only: divergence_local_only,
825+
upstream_only: divergence_upstream_only,
826+
matched: divergence_matched,
827+
merge_base: divergence_commit(repo, merge_base)?,
828+
};
829+
let local_tip = divergence
830+
.local_only
831+
.first()
832+
.map(|commit| commit.id)
833+
.or_else(|| divergence.matched.first().map(|commit| commit.id));
834+
let upstream_tip = divergence
835+
.upstream_only
836+
.first()
837+
.map(|commit| commit.id)
838+
.or_else(|| divergence.matched.first().map(|commit| commit.id));
839+
add_ref_label(
840+
&mut divergence.local_only,
841+
&mut divergence.matched,
842+
local_tip,
843+
divergence.branch_ref_name.shorten().to_string(),
844+
);
845+
add_ref_label(
846+
&mut divergence.upstream_only,
847+
&mut divergence.matched,
848+
upstream_tip,
849+
divergence.upstream_ref_name.shorten().to_string(),
850+
);
851+
852+
Ok(InitialBranchIntegration {
853+
integration,
854+
divergence,
819855
})
820856
}
821857

822858
/// Computes local and upstream commit lists (tip to merge-base, first-parent) together
823859
/// with their merge base for a branch and its tracking branch.
824-
fn get_commits_until_merge_base(
825-
ref_name: &gix::refs::FullNameRef,
826-
repo: &gix::Repository,
827-
) -> Result<(Vec<gix::ObjectId>, Vec<gix::ObjectId>, gix::ObjectId), anyhow::Error> {
860+
fn get_commits_until_merge_base<'a>(
861+
ref_name: &'a gix::refs::FullNameRef,
862+
repo: &'a gix::Repository,
863+
) -> Result<BranchMergeBaseCommits<'a>, anyhow::Error> {
828864
let (local_tip, upstream_ref_name, upstream_tip) =
829865
get_branch_tips_and_upstream(ref_name, repo)?;
830866
let cache = repo.commit_graph_if_enabled()?;
@@ -839,7 +875,12 @@ fn get_commits_until_merge_base(
839875
})?;
840876
let local_commits = branch_commits_until(repo, local_tip, merge_base)?;
841877
let upstream_commits = branch_commits_until(repo, upstream_tip, merge_base)?;
842-
Ok((local_commits, upstream_commits, merge_base))
878+
Ok(BranchMergeBaseCommits {
879+
local_commits,
880+
upstream_commits,
881+
merge_base,
882+
upstream_ref_name,
883+
})
843884
}
844885

845886
/// Resolves local/upstream branch tips and tracking reference name for `ref_name`.
@@ -945,6 +986,59 @@ fn effective_change_id(repo: &gix::Repository, commit_id: gix::ObjectId) -> Resu
945986
.to_string())
946987
}
947988

989+
fn divergence_commit(
990+
repo: &gix::Repository,
991+
commit_id: gix::ObjectId,
992+
) -> Result<IntegrationDivergenceCommit> {
993+
Ok(IntegrationDivergenceCommit {
994+
id: commit_id,
995+
subject: but_core::Commit::from_id(commit_id.attach(repo))?
996+
.message
997+
.lines()
998+
.next()
999+
.unwrap_or_default()
1000+
.to_str_lossy()
1001+
.into_owned(),
1002+
refs: Vec::new(),
1003+
})
1004+
}
1005+
1006+
fn add_ref_label(
1007+
primary: &mut [IntegrationDivergenceCommit],
1008+
secondary: &mut [IntegrationDivergenceCommit],
1009+
id: Option<gix::ObjectId>,
1010+
label: String,
1011+
) {
1012+
let Some(id) = id else {
1013+
return;
1014+
};
1015+
if let Some(commit) = primary.iter_mut().find(|commit| commit.id == id) {
1016+
if !commit.refs.contains(&label) {
1017+
commit.refs.push(label);
1018+
}
1019+
return;
1020+
}
1021+
if let Some(commit) = secondary.iter_mut().find(|commit| commit.id == id)
1022+
&& !commit.refs.contains(&label)
1023+
{
1024+
commit.refs.push(label);
1025+
}
1026+
}
1027+
1028+
fn graph_commit_string(prefix: &str, commit: &IntegrationDivergenceCommit) -> String {
1029+
let refs = if commit.refs.is_empty() {
1030+
String::new()
1031+
} else {
1032+
format!(" ({})", commit.refs.join(", "))
1033+
};
1034+
format!(
1035+
"{prefix}{}{} {}",
1036+
commit.id.to_hex_with_len(7),
1037+
refs,
1038+
commit.subject
1039+
)
1040+
}
1041+
9481042
#[cfg(test)]
9491043
mod tests {
9501044
use super::*;
@@ -1044,4 +1138,48 @@ mod tests {
10441138
"invalid squash message should produce a targeted error"
10451139
);
10461140
}
1141+
1142+
#[test]
1143+
fn divergence_display_renders_git_style_graph() {
1144+
let display = IntegrationDivergenceDisplay {
1145+
branch_ref_name: gix::refs::Category::LocalBranch
1146+
.to_full_name("feature")
1147+
.expect("valid local branch"),
1148+
upstream_ref_name: gix::refs::Category::RemoteBranch
1149+
.to_full_name("origin/feature")
1150+
.expect("valid remote branch"),
1151+
local_only: vec![IntegrationDivergenceCommit {
1152+
id: oid("1111111111111111111111111111111111111111"),
1153+
subject: "local tip".into(),
1154+
refs: vec!["feature".into()],
1155+
}],
1156+
upstream_only: vec![IntegrationDivergenceCommit {
1157+
id: oid("2222222222222222222222222222222222222222"),
1158+
subject: "remote tip".into(),
1159+
refs: vec!["origin/feature".into()],
1160+
}],
1161+
matched: vec![IntegrationDivergenceCommit {
1162+
id: oid("3333333333333333333333333333333333333333"),
1163+
subject: "shared".into(),
1164+
refs: Vec::new(),
1165+
}],
1166+
merge_base: IntegrationDivergenceCommit {
1167+
id: oid("4444444444444444444444444444444444444444"),
1168+
subject: "base".into(),
1169+
refs: Vec::new(),
1170+
},
1171+
};
1172+
1173+
insta::assert_snapshot!(
1174+
display.to_string(),
1175+
"graph output should stay stable because the CLI and frontend consume it directly",
1176+
@r"
1177+
* 1111111 (feature) local tip
1178+
| * 2222222 (origin/feature) remote tip
1179+
|/
1180+
* 3333333 shared
1181+
* 4444444 base
1182+
"
1183+
);
1184+
}
10471185
}

crates/but-workspace/tests/workspace/branch/integrate_branch_upstream.rs

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,11 @@ fn partitions_diverged_branch_into_application_order() -> Result<()> {
169169
let merge_base = repo.rev_parse_single("A~1")?.detach();
170170
configure_tracking_for_branch_a(&mut repo)?;
171171

172-
let integration = get_initial_integration_steps_for_branch(r("refs/heads/A"), &repo)?;
172+
let initial = get_initial_integration_steps_for_branch(r("refs/heads/A"), &repo)?;
173173

174174
insta::assert_snapshot!(
175175
labeled_integration_snapshot(
176-
&integration,
176+
&initial.integration,
177177
&[
178178
(merge_base, "merge-base"),
179179
(local_tip, "local-tip"),
@@ -187,7 +187,24 @@ fn partitions_diverged_branch_into_application_order() -> Result<()> {
187187
"
188188
);
189189

190-
let step_ids = pick_step_ids(&integration.steps);
190+
insta::assert_snapshot!(
191+
labeled_divergence_snapshot(
192+
&initial,
193+
&[
194+
(merge_base, "merge-base"),
195+
(local_tip, "local-tip"),
196+
(upstream_tip, "upstream-tip"),
197+
]
198+
),
199+
@"
200+
* local-tip (A) local change in A
201+
| * upstream-tip (origin/A) change in A
202+
|/
203+
* merge-base new file in A
204+
"
205+
);
206+
207+
let step_ids = pick_step_ids(&initial.integration.steps);
191208

192209
assert_eq!(
193210
step_ids,
@@ -266,11 +283,11 @@ fn matches_rewritten_commit_by_change_id_and_keeps_order() -> Result<()> {
266283
let merge_base = repo.rev_parse_single("A~2")?.detach();
267284
configure_tracking_for_branch_a(&mut repo)?;
268285

269-
let integration = get_initial_integration_steps_for_branch(r("refs/heads/A"), &repo)?;
286+
let initial = get_initial_integration_steps_for_branch(r("refs/heads/A"), &repo)?;
270287

271288
insta::assert_snapshot!(
272289
labeled_integration_snapshot(
273-
&integration,
290+
&initial.integration,
274291
&[
275292
(merge_base, "merge-base"),
276293
(local_only, "local-only"),
@@ -286,7 +303,26 @@ fn matches_rewritten_commit_by_change_id_and_keeps_order() -> Result<()> {
286303
"
287304
);
288305

289-
let step_ids = pick_step_ids(&integration.steps);
306+
insta::assert_snapshot!(
307+
labeled_divergence_snapshot(
308+
&initial,
309+
&[
310+
(merge_base, "merge-base"),
311+
(local_only, "local-only"),
312+
(remote_only, "remote-only"),
313+
(local_and_remote, "local-and-remote"),
314+
]
315+
),
316+
@"
317+
* local-only (A) A1
318+
| * remote-only (origin/A) A1
319+
|/
320+
* local-and-remote A2
321+
* merge-base init
322+
"
323+
);
324+
325+
let step_ids = pick_step_ids(&initial.integration.steps);
290326

291327
assert_eq!(
292328
step_ids,
@@ -1348,11 +1384,11 @@ fn initial_steps_remote_diverged_with_workspace_are_in_application_order() -> Re
13481384
let merge_base = repo.rev_parse_single("A~2")?.detach();
13491385
configure_tracking_for_branch_a(&mut repo)?;
13501386

1351-
let integration = get_initial_integration_steps_for_branch(r("refs/heads/A"), &repo)?;
1387+
let initial = get_initial_integration_steps_for_branch(r("refs/heads/A"), &repo)?;
13521388

13531389
insta::assert_snapshot!(
13541390
labeled_integration_snapshot(
1355-
&integration,
1391+
&initial.integration,
13561392
&[
13571393
(merge_base, "merge-base"),
13581394
(local_commit_2, "local-commit-2"),
@@ -1370,8 +1406,29 @@ fn initial_steps_remote_diverged_with_workspace_are_in_application_order() -> Re
13701406
"
13711407
);
13721408

1409+
insta::assert_snapshot!(
1410+
labeled_divergence_snapshot(
1411+
&initial,
1412+
&[
1413+
(merge_base, "merge-base"),
1414+
(local_commit_2, "local-commit-2"),
1415+
(local_commit_1, "local-commit-1"),
1416+
(remote_commit_2, "remote-commit-2"),
1417+
(remote_commit_1, "remote-commit-1"),
1418+
]
1419+
),
1420+
@"
1421+
* local-commit-2 (A) local change in A 2
1422+
* local-commit-1 local change in A 1
1423+
| * remote-commit-2 (origin/A) remote change in A 2
1424+
| * remote-commit-1 remote change in A 1
1425+
|/
1426+
* merge-base shared local/remote
1427+
"
1428+
);
1429+
13731430
assert_eq!(
1374-
pick_step_ids(&integration.steps),
1431+
pick_step_ids(&initial.integration.steps),
13751432
vec![
13761433
remote_commit_1,
13771434
remote_commit_2,

0 commit comments

Comments
 (0)