diff --git a/crates/gitbutler-branch/Cargo.toml b/crates/gitbutler-branch/Cargo.toml index e2273062c..dda5e05d3 100644 --- a/crates/gitbutler-branch/Cargo.toml +++ b/crates/gitbutler-branch/Cargo.toml @@ -22,6 +22,7 @@ git2-hooks = "0.3" url = { version = "2.5.2", features = ["serde"] } md5 = "0.7.0" futures = "0.3" +itertools = "0.13" [[test]] name="branches" @@ -35,4 +36,3 @@ gitbutler-git = { workspace = true, features = ["test-askpass-path"] } glob = "0.3.1" serial_test = "3.1.1" tempfile = "3.10" -itertools = "0.13" diff --git a/crates/gitbutler-branch/src/base.rs b/crates/gitbutler-branch/src/base.rs index 640a2dbaa..80cce4d33 100644 --- a/crates/gitbutler-branch/src/base.rs +++ b/crates/gitbutler-branch/src/base.rs @@ -8,6 +8,7 @@ use serde::Serialize; use super::r#virtual as vb; use super::r#virtual::convert_to_real_branch; +use crate::conflicts::RepoConflicts; use crate::integration::{get_workspace_head, update_gitbutler_integration}; use crate::remote::{commit_to_remote_commit, RemoteCommit}; use crate::VirtualBranchHunk; diff --git a/crates/gitbutler-branch/src/conflicts.rs b/crates/gitbutler-branch/src/conflicts.rs new file mode 100644 index 000000000..881b4b022 --- /dev/null +++ b/crates/gitbutler-branch/src/conflicts.rs @@ -0,0 +1,167 @@ +// stuff to manage merge conflict state +// this is the dumbest possible way to do this, but it is a placeholder +// conflicts are stored one path per line in .git/conflicts +// merge parent is stored in .git/base_merge_parent +// conflicts are removed as they are resolved, the conflicts file is removed when there are no more conflicts +// the merge parent file is removed when the merge is complete + +use std::{ + io::{BufRead, Write}, + path::{Path, PathBuf}, +}; + +use anyhow::{anyhow, Context, Result}; +use itertools::Itertools; + +use gitbutler_core::{error::Marker, project_repository::ProjectRepo}; + +pub fn mark, A: AsRef<[P]>>( + repository: &ProjectRepo, + paths: A, + parent: Option, +) -> Result<()> { + let paths = paths.as_ref(); + if paths.is_empty() { + return Ok(()); + } + let conflicts_path = repository.repo().path().join("conflicts"); + // write all the file paths to a file on disk + let mut file = std::fs::File::create(conflicts_path)?; + for path in paths { + file.write_all(path.as_ref().as_os_str().as_encoded_bytes())?; + file.write_all(b"\n")?; + } + + if let Some(parent) = parent { + let merge_path = repository.repo().path().join("base_merge_parent"); + // write all the file paths to a file on disk + let mut file = std::fs::File::create(merge_path)?; + file.write_all(parent.to_string().as_bytes())?; + } + + Ok(()) +} + +pub fn merge_parent(repository: &ProjectRepo) -> Result> { + let merge_path = repository.repo().path().join("base_merge_parent"); + if !merge_path.exists() { + return Ok(None); + } + + let file = std::fs::File::open(merge_path)?; + let reader = std::io::BufReader::new(file); + let mut lines = reader.lines(); + if let Some(parent) = lines.next() { + let parent = parent?; + let parent: git2::Oid = parent.parse()?; + Ok(Some(parent)) + } else { + Ok(None) + } +} + +pub fn resolve>(repository: &ProjectRepo, path: P) -> Result<()> { + let path = path.as_ref(); + let conflicts_path = repository.repo().path().join("conflicts"); + let file = std::fs::File::open(conflicts_path.clone())?; + let reader = std::io::BufReader::new(file); + let mut remaining = Vec::new(); + for line in reader.lines().map_ok(PathBuf::from) { + let line = line?; + if line != path { + remaining.push(line); + } + } + + // remove file + std::fs::remove_file(conflicts_path)?; + + // re-write file if needed + if !remaining.is_empty() { + mark(repository, &remaining, None)?; + } + Ok(()) +} + +pub fn conflicting_files(repository: &ProjectRepo) -> Result> { + let conflicts_path = repository.repo().path().join("conflicts"); + if !conflicts_path.exists() { + return Ok(vec![]); + } + + let file = std::fs::File::open(conflicts_path)?; + let reader = std::io::BufReader::new(file); + Ok(reader.lines().map_while(Result::ok).collect()) +} + +/// Check if `path` is conflicting in `repository`, or if `None`, check if there is any conflict. +// TODO(ST): Should this not rather check the conflicting state in the index? +pub fn is_conflicting(repository: &ProjectRepo, path: Option<&Path>) -> Result { + let conflicts_path = repository.repo().path().join("conflicts"); + if !conflicts_path.exists() { + return Ok(false); + } + + let file = std::fs::File::open(conflicts_path)?; + let reader = std::io::BufReader::new(file); + // TODO(ST): This shouldn't work on UTF8 strings. + let mut files = reader.lines().map_ok(PathBuf::from); + if let Some(pathname) = path { + // check if pathname is one of the lines in conflicts_path file + for line in files { + let line = line?; + + if line == pathname { + return Ok(true); + } + } + Ok(false) + } else { + Ok(files.next().transpose().map(|x| x.is_some())?) + } +} + +// is this project still in a resolving conflict state? +// - could be that there are no more conflicts, but the state is not committed +pub fn is_resolving(repository: &ProjectRepo) -> bool { + repository.repo().path().join("base_merge_parent").exists() +} + +pub fn clear(repository: &ProjectRepo) -> Result<()> { + let merge_path = repository.repo().path().join("base_merge_parent"); + std::fs::remove_file(merge_path)?; + + for file in conflicting_files(repository)? { + resolve(repository, &file)?; + } + + Ok(()) +} + +pub trait RepoConflicts { + fn assure_unconflicted(&self) -> Result<()>; + fn assure_resolved(&self) -> Result<()>; + fn is_resolving(&self) -> bool; +} + +impl RepoConflicts for ProjectRepo { + fn is_resolving(&self) -> bool { + is_resolving(self) + } + + fn assure_resolved(&self) -> Result<()> { + if self.is_resolving() { + Err(anyhow!("project has active conflicts")).context(Marker::ProjectConflict) + } else { + Ok(()) + } + } + + fn assure_unconflicted(&self) -> Result<()> { + if is_conflicting(self, None)? { + Err(anyhow!("project has active conflicts")).context(Marker::ProjectConflict) + } else { + Ok(()) + } + } +} diff --git a/crates/gitbutler-branch/src/integration.rs b/crates/gitbutler-branch/src/integration.rs index 969e9a49b..d00e6bd6f 100644 --- a/crates/gitbutler-branch/src/integration.rs +++ b/crates/gitbutler-branch/src/integration.rs @@ -13,10 +13,12 @@ use gitbutler_core::virtual_branches::{ }; use gitbutler_core::{ git::CommitExt, - project_repository::{self, conflicts, LogUntil}, + project_repository::{self, LogUntil}, virtual_branches::branch::BranchCreateRequest, }; +use crate::conflicts; + const WORKSPACE_HEAD: &str = "Workspace Head"; pub fn get_integration_commiter<'a>() -> Result> { diff --git a/crates/gitbutler-branch/src/lib.rs b/crates/gitbutler-branch/src/lib.rs index 38932c12d..19c681c17 100644 --- a/crates/gitbutler-branch/src/lib.rs +++ b/crates/gitbutler-branch/src/lib.rs @@ -14,3 +14,5 @@ pub mod integration; pub mod files; pub mod remote; + +pub mod conflicts; diff --git a/crates/gitbutler-branch/src/virtual.rs b/crates/gitbutler-branch/src/virtual.rs index f75cba7eb..41cf8b296 100644 --- a/crates/gitbutler-branch/src/virtual.rs +++ b/crates/gitbutler-branch/src/virtual.rs @@ -21,6 +21,7 @@ use gitbutler_core::virtual_branches::Author; use hex::ToHex; use serde::{Deserialize, Serialize}; +use crate::conflicts::{self, RepoConflicts}; use crate::integration::{get_integration_commiter, get_workspace_head}; use crate::remote::{branch_to_remote_branch, RemoteBranch}; use gitbutler_core::error::Code; @@ -46,7 +47,7 @@ use gitbutler_core::{ diff::{self}, Refname, RemoteRefname, }, - project_repository::{self, conflicts, LogUntil}, + project_repository::{self, LogUntil}, }; type AppliedStatuses = Vec<(branch::Branch, BranchStatus)>; diff --git a/crates/gitbutler-core/src/project_repository/mod.rs b/crates/gitbutler-core/src/project_repository/mod.rs index 11e4c89dd..3a0751268 100644 --- a/crates/gitbutler-core/src/project_repository/mod.rs +++ b/crates/gitbutler-core/src/project_repository/mod.rs @@ -1,5 +1,5 @@ mod config; -pub mod conflicts; +// pub mod conflicts; mod repository; pub use config::Config; diff --git a/crates/gitbutler-core/src/project_repository/repository.rs b/crates/gitbutler-core/src/project_repository/repository.rs index 328af3803..cea95ae1c 100644 --- a/crates/gitbutler-core/src/project_repository/repository.rs +++ b/crates/gitbutler-core/src/project_repository/repository.rs @@ -6,7 +6,8 @@ use std::{ use anyhow::{anyhow, Context, Result}; -use super::conflicts; +// use super::conflicts; +use crate::git::RepositoryExt; use crate::{ askpass, git::{self, Url}, @@ -15,7 +16,6 @@ use crate::{ virtual_branches::{Branch, BranchId}, }; use crate::{error::Code, git::CommitHeadersV2}; -use crate::{error::Marker, git::RepositoryExt}; pub struct ProjectRepo { git_repository: git2::Repository, @@ -140,32 +140,9 @@ pub trait RepoActions { fn git_index_size(&self) -> Result; fn config(&self) -> super::Config; fn path(&self) -> &path::Path; - fn assure_unconflicted(&self) -> Result<()>; - fn assure_resolved(&self) -> Result<()>; - fn is_resolving(&self) -> bool; } impl RepoActions for ProjectRepo { - fn is_resolving(&self) -> bool { - conflicts::is_resolving(self) - } - - fn assure_resolved(&self) -> Result<()> { - if self.is_resolving() { - Err(anyhow!("project has active conflicts")).context(Marker::ProjectConflict) - } else { - Ok(()) - } - } - - fn assure_unconflicted(&self) -> Result<()> { - if conflicts::is_conflicting(self, None)? { - Err(anyhow!("project has active conflicts")).context(Marker::ProjectConflict) - } else { - Ok(()) - } - } - fn path(&self) -> &path::Path { path::Path::new(&self.project.path) } diff --git a/crates/gitbutler-tauri/src/app.rs b/crates/gitbutler-tauri/src/app.rs index 0e521dc73..f7317cb5c 100644 --- a/crates/gitbutler-tauri/src/app.rs +++ b/crates/gitbutler-tauri/src/app.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result}; +use gitbutler_branch::conflicts; use gitbutler_core::{ git, - project_repository::{self, conflicts, RepoActions}, + project_repository::{self, RepoActions}, projects::{self, ProjectId}, virtual_branches::BranchId, };