Replace Git subprocesses with native APIs
This commit is contained in:
84
Cargo.lock
generated
84
Cargo.lock
generated
@@ -1166,6 +1166,7 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
|
"git2",
|
||||||
"gix",
|
"gix",
|
||||||
"iced",
|
"iced",
|
||||||
"image",
|
"image",
|
||||||
@@ -1761,6 +1762,21 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "git2"
|
||||||
|
version = "0.20.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.1",
|
||||||
|
"libc",
|
||||||
|
"libgit2-sys",
|
||||||
|
"log",
|
||||||
|
"openssl-probe",
|
||||||
|
"openssl-sys",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gix"
|
name = "gix"
|
||||||
version = "0.86.0"
|
version = "0.86.0"
|
||||||
@@ -3474,6 +3490,20 @@ version = "0.2.186"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libgit2-sys"
|
||||||
|
version = "0.18.7+1.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23c7391e4b9f4ffab1a624223cc1d7385ff9a678f490768add717de7ea2f4d89"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"libssh2-sys",
|
||||||
|
"libz-sys",
|
||||||
|
"openssl-sys",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -3512,6 +3542,20 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libssh2-sys"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c04141a07bb0c0bc461cb657808764de571702a59bc5c726c400ac9a7625e3ab"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"libz-sys",
|
||||||
|
"openssl-sys",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libxdo"
|
name = "libxdo"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -3537,6 +3581,18 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2e126dda6f34391ab7b444f9922055facc83c07a910da3eb16f1e4d9c45dc777"
|
checksum = "2e126dda6f34391ab7b444f9922055facc83c07a910da3eb16f1e4d9c45dc777"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libz-sys"
|
||||||
|
version = "1.1.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lilt"
|
name = "lilt"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -4208,6 +4264,34 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-probe"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-src"
|
||||||
|
version = "300.6.1+3.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"openssl-src",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ cc = "1.3.0"
|
|||||||
diesel = { version = "2.3.11", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "64-column-tables"] }
|
diesel = { version = "2.3.11", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "64-column-tables"] }
|
||||||
diesel_migrations = "2.3.2"
|
diesel_migrations = "2.3.2"
|
||||||
gix = { version = "0.86.0", default-features = false, features = ["max-performance-safe", "sha1", "status"] }
|
gix = { version = "0.86.0", default-features = false, features = ["max-performance-safe", "sha1", "status"] }
|
||||||
|
git2 = { version = "0.20.4", features = ["vendored-libgit2", "vendored-openssl"] }
|
||||||
iced = { version = "0.14.0", features = ["advanced", "highlighter", "image-without-codecs", "markdown", "svg", "tokio"] }
|
iced = { version = "0.14.0", features = ["advanced", "highlighter", "image-without-codecs", "markdown", "svg", "tokio"] }
|
||||||
image = { version = "0.25.10", default-features = false, features = ["gif", "jpeg", "png", "webp"] }
|
image = { version = "0.25.10", default-features = false, features = ["gif", "jpeg", "png", "webp"] }
|
||||||
memmap2 = "0.9.11"
|
memmap2 = "0.9.11"
|
||||||
|
|||||||
403
src/app/git.rs
403
src/app/git.rs
@@ -1,6 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::process::{Command, Output};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub(super) enum GitChangeKind {
|
pub(super) enum GitChangeKind {
|
||||||
@@ -255,12 +254,12 @@ impl App {
|
|||||||
let selected = self.selected_git_paths(project_id);
|
let selected = self.selected_git_paths(project_id);
|
||||||
if !selected.is_empty() {
|
if !selected.is_empty() {
|
||||||
self.start_git_operation(project_id, "Committing selected files", true, move || {
|
self.start_git_operation(project_id, "Committing selected files", true, move || {
|
||||||
run_git_paths(&root, &["add", "--"], &selected)?;
|
stage_paths(&root, &selected)?;
|
||||||
run_git_commit_paths(&root, &message, &selected)
|
commit_paths(&root, &message, &selected)
|
||||||
});
|
});
|
||||||
} else if worktree.files.iter().any(|file| file.staged_kind.is_some()) {
|
} else if worktree.files.iter().any(|file| file.staged_kind.is_some()) {
|
||||||
self.start_git_operation(project_id, "Committing staged changes", true, move || {
|
self.start_git_operation(project_id, "Committing staged changes", true, move || {
|
||||||
run_git_args(&root, &["commit", "-m", &message]).map(|_| ())
|
commit_index(&root, &message)
|
||||||
});
|
});
|
||||||
} else if worktree.files.is_empty() {
|
} else if worktree.files.is_empty() {
|
||||||
self.error = Some("There are no changes to commit.".into());
|
self.error = Some("There are no changes to commit.".into());
|
||||||
@@ -280,8 +279,8 @@ impl App {
|
|||||||
let root = worktree.root.clone();
|
let root = worktree.root.clone();
|
||||||
self.git_commit_all_confirmation = false;
|
self.git_commit_all_confirmation = false;
|
||||||
self.start_git_operation(project_id, "Committing all changes", true, move || {
|
self.start_git_operation(project_id, "Committing all changes", true, move || {
|
||||||
run_git_args(&root, &["add", "--all"])?;
|
stage_all(&root)?;
|
||||||
run_git_args(&root, &["commit", "-m", &message]).map(|_| ())
|
commit_index(&root, &message)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,105 +666,253 @@ fn diff_range_start(range: &str) -> Option<u32> {
|
|||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_has_head(root: &Path) -> bool {
|
fn stage_paths(root: &Path, paths: &[PathBuf]) -> Result<(), String> {
|
||||||
Command::new("git")
|
let repository = git2::Repository::discover(root).map_err(git_error("stage files"))?;
|
||||||
.arg("-C")
|
let mut index = repository
|
||||||
.arg(root)
|
.index()
|
||||||
.args(["rev-parse", "--verify", "HEAD"])
|
.map_err(git_error("open the Git index"))?;
|
||||||
.output()
|
for path in paths {
|
||||||
.is_ok_and(|output| output.status.success())
|
match root.join(path).symlink_metadata() {
|
||||||
|
Ok(_) => index.add_path(path).map_err(git_error("stage files"))?,
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
index.remove_path(path).map_err(git_error("stage files"))?
|
||||||
|
}
|
||||||
|
Err(error) => return Err(format!("Could not stage files: {error}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index.write().map_err(git_error("write the Git index"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stage_paths(root: &Path, paths: &[PathBuf]) -> Result<(), String> {
|
fn stage_all(root: &Path) -> Result<(), String> {
|
||||||
run_git_paths(root, &["add", "--"], paths).map(|_| ())
|
let repository = git2::Repository::discover(root).map_err(git_error("stage files"))?;
|
||||||
|
let mut index = repository
|
||||||
|
.index()
|
||||||
|
.map_err(git_error("open the Git index"))?;
|
||||||
|
index
|
||||||
|
.add_all(["*"], git2::IndexAddOption::DEFAULT, None)
|
||||||
|
.map_err(git_error("stage files"))?;
|
||||||
|
index.write().map_err(git_error("write the Git index"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unstage_paths(root: &Path, paths: &[PathBuf]) -> Result<(), String> {
|
fn unstage_paths(root: &Path, paths: &[PathBuf]) -> Result<(), String> {
|
||||||
if git_has_head(root) {
|
let repository = git2::Repository::discover(root).map_err(git_error("unstage files"))?;
|
||||||
run_git_paths(root, &["restore", "--staged", "--"], paths)?;
|
let head = repository
|
||||||
} else {
|
.head()
|
||||||
run_git_paths(
|
.ok()
|
||||||
root,
|
.and_then(|head| head.peel_to_commit().ok());
|
||||||
&["rm", "--cached", "--quiet", "--ignore-unmatch", "--"],
|
repository
|
||||||
paths,
|
.reset_default(head.as_ref().map(|commit| commit.as_object()), paths.iter())
|
||||||
)?;
|
.map_err(git_error("unstage files"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit_paths(root: &Path, message: &str, paths: &[PathBuf]) -> Result<(), String> {
|
||||||
|
let repository =
|
||||||
|
git2::Repository::discover(root).map_err(git_error("commit selected files"))?;
|
||||||
|
let index = repository
|
||||||
|
.index()
|
||||||
|
.map_err(git_error("open the Git index"))?;
|
||||||
|
let mut commit_index = git2::Index::new().map_err(git_error("create a Git index"))?;
|
||||||
|
if let Ok(head) = repository.head().and_then(|head| head.peel_to_commit()) {
|
||||||
|
commit_index
|
||||||
|
.read_tree(&head.tree().map_err(git_error("read the HEAD tree"))?)
|
||||||
|
.map_err(git_error("read the HEAD tree"))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
for path in paths {
|
||||||
|
if let Some(entry) = index.get_path(path, 0) {
|
||||||
|
commit_index
|
||||||
|
.add(&entry)
|
||||||
|
.map_err(git_error("prepare the commit"))?;
|
||||||
|
} else {
|
||||||
|
commit_index
|
||||||
|
.remove_path(path)
|
||||||
|
.map_err(git_error("prepare the commit"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tree = commit_index
|
||||||
|
.write_tree_to(&repository)
|
||||||
|
.map_err(git_error("write the commit tree"))?;
|
||||||
|
let commit = create_commit(&repository, message, tree)?;
|
||||||
|
let commit = repository
|
||||||
|
.find_object(commit, None)
|
||||||
|
.map_err(git_error("read the new commit"))?;
|
||||||
|
repository
|
||||||
|
.reset_default(Some(&commit), paths.iter())
|
||||||
|
.map_err(git_error("refresh the Git index"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_git_commit_paths(root: &Path, message: &str, paths: &[PathBuf]) -> Result<(), String> {
|
fn commit_index(root: &Path, message: &str) -> Result<(), String> {
|
||||||
let mut command = git_command(root);
|
let repository = git2::Repository::discover(root).map_err(git_error("commit changes"))?;
|
||||||
command.args(["commit", "-m", message, "--"]);
|
let tree = repository
|
||||||
command.args(paths);
|
.index()
|
||||||
finish_git(command.output(), "commit selected files").map(|_| ())
|
.and_then(|mut index| index.write_tree())
|
||||||
|
.map_err(git_error("write the commit tree"))?;
|
||||||
|
create_commit(&repository, message, tree).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_git_paths(root: &Path, arguments: &[&str], paths: &[PathBuf]) -> Result<String, String> {
|
fn create_commit(
|
||||||
let mut command = git_command(root);
|
repository: &git2::Repository,
|
||||||
command.args(arguments);
|
message: &str,
|
||||||
command.args(paths);
|
tree: git2::Oid,
|
||||||
finish_git(
|
) -> Result<git2::Oid, String> {
|
||||||
command.output(),
|
let signature = repository
|
||||||
arguments.first().copied().unwrap_or("run Git"),
|
.signature()
|
||||||
)
|
.map_err(git_error("read Git identity"))?;
|
||||||
}
|
let tree = repository
|
||||||
|
.find_tree(tree)
|
||||||
fn run_git_args(root: &Path, arguments: &[&str]) -> Result<String, String> {
|
.map_err(git_error("read the commit tree"))?;
|
||||||
let mut command = git_command(root);
|
let parent = repository
|
||||||
command.args(arguments);
|
.head()
|
||||||
finish_git(
|
.ok()
|
||||||
command.output(),
|
.and_then(|head| head.peel_to_commit().ok());
|
||||||
arguments.first().copied().unwrap_or("run Git"),
|
let parents = parent.iter().collect::<Vec<_>>();
|
||||||
)
|
repository
|
||||||
|
.commit(
|
||||||
|
Some("HEAD"),
|
||||||
|
&signature,
|
||||||
|
&signature,
|
||||||
|
message,
|
||||||
|
&tree,
|
||||||
|
&parents,
|
||||||
|
)
|
||||||
|
.map_err(git_error("commit changes"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_git_remote(root: &Path, action: &str, branch: Option<&str>) -> Result<(), String> {
|
fn run_git_remote(root: &Path, action: &str, branch: Option<&str>) -> Result<(), String> {
|
||||||
let mut command = git_command(root);
|
let repository =
|
||||||
|
git2::Repository::discover(root).map_err(|error| format!("Could not {action}: {error}"))?;
|
||||||
|
let mut remote = repository
|
||||||
|
.find_remote("origin")
|
||||||
|
.map_err(|error| format!("Could not {action}: {error}"))?;
|
||||||
match action {
|
match action {
|
||||||
"fetch" => {
|
"fetch" => {
|
||||||
command.args(["fetch", "origin"]);
|
let mut options = git2::FetchOptions::new();
|
||||||
|
options.remote_callbacks(remote_callbacks(&repository)?);
|
||||||
|
remote
|
||||||
|
.fetch(&[] as &[&str], Some(&mut options), None)
|
||||||
|
.map_err(git_error("fetch"))
|
||||||
}
|
}
|
||||||
"pull" => {
|
"pull" => {
|
||||||
command.args([
|
let branch = branch.ok_or_else(|| "The current branch is unavailable.".to_owned())?;
|
||||||
"pull",
|
let mut options = git2::FetchOptions::new();
|
||||||
"--no-rebase",
|
options.remote_callbacks(remote_callbacks(&repository)?);
|
||||||
"origin",
|
remote
|
||||||
branch.ok_or_else(|| "The current branch is unavailable.".to_owned())?,
|
.fetch(&[branch], Some(&mut options), None)
|
||||||
]);
|
.map_err(git_error("pull"))?;
|
||||||
|
merge_fetch_head(&repository, branch)
|
||||||
}
|
}
|
||||||
"push" => {
|
"push" => {
|
||||||
command.args([
|
let branch = branch.ok_or_else(|| "The current branch is unavailable.".to_owned())?;
|
||||||
"push",
|
let refspec = format!("refs/heads/{branch}:refs/heads/{branch}");
|
||||||
"origin",
|
let mut options = git2::PushOptions::new();
|
||||||
&format!(
|
options.remote_callbacks(remote_callbacks(&repository)?);
|
||||||
"HEAD:{}",
|
remote
|
||||||
branch.ok_or_else(|| "The current branch is unavailable.".to_owned())?
|
.push(&[&refspec], Some(&mut options))
|
||||||
),
|
.map_err(git_error("push"))
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
_ => return Err("The Git remote action is unavailable.".into()),
|
_ => Err("The Git remote action is unavailable.".into()),
|
||||||
}
|
}
|
||||||
finish_git(command.output(), action).map(|_| ())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_command(root: &Path) -> Command {
|
fn merge_fetch_head(repository: &git2::Repository, branch: &str) -> Result<(), String> {
|
||||||
let mut command = Command::new("git");
|
let fetch_head = repository
|
||||||
command.arg("-C").arg(root).env("GIT_TERMINAL_PROMPT", "0");
|
.find_reference("FETCH_HEAD")
|
||||||
command
|
.and_then(|reference| repository.reference_to_annotated_commit(&reference))
|
||||||
|
.map_err(git_error("read the fetched branch"))?;
|
||||||
|
let (analysis, _) = repository
|
||||||
|
.merge_analysis(&[&fetch_head])
|
||||||
|
.map_err(git_error("analyze the pull"))?;
|
||||||
|
if analysis.is_up_to_date() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if analysis.is_fast_forward() || analysis.is_unborn() {
|
||||||
|
let object = repository
|
||||||
|
.find_object(fetch_head.id(), None)
|
||||||
|
.map_err(git_error("read the fetched commit"))?;
|
||||||
|
repository
|
||||||
|
.checkout_tree(&object, Some(git2::build::CheckoutBuilder::new().safe()))
|
||||||
|
.map_err(git_error("check out the fetched commit"))?;
|
||||||
|
let reference = format!("refs/heads/{branch}");
|
||||||
|
repository
|
||||||
|
.reference(&reference, fetch_head.id(), true, "pull: fast-forward")
|
||||||
|
.and_then(|_| repository.set_head(&reference))
|
||||||
|
.map_err(git_error("fast-forward the current branch"))?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if !analysis.is_normal() {
|
||||||
|
return Err("Could not pull: the fetched branch cannot be merged.".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let local = repository
|
||||||
|
.head()
|
||||||
|
.and_then(|head| head.peel_to_commit())
|
||||||
|
.map_err(git_error("read the current commit"))?;
|
||||||
|
repository
|
||||||
|
.merge(
|
||||||
|
&[&fetch_head],
|
||||||
|
None,
|
||||||
|
Some(git2::build::CheckoutBuilder::new().safe()),
|
||||||
|
)
|
||||||
|
.map_err(git_error("merge the fetched branch"))?;
|
||||||
|
let mut index = repository
|
||||||
|
.index()
|
||||||
|
.map_err(git_error("read the merge index"))?;
|
||||||
|
if index.has_conflicts() {
|
||||||
|
return Err("Could not pull: the merge has conflicts.".into());
|
||||||
|
}
|
||||||
|
let tree = index
|
||||||
|
.write_tree()
|
||||||
|
.and_then(|id| repository.find_tree(id))
|
||||||
|
.map_err(git_error("write the merge tree"))?;
|
||||||
|
let remote = repository
|
||||||
|
.find_commit(fetch_head.id())
|
||||||
|
.map_err(git_error("read the fetched commit"))?;
|
||||||
|
let signature = repository
|
||||||
|
.signature()
|
||||||
|
.map_err(git_error("read Git identity"))?;
|
||||||
|
repository
|
||||||
|
.commit(
|
||||||
|
Some("HEAD"),
|
||||||
|
&signature,
|
||||||
|
&signature,
|
||||||
|
&format!("Merge remote-tracking branch 'origin/{branch}'"),
|
||||||
|
&tree,
|
||||||
|
&[&local, &remote],
|
||||||
|
)
|
||||||
|
.map_err(git_error("commit the merge"))?;
|
||||||
|
repository
|
||||||
|
.cleanup_state()
|
||||||
|
.map_err(git_error("finish the pull"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_git(output: std::io::Result<Output>, action: &str) -> Result<String, String> {
|
fn remote_callbacks(
|
||||||
let output = output.map_err(|error| format!("Could not {action}: {error}"))?;
|
repository: &git2::Repository,
|
||||||
if output.status.success() {
|
) -> Result<git2::RemoteCallbacks<'static>, String> {
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
let config = repository
|
||||||
} else {
|
.config()
|
||||||
let detail = String::from_utf8_lossy(&output.stderr).trim().to_owned();
|
.map_err(git_error("read Git configuration"))?;
|
||||||
Err(if detail.is_empty() {
|
let mut callbacks = git2::RemoteCallbacks::new();
|
||||||
format!("Could not {action}.")
|
callbacks.credentials(move |url, username, allowed| {
|
||||||
} else {
|
if allowed.contains(git2::CredentialType::SSH_KEY) {
|
||||||
format!("Could not {action}: {detail}")
|
return git2::Cred::ssh_key_from_agent(username.unwrap_or("git"));
|
||||||
})
|
}
|
||||||
}
|
if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
|
||||||
|
return git2::Cred::credential_helper(&config, url, username);
|
||||||
|
}
|
||||||
|
if allowed.contains(git2::CredentialType::USERNAME) {
|
||||||
|
return git2::Cred::username(username.unwrap_or("git"));
|
||||||
|
}
|
||||||
|
if allowed.contains(git2::CredentialType::DEFAULT) {
|
||||||
|
return git2::Cred::default();
|
||||||
|
}
|
||||||
|
Err(git2::Error::from_str(
|
||||||
|
"no supported Git credentials are available",
|
||||||
|
))
|
||||||
|
});
|
||||||
|
Ok(callbacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn git_error(action: &'static str) -> impl FnOnce(git2::Error) -> String {
|
||||||
|
move |error| format!("Could not {action}: {error}")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -780,9 +927,14 @@ mod tests {
|
|||||||
fn gitoxide_status_merges_staged_and_worktree_changes() {
|
fn gitoxide_status_merges_staged_and_worktree_changes() {
|
||||||
let directory = repository_fixture();
|
let directory = repository_fixture();
|
||||||
std::fs::write(directory.join("added.txt"), "added\n").unwrap();
|
std::fs::write(directory.join("added.txt"), "added\n").unwrap();
|
||||||
run_git_args(&directory, &["config", "status.showUntrackedFiles", "no"]).unwrap();
|
git2::Repository::open(&directory)
|
||||||
|
.unwrap()
|
||||||
|
.config()
|
||||||
|
.unwrap()
|
||||||
|
.set_bool("status.showUntrackedFiles", false)
|
||||||
|
.unwrap();
|
||||||
std::fs::write(directory.join("changed.txt"), "changed\n").unwrap();
|
std::fs::write(directory.join("changed.txt"), "changed\n").unwrap();
|
||||||
run_git_args(&directory, &["add", "changed.txt"]).unwrap();
|
stage_paths(&directory, &["changed.txt".into()]).unwrap();
|
||||||
std::fs::write(directory.join("changed.txt"), "changed again\n").unwrap();
|
std::fs::write(directory.join("changed.txt"), "changed again\n").unwrap();
|
||||||
std::fs::remove_file(directory.join("deleted.txt")).unwrap();
|
std::fs::remove_file(directory.join("deleted.txt")).unwrap();
|
||||||
|
|
||||||
@@ -846,7 +998,7 @@ mod tests {
|
|||||||
assert_eq!(added.worktree_kind, Some(GitChangeKind::Added));
|
assert_eq!(added.worktree_kind, Some(GitChangeKind::Added));
|
||||||
|
|
||||||
stage_paths(&directory, std::slice::from_ref(&added_path)).unwrap();
|
stage_paths(&directory, std::slice::from_ref(&added_path)).unwrap();
|
||||||
run_git_commit_paths(
|
commit_paths(
|
||||||
&directory,
|
&directory,
|
||||||
"Commit selected file",
|
"Commit selected file",
|
||||||
std::slice::from_ref(&added_path),
|
std::slice::from_ref(&added_path),
|
||||||
@@ -892,36 +1044,25 @@ mod tests {
|
|||||||
let local = repository_fixture();
|
let local = repository_fixture();
|
||||||
let remote = local.with_extension("remote.git");
|
let remote = local.with_extension("remote.git");
|
||||||
let collaborator = local.with_extension("collaborator");
|
let collaborator = local.with_extension("collaborator");
|
||||||
let output = Command::new("git")
|
git2::Repository::init_bare(&remote).unwrap();
|
||||||
.args(["init", "--bare"])
|
git2::Repository::open(&local)
|
||||||
.arg(&remote)
|
.unwrap()
|
||||||
.output()
|
.remote("origin", remote.to_str().unwrap())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(output.status.success());
|
|
||||||
run_git_args(
|
|
||||||
&local,
|
|
||||||
&["remote", "add", "origin", remote.to_str().unwrap()],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
run_git_remote(&local, "push", Some("main")).unwrap();
|
run_git_remote(&local, "push", Some("main")).unwrap();
|
||||||
run_git_args(&remote, &["symbolic-ref", "HEAD", "refs/heads/main"]).unwrap();
|
git2::Repository::open_bare(&remote)
|
||||||
|
.unwrap()
|
||||||
let output = Command::new("git")
|
.set_head("refs/heads/main")
|
||||||
.arg("clone")
|
|
||||||
.arg(&remote)
|
|
||||||
.arg(&collaborator)
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(output.status.success());
|
|
||||||
run_git_args(&collaborator, &["config", "user.name", "DS4Server Test"]).unwrap();
|
let cloned = git2::build::RepoBuilder::new()
|
||||||
run_git_args(
|
.clone(remote.to_str().unwrap(), &collaborator)
|
||||||
&collaborator,
|
.unwrap();
|
||||||
&["config", "user.email", "test@ds4server.invalid"],
|
configure_test_repository(&cloned);
|
||||||
)
|
drop(cloned);
|
||||||
.unwrap();
|
|
||||||
std::fs::write(collaborator.join("remote.txt"), "from origin\n").unwrap();
|
std::fs::write(collaborator.join("remote.txt"), "from origin\n").unwrap();
|
||||||
run_git_args(&collaborator, &["add", "remote.txt"]).unwrap();
|
stage_paths(&collaborator, &["remote.txt".into()]).unwrap();
|
||||||
run_git_args(&collaborator, &["commit", "-m", "Remote change"]).unwrap();
|
commit_index(&collaborator, "Remote change").unwrap();
|
||||||
run_git_remote(&collaborator, "push", Some("main")).unwrap();
|
run_git_remote(&collaborator, "push", Some("main")).unwrap();
|
||||||
|
|
||||||
run_git_remote(&local, "fetch", None).unwrap();
|
run_git_remote(&local, "fetch", None).unwrap();
|
||||||
@@ -931,15 +1072,22 @@ mod tests {
|
|||||||
"from origin\n"
|
"from origin\n"
|
||||||
);
|
);
|
||||||
std::fs::write(local.join("local.txt"), "to origin\n").unwrap();
|
std::fs::write(local.join("local.txt"), "to origin\n").unwrap();
|
||||||
run_git_args(&local, &["add", "local.txt"]).unwrap();
|
stage_paths(&local, &["local.txt".into()]).unwrap();
|
||||||
run_git_args(&local, &["commit", "-m", "Local change"]).unwrap();
|
commit_index(&local, "Local change").unwrap();
|
||||||
run_git_remote(&local, "push", Some("main")).unwrap();
|
run_git_remote(&local, "push", Some("main")).unwrap();
|
||||||
assert_eq!(
|
let content = {
|
||||||
run_git_args(&remote, &["show", "main:local.txt"])
|
let bare = git2::Repository::open_bare(&remote).unwrap();
|
||||||
|
let commit = bare
|
||||||
|
.find_branch("main", git2::BranchType::Local)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.trim(),
|
.get()
|
||||||
"to origin"
|
.peel_to_commit()
|
||||||
);
|
.unwrap();
|
||||||
|
let tree = commit.tree().unwrap();
|
||||||
|
let entry = tree.get_path(Path::new("local.txt")).unwrap();
|
||||||
|
bare.find_blob(entry.id()).unwrap().content().to_vec()
|
||||||
|
};
|
||||||
|
assert_eq!(content, b"to origin\n");
|
||||||
|
|
||||||
std::fs::remove_dir_all(local).unwrap();
|
std::fs::remove_dir_all(local).unwrap();
|
||||||
std::fs::remove_dir_all(remote).unwrap();
|
std::fs::remove_dir_all(remote).unwrap();
|
||||||
@@ -974,8 +1122,8 @@ mod tests {
|
|||||||
let directory = empty_repository_fixture();
|
let directory = empty_repository_fixture();
|
||||||
std::fs::write(directory.join("changed.txt"), "original\n").unwrap();
|
std::fs::write(directory.join("changed.txt"), "original\n").unwrap();
|
||||||
std::fs::write(directory.join("deleted.txt"), "original\n").unwrap();
|
std::fs::write(directory.join("deleted.txt"), "original\n").unwrap();
|
||||||
run_git_args(&directory, &["add", "changed.txt", "deleted.txt"]).unwrap();
|
stage_paths(&directory, &["changed.txt".into(), "deleted.txt".into()]).unwrap();
|
||||||
run_git_args(&directory, &["commit", "-m", "Initial"]).unwrap();
|
commit_index(&directory, "Initial").unwrap();
|
||||||
directory
|
directory
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -990,13 +1138,18 @@ mod tests {
|
|||||||
NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
|
NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
|
||||||
));
|
));
|
||||||
std::fs::create_dir(&directory).unwrap();
|
std::fs::create_dir(&directory).unwrap();
|
||||||
run_git_args(&directory, &["init", "-b", "main"]).unwrap();
|
let mut options = git2::RepositoryInitOptions::new();
|
||||||
run_git_args(&directory, &["config", "user.name", "DS4Server Test"]).unwrap();
|
options.initial_head("main");
|
||||||
run_git_args(
|
let repository = git2::Repository::init_opts(&directory, &options).unwrap();
|
||||||
&directory,
|
configure_test_repository(&repository);
|
||||||
&["config", "user.email", "test@ds4server.invalid"],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
directory
|
directory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn configure_test_repository(repository: &git2::Repository) {
|
||||||
|
let mut config = repository.config().unwrap();
|
||||||
|
config.set_str("user.name", "DS4Server Test").unwrap();
|
||||||
|
config
|
||||||
|
.set_str("user.email", "test@ds4server.invalid")
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,48 +368,31 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_git_state(path: &Path) -> Option<GitState> {
|
fn read_git_state(path: &Path) -> Option<GitState> {
|
||||||
let path = path.to_str()?;
|
let repository = gix::discover(path).ok()?;
|
||||||
let branches = std::process::Command::new("git")
|
let mut branches = repository
|
||||||
.args([
|
.references()
|
||||||
"-C",
|
.ok()?
|
||||||
path,
|
.local_branches()
|
||||||
"for-each-ref",
|
.ok()?
|
||||||
"--format=%(refname:short)",
|
.map(|reference| {
|
||||||
"refs/heads",
|
reference
|
||||||
])
|
.map(|reference| String::from_utf8_lossy(reference.name().shorten()).into_owned())
|
||||||
.output()
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.ok()?;
|
.ok()?;
|
||||||
if !branches.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut branches = String::from_utf8_lossy(&branches.stdout)
|
|
||||||
.lines()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|branch| !branch.is_empty())
|
|
||||||
.map(str::to_owned)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
branches.sort();
|
branches.sort();
|
||||||
branches.dedup();
|
branches.dedup();
|
||||||
|
|
||||||
let current = std::process::Command::new("git")
|
let current = repository
|
||||||
.args(["-C", path, "symbolic-ref", "--quiet", "--short", "HEAD"])
|
.head_name()
|
||||||
.output()
|
.ok()?
|
||||||
.ok()
|
.map(|name| String::from_utf8_lossy(name.shorten()).into_owned());
|
||||||
.filter(|output| output.status.success())
|
|
||||||
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
|
|
||||||
.filter(|branch| !branch.is_empty());
|
|
||||||
let label = current.clone().unwrap_or_else(|| {
|
let label = current.clone().unwrap_or_else(|| {
|
||||||
std::process::Command::new("git")
|
repository
|
||||||
.args(["-C", path, "rev-parse", "--short", "HEAD"])
|
.head_id()
|
||||||
.output()
|
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|output| output.status.success())
|
.and_then(|id| id.shorten().ok())
|
||||||
.map(|output| {
|
.map(|id| format!("detached @ {id}"))
|
||||||
format!(
|
|
||||||
"detached @ {}",
|
|
||||||
String::from_utf8_lossy(&output.stdout).trim()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| "No branch".to_owned())
|
.unwrap_or_else(|| "No branch".to_owned())
|
||||||
});
|
});
|
||||||
Some(GitState {
|
Some(GitState {
|
||||||
@@ -420,22 +403,25 @@ fn read_git_state(path: &Path) -> Option<GitState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn git_switch(path: &Path, branch: &str) -> Result<(), String> {
|
fn git_switch(path: &Path, branch: &str) -> Result<(), String> {
|
||||||
let output = std::process::Command::new("git")
|
let repository = git2::Repository::discover(path)
|
||||||
.arg("-C")
|
.map_err(|error| format!("Could not switch Git branches: {error}"))?;
|
||||||
.arg(path)
|
let reference = format!("refs/heads/{branch}");
|
||||||
.args(["switch", "--", branch])
|
let previous = repository
|
||||||
.output()
|
.find_reference("HEAD")
|
||||||
.map_err(|error| format!("Could not run Git: {error}"))?;
|
.ok()
|
||||||
if output.status.success() {
|
.map(|head| (head.symbolic_target().map(str::to_owned), head.target()));
|
||||||
Ok(())
|
repository
|
||||||
} else {
|
.set_head(&reference)
|
||||||
let detail = String::from_utf8_lossy(&output.stderr).trim().to_owned();
|
.map_err(|error| format!("Could not switch Git branches: {error}"))?;
|
||||||
Err(if detail.is_empty() {
|
if let Err(error) = repository.checkout_head(Some(git2::build::CheckoutBuilder::new().safe())) {
|
||||||
"Could not switch Git branches.".into()
|
if let Some((Some(reference), _)) = &previous {
|
||||||
} else {
|
let _ = repository.set_head(reference);
|
||||||
format!("Could not switch Git branches: {detail}")
|
} else if let Some((_, Some(target))) = previous {
|
||||||
})
|
let _ = repository.set_head_detached(target);
|
||||||
|
}
|
||||||
|
return Err(format!("Could not switch Git branches: {error}"));
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn draft_title(projects: &[ProjectWithSessions], project_id: i32) -> String {
|
pub(super) fn draft_title(projects: &[ProjectWithSessions], project_id: i32) -> String {
|
||||||
@@ -450,7 +436,6 @@ pub(super) fn draft_title(projects: &[ProjectWithSessions], project_id: i32) ->
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::database::{Project, Session};
|
use crate::database::{Project, Session};
|
||||||
use std::process::Command;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -541,37 +526,30 @@ mod tests {
|
|||||||
std::process::id()
|
std::process::id()
|
||||||
));
|
));
|
||||||
std::fs::create_dir(&directory).unwrap();
|
std::fs::create_dir(&directory).unwrap();
|
||||||
for arguments in [
|
let mut options = git2::RepositoryInitOptions::new();
|
||||||
&["init", "-b", "main"][..],
|
options.initial_head("main");
|
||||||
&["config", "user.name", "DS4Server Test"],
|
let repository = git2::Repository::init_opts(&directory, &options).unwrap();
|
||||||
&["config", "user.email", "test@ds4server.invalid"],
|
let mut config = repository.config().unwrap();
|
||||||
] {
|
config.set_str("user.name", "DS4Server Test").unwrap();
|
||||||
assert!(
|
config
|
||||||
Command::new("git")
|
.set_str("user.email", "test@ds4server.invalid")
|
||||||
.arg("-C")
|
.unwrap();
|
||||||
.arg(&directory)
|
drop(config);
|
||||||
.args(arguments)
|
|
||||||
.status()
|
|
||||||
.unwrap()
|
|
||||||
.success()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
std::fs::write(directory.join("tracked.txt"), "test").unwrap();
|
std::fs::write(directory.join("tracked.txt"), "test").unwrap();
|
||||||
for arguments in [
|
let mut index = repository.index().unwrap();
|
||||||
&["add", "tracked.txt"][..],
|
index.add_path(Path::new("tracked.txt")).unwrap();
|
||||||
&["commit", "-m", "Initial"],
|
let tree_id = index.write_tree().unwrap();
|
||||||
&["branch", "feature"],
|
index.write().unwrap();
|
||||||
] {
|
let tree = repository.find_tree(tree_id).unwrap();
|
||||||
assert!(
|
let signature = repository.signature().unwrap();
|
||||||
Command::new("git")
|
let commit = repository
|
||||||
.arg("-C")
|
.commit(Some("HEAD"), &signature, &signature, "Initial", &tree, &[])
|
||||||
.arg(&directory)
|
.unwrap();
|
||||||
.args(arguments)
|
let commit = repository.find_commit(commit).unwrap();
|
||||||
.status()
|
repository.branch("feature", &commit, false).unwrap();
|
||||||
.unwrap()
|
drop(commit);
|
||||||
.success()
|
drop(tree);
|
||||||
);
|
drop(repository);
|
||||||
}
|
|
||||||
|
|
||||||
let state = read_git_state(&directory).unwrap();
|
let state = read_git_state(&directory).unwrap();
|
||||||
assert_eq!(state.current.as_deref(), Some("main"));
|
assert_eq!(state.current.as_deref(), Some("main"));
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use sha2::{Digest, Sha256};
|
|||||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::process::Command;
|
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@@ -1113,35 +1112,34 @@ fn git_revision_matches(root: &Path, revision: &str, current: &str) -> bool {
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Command::new("git")
|
git2::Repository::discover(root)
|
||||||
.args(["-C"])
|
|
||||||
.arg(root)
|
|
||||||
.args(["rev-parse", "--verify"])
|
|
||||||
.arg(format!("{revision}^{{commit}}"))
|
|
||||||
.output()
|
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|output| output.status.success())
|
.is_some_and(|repository| {
|
||||||
.is_some_and(|output| String::from_utf8_lossy(&output.stdout).trim() == current)
|
repository
|
||||||
|
.revparse_single(revision)
|
||||||
|
.ok()
|
||||||
|
.and_then(|object| object.peel_to_commit().ok())
|
||||||
|
.is_some_and(|commit| commit.id().to_string() == current)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_state(root: &Path) -> Option<(String, bool)> {
|
fn git_state(root: &Path) -> Option<(String, bool)> {
|
||||||
let revision = Command::new("git")
|
let repository = git2::Repository::discover(root).ok()?;
|
||||||
.args(["-C"])
|
let revision = repository
|
||||||
.arg(root)
|
.head()
|
||||||
.args(["rev-parse", "HEAD"])
|
.ok()?
|
||||||
.output()
|
.peel_to_commit()
|
||||||
.ok()
|
.ok()?
|
||||||
.filter(|output| output.status.success())
|
.id()
|
||||||
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())?;
|
.to_string();
|
||||||
let status = Command::new("git")
|
let mut options = git2::StatusOptions::new();
|
||||||
.args(["-C"])
|
options
|
||||||
.arg(root)
|
.include_untracked(true)
|
||||||
.args(["status", "--porcelain"])
|
.recurse_untracked_dirs(true)
|
||||||
.output()
|
.include_ignored(false);
|
||||||
.ok()?;
|
|
||||||
Some((
|
Some((
|
||||||
revision,
|
revision,
|
||||||
status.status.success() && status.stdout.is_empty(),
|
repository.statuses(Some(&mut options)).ok()?.is_empty(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1586,29 +1584,18 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn clean_full_and_short_git_revisions_become_stale_when_the_worktree_changes() {
|
fn clean_full_and_short_git_revisions_become_stale_when_the_worktree_changes() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
for arguments in [
|
let repository = git2::Repository::init(&fixture.project).unwrap();
|
||||||
vec!["init"],
|
let mut index = repository.index().unwrap();
|
||||||
vec!["add", "source.rs"],
|
index.add_path(Path::new("source.rs")).unwrap();
|
||||||
vec![
|
let tree_id = index.write_tree().unwrap();
|
||||||
"-c",
|
index.write().unwrap();
|
||||||
"user.name=DS4Server",
|
let tree = repository.find_tree(tree_id).unwrap();
|
||||||
"-c",
|
let signature = git2::Signature::now("DS4Server", "ds4@example.invalid").unwrap();
|
||||||
"user.email=ds4@example.invalid",
|
repository
|
||||||
"commit",
|
.commit(Some("HEAD"), &signature, &signature, "Initial", &tree, &[])
|
||||||
"-m",
|
.unwrap();
|
||||||
"Initial",
|
drop(tree);
|
||||||
],
|
drop(repository);
|
||||||
] {
|
|
||||||
assert!(
|
|
||||||
Command::new("git")
|
|
||||||
.arg("-C")
|
|
||||||
.arg(&fixture.project)
|
|
||||||
.args(arguments)
|
|
||||||
.status()
|
|
||||||
.unwrap()
|
|
||||||
.success()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let revision = git_state(&fixture.project).unwrap().0;
|
let revision = git_state(&fixture.project).unwrap().0;
|
||||||
assert!(git_revision_matches(
|
assert!(git_revision_matches(
|
||||||
&fixture.project,
|
&fixture.project,
|
||||||
|
|||||||
Reference in New Issue
Block a user