Skip to content

Commit 1ba063f

Browse files
authored
Merge pull request #10711 from Byron/next
v3 apply: finishing touches?
2 parents f70ef7b + 18f80c0 commit 1ba063f

File tree

12 files changed

+221
-53
lines changed

12 files changed

+221
-53
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.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ chrono = { version = "0.4.42", default-features = false, features = ["std"] }
105105
md5 = "0.8.0"
106106
bitflags = "2.9.4"
107107
notify = "8.2.0"
108+
snapbox = "0.6.22"
108109

109110
[profile.release]
110111
# There are no overrides here as we optimise for fast release builds,

crates/but-db/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ bitflags.workspace = true
2222
serde.workspace = true
2323
anyhow.workspace = true
2424
diesel_migrations = { version = "2.3.0", features = ["sqlite"] }
25-
chrono.workspace = true
25+
chrono = { workspace = true, features = ["serde"] }
2626
# other things
2727
tokio = { workspace = true, features = ["rt-multi-thread", "parking_lot", "time", "sync", "macros"] }
2828
tracing.workspace = true

crates/but-graph/src/init/walk.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -595,11 +595,10 @@ pub fn find(
595595
}
596596

597597
/// Returns `([(workspace_tip, workspace_ref_name, workspace_info)], target_refs, desired_refs)` for all available workspace,
598-
/// or exactly one workspace if `maybe_ref_name`.
599-
/// already points to a workspace. That way we can discover the workspace containing any starting point, but only if needed.
598+
/// or exactly one workspace if `maybe_ref_name` has workspace metadata (and only then).
600599
///
600+
/// That way we can discover the workspace containing any starting point, but only if needed.
601601
/// This means we process all workspaces if we aren't currently and clearly looking at a workspace.
602-
///
603602
/// Also prune all non-standard workspaces early, or those that don't have a tip.
604603
#[expect(clippy::type_complexity)]
605604
pub fn obtain_workspace_infos<T: RefMetadata>(

crates/but-testsupport/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ rust-version = "1.89"
99
[lib]
1010
doctest = false
1111

12+
[features]
13+
snapbox = ["dep:snapbox"]
14+
1215
[dependencies]
1316
gix.workspace = true
1417
anyhow.workspace = true
@@ -17,5 +20,6 @@ gix-testtools.workspace = true
1720
but-graph.workspace = true
1821
but-core.workspace = true
1922
regex = "1.11.3"
23+
snapbox = { workspace = true, optional = true }
2024

2125
[dev-dependencies]

crates/but-testsupport/src/lib.rs

Lines changed: 38 additions & 4 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,13 +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"));
40+
isolate_env_std_cmd(&mut cmd);
3841
cmd
3942
}
4043

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+
4169
/// Open a repository at `path` suitable for testing which means that:
4270
///
4371
/// * author and committer are configured, as well as a stable time.
@@ -75,7 +103,7 @@ pub fn hex_to_id(hex: &str) -> gix::ObjectId {
75103

76104
/// Sets and environment that assures commits are reproducible.
77105
/// This needs the `testing` feature enabled in `but-core` as well to work.
78-
/// This changes the process environment, be aware.
106+
/// **This changes the process environment, be aware.**
79107
pub fn assure_stable_env() {
80108
let env = gix_testtools::Env::new()
81109
// TODO(gix): once everything is ported, all these can be configured on `gix::Repository`.
@@ -319,3 +347,9 @@ pub fn debug_str(input: &dyn std::fmt::Debug) -> String {
319347
mod graph;
320348

321349
pub use graph::{graph_tree, graph_workspace};
350+
351+
mod prepare_cmd_env;
352+
pub use prepare_cmd_env::isolate_env_std_cmd;
353+
354+
#[cfg(feature = "snapbox")]
355+
pub use prepare_cmd_env::isolate_snapbox_cmd;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use std::borrow::Cow;
2+
use std::ffi::OsStr;
3+
4+
/// Change the `cmd` environment to be very isolated, particularly when Git is involved.
5+
pub fn isolate_env_std_cmd(cmd: &mut std::process::Command) -> &mut std::process::Command {
6+
for op in updates() {
7+
match op {
8+
EnvOp::Remove(var) => {
9+
cmd.env_remove(var);
10+
}
11+
EnvOp::Add { name, value } => {
12+
cmd.env(name, value);
13+
}
14+
}
15+
}
16+
cmd
17+
}
18+
19+
/// Change the `cmd` environment to be very isolated, particularly when Git is involved.
20+
#[cfg(feature = "snapbox")]
21+
pub fn isolate_snapbox_cmd(mut cmd: snapbox::cmd::Command) -> snapbox::cmd::Command {
22+
for op in updates() {
23+
cmd = match op {
24+
EnvOp::Remove(var) => cmd.env_remove(var),
25+
EnvOp::Add { name, value } => cmd.env(name, value),
26+
};
27+
}
28+
cmd
29+
}
30+
31+
enum EnvOp {
32+
Remove(&'static str),
33+
Add {
34+
name: &'static str,
35+
value: Cow<'static, OsStr>,
36+
},
37+
}
38+
39+
fn updates() -> Vec<EnvOp> {
40+
// Copied from gix-testtools/lib.rs, in an attempt to isolate everything as good as possible,
41+
#[cfg(windows)]
42+
const NULL_DEVICE: &str = "NUL";
43+
#[cfg(not(windows))]
44+
const NULL_DEVICE: &str = "/dev/null";
45+
46+
// particularly mutation.
47+
let mut msys_for_git_bash_on_windows = std::env::var_os("MSYS").unwrap_or_default();
48+
msys_for_git_bash_on_windows.push(" winsymlinks:nativestrict");
49+
[
50+
EnvOp::Remove("GIT_DIR"),
51+
EnvOp::Remove("GIT_INDEX_FILE"),
52+
EnvOp::Remove("GIT_OBJECT_DIRECTORY"),
53+
EnvOp::Remove("GIT_ALTERNATE_OBJECT_DIRECTORIES"),
54+
EnvOp::Remove("GIT_WORK_TREE"),
55+
EnvOp::Remove("GIT_COMMON_DIR"),
56+
EnvOp::Remove("GIT_ASKPASS"),
57+
EnvOp::Remove("SSH_ASKPASS"),
58+
]
59+
.into_iter()
60+
.chain(
61+
[
62+
("GIT_CONFIG_NOSYSTEM", "1"),
63+
("GIT_CONFIG_GLOBAL", NULL_DEVICE),
64+
("GIT_TERMINAL_PROMPT", "false"),
65+
("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000"),
66+
("GIT_AUTHOR_EMAIL", "[email protected]"),
67+
("GIT_AUTHOR_NAME", "author"),
68+
("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000"),
69+
("GIT_COMMITTER_EMAIL", "[email protected]"),
70+
("GIT_COMMITTER_NAME", "committer"),
71+
("GIT_CONFIG_COUNT", "4"),
72+
("GIT_CONFIG_KEY_0", "commit.gpgsign"),
73+
("GIT_CONFIG_VALUE_0", "false"),
74+
("GIT_CONFIG_KEY_1", "tag.gpgsign"),
75+
("GIT_CONFIG_VALUE_1", "false"),
76+
("GIT_CONFIG_KEY_2", "init.defaultBranch"),
77+
("GIT_CONFIG_VALUE_2", "main"),
78+
("GIT_CONFIG_KEY_3", "protocol.file.allow"),
79+
("GIT_CONFIG_VALUE_3", "always"),
80+
("CLICOLOR_FORCE", "1"),
81+
("RUST_BACKTRACE", "0"),
82+
]
83+
.into_iter()
84+
.map(|(name, value)| EnvOp::Add {
85+
name,
86+
value: Cow::Borrowed(OsStr::new(value)),
87+
}),
88+
)
89+
.chain(Some(EnvOp::Add {
90+
name: "MSYS",
91+
value: Cow::Owned(msys_for_git_bash_on_windows),
92+
}))
93+
.collect()
94+
}

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: 73 additions & 3 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<()> {
@@ -238,6 +240,74 @@ mod from_new_merge_with_metadata {
238240
Ok(())
239241
}
240242

243+
#[test]
244+
fn with_conflict_commits() -> anyhow::Result<()> {
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+
308+
Ok(())
309+
}
310+
241311
#[test]
242312
fn with_conflict_journey() -> anyhow::Result<()> {
243313
let (repo, mut meta) =

0 commit comments

Comments
 (0)