Skip to content

Commit 1e5f9d7

Browse files
committed
Workspace merges can now deal with conflicting commits.
1 parent 88db615 commit 1e5f9d7

File tree

4 files changed

+104
-13
lines changed

4 files changed

+104
-13
lines changed

crates/but-testsupport/src/lib.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
//! Utilities for testing.
22
#![deny(missing_docs)]
33

4-
use std::{collections::HashMap, path::Path};
5-
64
use gix::{
75
Repository,
86
bstr::{BStr, ByteSlice},
97
config::tree::Key,
108
};
119
pub use gix_testtools;
10+
use std::io::Write;
11+
use std::{collections::HashMap, path::Path};
1212

1313
mod in_memory_meta;
1414
pub use in_memory_meta::{InMemoryRefMetadata, InMemoryRefMetadataHandle, StackState};
@@ -31,14 +31,41 @@ pub fn hunk_header(old: &str, new: &str) -> ((u32, u32), (u32, u32)) {
3131
(parse_header(old), parse_header(new))
3232
}
3333

34-
/// While `gix` can't (or can't conveniently) do everything, let's make using `git` easier.
34+
/// While `gix` can't (or can't conveniently) do everything, let's make using `git` easier,
35+
/// by producing a command that is anchored to the `gix` repository.
36+
/// Call [`run()`](CommandExt::run) when done configuring its arguments.
3537
pub fn git(repo: &gix::Repository) -> std::process::Command {
3638
let mut cmd = std::process::Command::new(gix::path::env::exe_invocation());
3739
cmd.current_dir(repo.workdir().expect("non-bare"));
3840
isolate_env_std_cmd(&mut cmd);
3941
cmd
4042
}
4143

44+
/// Run the given `script` in bash, with the `cwd` set to the `repo` worktree.
45+
/// Panic if the script fails.
46+
pub fn invoke_bash(script: &str, repo: &gix::Repository) {
47+
let mut cmd = std::process::Command::new("bash");
48+
cmd.current_dir(repo.workdir().expect("non-bare"));
49+
isolate_env_std_cmd(&mut cmd);
50+
cmd.stdin(std::process::Stdio::piped())
51+
.stdout(std::process::Stdio::piped())
52+
.stderr(std::process::Stdio::piped());
53+
let mut child = cmd.spawn().expect("bash can be spawned");
54+
child
55+
.stdin
56+
.as_mut()
57+
.unwrap()
58+
.write_all(script.as_bytes())
59+
.expect("failed to write to stdin");
60+
let out = child.wait_with_output().expect("can wait for output");
61+
assert!(
62+
out.status.success(),
63+
"{cmd:?} failed: {}\n\n{}",
64+
out.stdout.as_bstr(),
65+
out.stderr.as_bstr()
66+
);
67+
}
68+
4269
/// Open a repository at `path` suitable for testing which means that:
4370
///
4471
/// * author and committer are configured, as well as a stable time.

crates/but-workspace/src/commit.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,8 @@ pub mod merge {
365365
}
366366

367367
fn peel_to_tree(commit: gix::Id) -> anyhow::Result<gix::ObjectId> {
368-
Ok(commit.object()?.peel_to_tree()?.id)
368+
let commit = but_core::Commit::from_id(commit)?;
369+
Ok(commit.tree_id_or_auto_resolution()?.detach())
369370
}
370371
}
371372

crates/but-workspace/tests/fixtures/scenario/with-conflict.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
set -eu -o pipefail
44

55
git init
6-
# A repository with a normal and an artificial conflicting commit
6+
echo "A repository with a normal and an artificial conflicting commit" >.git/description
7+
78
echo content >file && git add . && git commit -m "init"
89
git tag normal
910

crates/but-workspace/tests/workspace/commit.rs

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
mod from_new_merge_with_metadata {
2+
use crate::ref_info::with_workspace_commit::utils::{
3+
named_read_only_in_memory_scenario, named_writable_scenario_with_description_and_graph,
4+
};
25
use bstr::ByteSlice;
3-
use but_graph::init::Options;
6+
use but_graph::init::{Options, Overlay};
47
use but_testsupport::{visualize_commit_graph_all, visualize_tree};
58
use but_workspace::WorkspaceCommit;
69
use gix::prelude::ObjectIdExt;
7-
8-
use crate::ref_info::with_workspace_commit::utils::named_read_only_in_memory_scenario;
10+
use gix::refs::Target;
911

1012
#[test]
1113
fn without_conflict_journey() -> anyhow::Result<()> {
@@ -240,9 +242,69 @@ mod from_new_merge_with_metadata {
240242

241243
#[test]
242244
fn with_conflict_commits() -> anyhow::Result<()> {
243-
let (repo, mut meta) = named_read_only_in_memory_scenario("with-conflict", "")?;
244-
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"");
245-
// but_testsupport::git(&repo)
245+
let (_tmp, mut graph, repo, mut meta, _description) =
246+
named_writable_scenario_with_description_and_graph("with-conflict", |_| {})?;
247+
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
248+
* 8450331 (HEAD -> main, tag: conflicted) GitButler WIP Commit
249+
* a047f81 (tag: normal) init
250+
");
251+
but_testsupport::invoke_bash(
252+
r#"
253+
git branch tip-conflicted
254+
git reset --hard @~1
255+
git checkout -b unrelated
256+
touch unrelated-file && git add unrelated-file && git commit -m "unrelated"
257+
"#,
258+
&repo,
259+
);
260+
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
261+
* 8450331 (tag: conflicted, tip-conflicted) GitButler WIP Commit
262+
| * 8ab1c4d (HEAD -> unrelated) unrelated
263+
|/
264+
* a047f81 (tag: normal, main) init
265+
");
266+
267+
let stacks = ["tip-conflicted", "unrelated"];
268+
add_stacks(&mut meta, stacks);
269+
270+
graph = graph.redo_traversal_with_overlay(
271+
&repo,
272+
&meta,
273+
Overlay::default().with_references_if_new([
274+
repo.find_reference("unrelated")?.inner,
275+
// The workspace ref is needed so the workspace and its stacks are iterated as well.
276+
// Algorithms which work with simulation also have to be mindful about this.
277+
gix::refs::Reference {
278+
name: "refs/heads/gitbutler/workspace".try_into()?,
279+
target: Target::Object(repo.rev_parse_single("main")?.detach()),
280+
peeled: None,
281+
},
282+
]),
283+
)?;
284+
285+
let out =
286+
WorkspaceCommit::from_new_merge_with_metadata(&to_stacks(stacks), &graph, &repo, None)?;
287+
insta::assert_debug_snapshot!(out, @r#"
288+
Outcome {
289+
workspace_commit_id: Sha1(ed5a3012c6a4798404f5b8586588d0ede0664683),
290+
stacks: [
291+
Stack { tip: 8450331, name: "tip-conflicted" },
292+
Stack { tip: 8ab1c4d, name: "unrelated" },
293+
],
294+
missing_stacks: [],
295+
conflicting_stacks: [],
296+
}
297+
"#);
298+
299+
// There it auto-resolves the commit to not merge the actual tree structure.
300+
insta::assert_snapshot!(visualize_tree(
301+
out.workspace_commit_id.attach(&repo).object()?.into_commit().tree_id()?
302+
), @r#"
303+
8882acc
304+
├── file:100644:e69de29 ""
305+
└── unrelated-file:100644:e69de29 ""
306+
"#);
307+
246308
Ok(())
247309
}
248310

@@ -303,7 +365,7 @@ mod from_new_merge_with_metadata {
303365
This is a merge commit of the virtual branches in your workspace.
304366
305367
Due to GitButler managing multiple virtual branches, you cannot switch back and
306-
forth between git branches and virtual branches easily.
368+
forth between git branches and virtual branches easily.
307369
308370
If you switch to another branch, GitButler will need to be reinitialized.
309371
If you commit on this branch, GitButler will throw it away.
@@ -366,7 +428,7 @@ mod from_new_merge_with_metadata {
366428
This is a merge commit of the virtual branches in your workspace.
367429
368430
Due to GitButler managing multiple virtual branches, you cannot switch back and
369-
forth between git branches and virtual branches easily.
431+
forth between git branches and virtual branches easily.
370432
371433
If you switch to another branch, GitButler will need to be reinitialized.
372434
If you commit on this branch, GitButler will throw it away.

0 commit comments

Comments
 (0)