Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ chrono = { version = "0.4.42", default-features = false, features = ["std"] }
md5 = "0.8.0"
bitflags = "2.9.4"
notify = "8.2.0"
snapbox = "0.6.22"

[profile.release]
# There are no overrides here as we optimise for fast release builds,
Expand Down
2 changes: 1 addition & 1 deletion crates/but-db/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ bitflags.workspace = true
serde.workspace = true
anyhow.workspace = true
diesel_migrations = { version = "2.3.0", features = ["sqlite"] }
chrono.workspace = true
chrono = { workspace = true, features = ["serde"] }
# other things
tokio = { workspace = true, features = ["rt-multi-thread", "parking_lot", "time", "sync", "macros"] }
tracing.workspace = true
Expand Down
5 changes: 2 additions & 3 deletions crates/but-graph/src/init/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,11 +595,10 @@ pub fn find(
}

/// Returns `([(workspace_tip, workspace_ref_name, workspace_info)], target_refs, desired_refs)` for all available workspace,
/// or exactly one workspace if `maybe_ref_name`.
/// already points to a workspace. That way we can discover the workspace containing any starting point, but only if needed.
/// or exactly one workspace if `maybe_ref_name` has workspace metadata (and only then).
///
/// That way we can discover the workspace containing any starting point, but only if needed.
/// This means we process all workspaces if we aren't currently and clearly looking at a workspace.
///
/// Also prune all non-standard workspaces early, or those that don't have a tip.
#[expect(clippy::type_complexity)]
pub fn obtain_workspace_infos<T: RefMetadata>(
Expand Down
4 changes: 4 additions & 0 deletions crates/but-testsupport/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ rust-version = "1.89"
[lib]
doctest = false

[features]
snapbox = ["dep:snapbox"]

[dependencies]
gix.workspace = true
anyhow.workspace = true
Expand All @@ -17,5 +20,6 @@ gix-testtools.workspace = true
but-graph.workspace = true
but-core.workspace = true
regex = "1.11.3"
snapbox = { workspace = true, optional = true }

[dev-dependencies]
42 changes: 38 additions & 4 deletions crates/but-testsupport/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
//! Utilities for testing.
#![deny(missing_docs)]

use std::{collections::HashMap, path::Path};

use gix::{
Repository,
bstr::{BStr, ByteSlice},
config::tree::Key,
};
pub use gix_testtools;
use std::io::Write;
use std::{collections::HashMap, path::Path};

mod in_memory_meta;
pub use in_memory_meta::{InMemoryRefMetadata, InMemoryRefMetadataHandle, StackState};
Expand All @@ -31,13 +31,41 @@ pub fn hunk_header(old: &str, new: &str) -> ((u32, u32), (u32, u32)) {
(parse_header(old), parse_header(new))
}

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

/// Run the given `script` in bash, with the `cwd` set to the `repo` worktree.
/// Panic if the script fails.
pub fn invoke_bash(script: &str, repo: &gix::Repository) {
let mut cmd = std::process::Command::new("bash");
cmd.current_dir(repo.workdir().expect("non-bare"));
isolate_env_std_cmd(&mut cmd);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn().expect("bash can be spawned");
child
.stdin
.as_mut()
.unwrap()
.write_all(script.as_bytes())
.expect("failed to write to stdin");
let out = child.wait_with_output().expect("can wait for output");
assert!(
out.status.success(),
"{cmd:?} failed: {}\n\n{}",
out.stdout.as_bstr(),
out.stderr.as_bstr()
);
}

/// Open a repository at `path` suitable for testing which means that:
///
/// * author and committer are configured, as well as a stable time.
Expand Down Expand Up @@ -75,7 +103,7 @@ pub fn hex_to_id(hex: &str) -> gix::ObjectId {

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

pub use graph::{graph_tree, graph_workspace};

mod prepare_cmd_env;
pub use prepare_cmd_env::isolate_env_std_cmd;

#[cfg(feature = "snapbox")]
pub use prepare_cmd_env::isolate_snapbox_cmd;
94 changes: 94 additions & 0 deletions crates/but-testsupport/src/prepare_cmd_env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use std::borrow::Cow;
use std::ffi::OsStr;

/// Change the `cmd` environment to be very isolated, particularly when Git is involved.
pub fn isolate_env_std_cmd(cmd: &mut std::process::Command) -> &mut std::process::Command {
for op in updates() {
match op {
EnvOp::Remove(var) => {
cmd.env_remove(var);
}
EnvOp::Add { name, value } => {
cmd.env(name, value);
}
}
}
cmd
}

/// Change the `cmd` environment to be very isolated, particularly when Git is involved.
#[cfg(feature = "snapbox")]
pub fn isolate_snapbox_cmd(mut cmd: snapbox::cmd::Command) -> snapbox::cmd::Command {
for op in updates() {
cmd = match op {
EnvOp::Remove(var) => cmd.env_remove(var),
EnvOp::Add { name, value } => cmd.env(name, value),
};
}
cmd
}

enum EnvOp {
Remove(&'static str),
Add {
name: &'static str,
value: Cow<'static, OsStr>,
},
}

fn updates() -> Vec<EnvOp> {
// Copied from gix-testtools/lib.rs, in an attempt to isolate everything as good as possible,
#[cfg(windows)]
const NULL_DEVICE: &str = "NUL";
#[cfg(not(windows))]
const NULL_DEVICE: &str = "/dev/null";

// particularly mutation.
let mut msys_for_git_bash_on_windows = std::env::var_os("MSYS").unwrap_or_default();
msys_for_git_bash_on_windows.push(" winsymlinks:nativestrict");
[
EnvOp::Remove("GIT_DIR"),
EnvOp::Remove("GIT_INDEX_FILE"),
EnvOp::Remove("GIT_OBJECT_DIRECTORY"),
EnvOp::Remove("GIT_ALTERNATE_OBJECT_DIRECTORIES"),
EnvOp::Remove("GIT_WORK_TREE"),
EnvOp::Remove("GIT_COMMON_DIR"),
EnvOp::Remove("GIT_ASKPASS"),
EnvOp::Remove("SSH_ASKPASS"),
]
.into_iter()
.chain(
[
("GIT_CONFIG_NOSYSTEM", "1"),
("GIT_CONFIG_GLOBAL", NULL_DEVICE),
("GIT_TERMINAL_PROMPT", "false"),
("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000"),
("GIT_AUTHOR_EMAIL", "[email protected]"),
("GIT_AUTHOR_NAME", "author"),
("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000"),
("GIT_COMMITTER_EMAIL", "[email protected]"),
("GIT_COMMITTER_NAME", "committer"),
("GIT_CONFIG_COUNT", "4"),
("GIT_CONFIG_KEY_0", "commit.gpgsign"),
("GIT_CONFIG_VALUE_0", "false"),
("GIT_CONFIG_KEY_1", "tag.gpgsign"),
("GIT_CONFIG_VALUE_1", "false"),
("GIT_CONFIG_KEY_2", "init.defaultBranch"),
("GIT_CONFIG_VALUE_2", "main"),
("GIT_CONFIG_KEY_3", "protocol.file.allow"),
("GIT_CONFIG_VALUE_3", "always"),
("CLICOLOR_FORCE", "1"),
("RUST_BACKTRACE", "0"),
]
.into_iter()
.map(|(name, value)| EnvOp::Add {
name,
value: Cow::Borrowed(OsStr::new(value)),
}),
)
.chain(Some(EnvOp::Add {
name: "MSYS",
value: Cow::Owned(msys_for_git_bash_on_windows),
}))
.collect()
}
3 changes: 2 additions & 1 deletion crates/but-workspace/src/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,8 @@ pub mod merge {
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
set -eu -o pipefail

git init
# A repository with a normal and an artificial conflicting commit
echo "A repository with a normal and an artificial conflicting commit" >.git/description

echo content >file && git add . && git commit -m "init"
git tag normal

Expand Down
76 changes: 73 additions & 3 deletions crates/but-workspace/tests/workspace/commit.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
mod from_new_merge_with_metadata {
use crate::ref_info::with_workspace_commit::utils::{
named_read_only_in_memory_scenario, named_writable_scenario_with_description_and_graph,
};
use bstr::ByteSlice;
use but_graph::init::Options;
use but_graph::init::{Options, Overlay};
use but_testsupport::{visualize_commit_graph_all, visualize_tree};
use but_workspace::WorkspaceCommit;
use gix::prelude::ObjectIdExt;

use crate::ref_info::with_workspace_commit::utils::named_read_only_in_memory_scenario;
use gix::refs::Target;

#[test]
fn without_conflict_journey() -> anyhow::Result<()> {
Expand Down Expand Up @@ -238,6 +240,74 @@ mod from_new_merge_with_metadata {
Ok(())
}

#[test]
fn with_conflict_commits() -> anyhow::Result<()> {
let (_tmp, mut graph, repo, mut meta, _description) =
named_writable_scenario_with_description_and_graph("with-conflict", |_| {})?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
* 8450331 (HEAD -> main, tag: conflicted) GitButler WIP Commit
* a047f81 (tag: normal) init
");
but_testsupport::invoke_bash(
r#"
git branch tip-conflicted
git reset --hard @~1
git checkout -b unrelated
touch unrelated-file && git add unrelated-file && git commit -m "unrelated"
"#,
&repo,
);
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
* 8450331 (tag: conflicted, tip-conflicted) GitButler WIP Commit
| * 8ab1c4d (HEAD -> unrelated) unrelated
|/
* a047f81 (tag: normal, main) init
");

let stacks = ["tip-conflicted", "unrelated"];
add_stacks(&mut meta, stacks);

graph = graph.redo_traversal_with_overlay(
&repo,
&meta,
Overlay::default().with_references_if_new([
repo.find_reference("unrelated")?.inner,
// The workspace ref is needed so the workspace and its stacks are iterated as well.
// Algorithms which work with simulation also have to be mindful about this.
gix::refs::Reference {
name: "refs/heads/gitbutler/workspace".try_into()?,
target: Target::Object(repo.rev_parse_single("main")?.detach()),
peeled: None,
},
]),
)?;

let out =
WorkspaceCommit::from_new_merge_with_metadata(&to_stacks(stacks), &graph, &repo, None)?;
insta::assert_debug_snapshot!(out, @r#"
Outcome {
workspace_commit_id: Sha1(ed5a3012c6a4798404f5b8586588d0ede0664683),
stacks: [
Stack { tip: 8450331, name: "tip-conflicted" },
Stack { tip: 8ab1c4d, name: "unrelated" },
],
missing_stacks: [],
conflicting_stacks: [],
}
"#);

// There it auto-resolves the commit to not merge the actual tree structure.
insta::assert_snapshot!(visualize_tree(
out.workspace_commit_id.attach(&repo).object()?.into_commit().tree_id()?
), @r#"
8882acc
├── file:100644:e69de29 ""
└── unrelated-file:100644:e69de29 ""
"#);

Ok(())
}

#[test]
fn with_conflict_journey() -> anyhow::Result<()> {
let (repo, mut meta) =
Expand Down
4 changes: 2 additions & 2 deletions crates/but/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ tracing-subscriber = { version = "0.3", features = [
tracing-forest = { version = "0.2.0" }

[dev-dependencies]
but-testsupport.workspace = true
snapbox = { version = "0.6.22", features = ["term-svg", "regex"] }
but-testsupport = { workspace = true, features = ["snapbox"] }
snapbox = { workspace = true, features = ["term-svg", "regex"] }
shell-words = "1.1.0"
insta.workspace = true
39 changes: 1 addition & 38 deletions crates/but/tests/but/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,44 +227,7 @@ impl Sandbox {
}

fn with_updated_env(&self, cmd: snapbox::cmd::Command) -> snapbox::cmd::Command {
// Copied from gix-testtools/lib.rs, in an attempt to isolate everything as good as possible,
#[cfg(windows)]
const NULL_DEVICE: &str = "NUL";
#[cfg(not(windows))]
const NULL_DEVICE: &str = "/dev/null";

// particularly mutation.
let mut msys_for_git_bash_on_windows = env::var_os("MSYS").unwrap_or_default();
msys_for_git_bash_on_windows.push(" winsymlinks:nativestrict");
cmd.env_remove("GIT_DIR")
.env_remove("GIT_INDEX_FILE")
.env_remove("GIT_OBJECT_DIRECTORY")
.env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_COMMON_DIR")
.env_remove("GIT_ASKPASS")
.env_remove("SSH_ASKPASS")
.env("MSYS", msys_for_git_bash_on_windows)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", NULL_DEVICE)
.env("GIT_TERMINAL_PROMPT", "false")
.env("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000")
.env("GIT_AUTHOR_EMAIL", "[email protected]")
.env("GIT_AUTHOR_NAME", "author")
.env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000")
.env("GIT_COMMITTER_EMAIL", "[email protected]")
.env("GIT_COMMITTER_NAME", "committer")
.env("GIT_CONFIG_COUNT", "4")
.env("GIT_CONFIG_KEY_0", "commit.gpgsign")
.env("GIT_CONFIG_VALUE_0", "false")
.env("GIT_CONFIG_KEY_1", "tag.gpgsign")
.env("GIT_CONFIG_VALUE_1", "false")
.env("GIT_CONFIG_KEY_2", "init.defaultBranch")
.env("GIT_CONFIG_VALUE_2", "main")
.env("GIT_CONFIG_KEY_3", "protocol.file.allow")
.env("GIT_CONFIG_VALUE_3", "always")
.env("CLICOLOR_FORCE", "1")
.env("RUST_BACKTRACE", "0")
but_testsupport::isolate_snapbox_cmd(cmd)
.env("E2E_TEST_APP_DATA_DIR", self.app_root())
.current_dir(self.projects_root())
}
Expand Down
Loading