diff --git a/Cargo.lock b/Cargo.lock index 7ab067fc5..154ee3192 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2267,6 +2267,9 @@ dependencies = [ "bstr", "git2", "gitbutler-core", + "gitbutler-git", + "tokio", + "tracing", ] [[package]] @@ -2296,6 +2299,7 @@ dependencies = [ "gitbutler-branch", "gitbutler-core", "gitbutler-oplog", + "gitbutler-repo", "gitbutler-testsupport", "gitbutler-watcher", "log", diff --git a/crates/gitbutler-branch/src/base.rs b/crates/gitbutler-branch/src/base.rs index 4a7444589..c89c1f080 100644 --- a/crates/gitbutler-branch/src/base.rs +++ b/crates/gitbutler-branch/src/base.rs @@ -3,7 +3,7 @@ use std::{path::Path, time}; use anyhow::{anyhow, Context, Result}; use git2::Index; use gitbutler_branchstate::{VirtualBranchesAccess, VirtualBranchesHandle}; -use gitbutler_core::project_repository::RepoActions; +use gitbutler_repo::{LogUntil, RepoActions}; use serde::Serialize; use super::r#virtual as vb; @@ -16,7 +16,7 @@ use gitbutler_core::virtual_branches::{branch, target, BranchId, GITBUTLER_INTEG use gitbutler_core::{error::Marker, git::RepositoryExt}; use gitbutler_core::{ git::{self, diff}, - project_repository::{self, LogUntil}, + project_repository, projects::FetchResult, virtual_branches::branch::BranchOwnershipClaims, }; @@ -588,7 +588,7 @@ pub fn target_to_base_branch( // gather a list of commits between oid and target.sha let upstream_commits = project_repository - .log(oid, project_repository::LogUntil::Commit(target.sha)) + .log(oid, LogUntil::Commit(target.sha)) .context("failed to get upstream commits")? .iter() .map(commit_to_remote_commit) diff --git a/crates/gitbutler-branch/src/controller.rs b/crates/gitbutler-branch/src/controller.rs index 9ad624cef..5dda43732 100644 --- a/crates/gitbutler-branch/src/controller.rs +++ b/crates/gitbutler-branch/src/controller.rs @@ -2,7 +2,7 @@ use anyhow::Result; use gitbutler_branchstate::{VirtualBranchesAccess, VirtualBranchesHandle}; use gitbutler_core::{ git::{credentials::Helper, BranchExt, RepositoryExt}, - project_repository::{ProjectRepo, RepoActions}, + project_repository::ProjectRepo, projects::FetchResult, types::ReferenceName, }; @@ -11,6 +11,7 @@ use gitbutler_oplog::{ oplog::Oplog, snapshot::Snapshot, }; +use gitbutler_repo::RepoActions; use std::{path::Path, sync::Arc}; use tokio::sync::Semaphore; diff --git a/crates/gitbutler-branch/src/integration.rs b/crates/gitbutler-branch/src/integration.rs index cfebfbab1..20707a1cb 100644 --- a/crates/gitbutler-branch/src/integration.rs +++ b/crates/gitbutler-branch/src/integration.rs @@ -6,16 +6,14 @@ use bstr::ByteSlice; use gitbutler_branchstate::{VirtualBranchesAccess, VirtualBranchesHandle}; use gitbutler_core::error::Marker; use gitbutler_core::git::RepositoryExt; -use gitbutler_core::project_repository::RepoActions; use gitbutler_core::virtual_branches::{ GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL, GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME, GITBUTLER_INTEGRATION_REFERENCE, }; use gitbutler_core::{ - git::CommitExt, - project_repository::{self, LogUntil}, - virtual_branches::branch::BranchCreateRequest, + git::CommitExt, project_repository, virtual_branches::branch::BranchCreateRequest, }; +use gitbutler_repo::{LogUntil, RepoActions}; use crate::conflicts; diff --git a/crates/gitbutler-branch/src/remote.rs b/crates/gitbutler-branch/src/remote.rs index 5270ce154..e893fa8b2 100644 --- a/crates/gitbutler-branch/src/remote.rs +++ b/crates/gitbutler-branch/src/remote.rs @@ -3,13 +3,13 @@ use std::path::Path; use anyhow::{Context, Result}; use bstr::BString; use gitbutler_branchstate::VirtualBranchesHandle; -use gitbutler_core::project_repository::RepoActions; +use gitbutler_repo::{LogUntil, RepoActions}; use serde::Serialize; use gitbutler_core::virtual_branches::{target, Author}; use gitbutler_core::{ git::{self, CommitExt, RepositoryExt}, - project_repository::{self, LogUntil}, + project_repository, }; // this struct is a mapping to the view `RemoteBranch` type in Typescript diff --git a/crates/gitbutler-branch/src/virtual.rs b/crates/gitbutler-branch/src/virtual.rs index ea8eb3d3c..b32141cbc 100644 --- a/crates/gitbutler-branch/src/virtual.rs +++ b/crates/gitbutler-branch/src/virtual.rs @@ -1,6 +1,6 @@ use gitbutler_branchstate::{VirtualBranchesAccess, VirtualBranchesHandle}; -use gitbutler_core::project_repository::RepoActions; use gitbutler_oplog::snapshot::Snapshot; +use gitbutler_repo::{LogUntil, RepoActions}; use std::borrow::Borrow; #[cfg(target_family = "unix")] use std::os::unix::prelude::PermissionsExt; @@ -41,12 +41,8 @@ use gitbutler_core::virtual_branches::{ }; use gitbutler_core::{ dedup::{dedup, dedup_fmt}, - git::{ - self, - diff::{self}, - Refname, RemoteRefname, - }, - project_repository::{self, LogUntil}, + git::{self, diff, Refname, RemoteRefname}, + project_repository, }; use gitbutler_repo::rebase::{cherry_rebase, cherry_rebase_group}; @@ -2238,10 +2234,7 @@ fn is_commit_integrated( .find_branch_by_refname(&target.branch.clone().into())? .ok_or(anyhow!("failed to get branch"))?; let remote_head = remote_branch.get().peel_to_commit()?; - let upstream_commits = project_repository.l( - remote_head.id(), - project_repository::LogUntil::Commit(target.sha), - )?; + let upstream_commits = project_repository.l(remote_head.id(), LogUntil::Commit(target.sha))?; if target.sha.eq(&commit.id()) { // could not be integrated if heads are the same. @@ -2360,10 +2353,8 @@ pub fn move_commit_file( .context("failed to find commit")?; // find all the commits upstream from the target "to" commit - let mut upstream_commits = project_repository.l( - target_branch.head, - project_repository::LogUntil::Commit(amend_commit.id()), - )?; + let mut upstream_commits = + project_repository.l(target_branch.head, LogUntil::Commit(amend_commit.id()))?; // get a list of all the diffs across all the virtual branches let base_file_diffs = diff::workdir(project_repository.repo(), &default_target.sha) @@ -2491,15 +2482,11 @@ pub fn move_commit_file( // ok, now we need to identify which the new "to" commit is in the rebased history // so we'll take a list of the upstream oids and find it simply based on location // (since the order should not have changed in our simple rebase) - let old_upstream_commit_oids = project_repository.l( - target_branch.head, - project_repository::LogUntil::Commit(default_target.sha), - )?; + let old_upstream_commit_oids = + project_repository.l(target_branch.head, LogUntil::Commit(default_target.sha))?; - let new_upstream_commit_oids = project_repository.l( - new_head, - project_repository::LogUntil::Commit(default_target.sha), - )?; + let new_upstream_commit_oids = + project_repository.l(new_head, LogUntil::Commit(default_target.sha))?; // find to_commit_oid offset in upstream_commits vector let to_commit_offset = old_upstream_commit_oids @@ -2519,10 +2506,7 @@ pub fn move_commit_file( .context("failed to find commit")?; // reset the concept of what the upstream commits are to be the rebased ones - upstream_commits = project_repository.l( - new_head, - project_repository::LogUntil::Commit(amend_commit.id()), - )?; + upstream_commits = project_repository.l(new_head, LogUntil::Commit(amend_commit.id()))?; } // ok, now we will apply the moved changes to the "to" commit. @@ -2633,10 +2617,7 @@ pub fn amend( } if project_repository - .l( - target_branch.head, - project_repository::LogUntil::Commit(default_target.sha), - )? + .l(target_branch.head, LogUntil::Commit(default_target.sha))? .is_empty() { bail!("branch has no commits - there is nothing to amend to"); @@ -2701,10 +2682,8 @@ pub fn amend( .context("failed to create commit")?; // now rebase upstream commits, if needed - let upstream_commits = project_repository.l( - target_branch.head, - project_repository::LogUntil::Commit(amend_commit.id()), - )?; + let upstream_commits = + project_repository.l(target_branch.head, LogUntil::Commit(amend_commit.id()))?; // if there are no upstream commits, we're done if upstream_commits.is_empty() { target_branch.head = commit_oid; @@ -2764,10 +2743,7 @@ pub fn reorder_commit( } // get a list of the commits to rebase - let mut ids_to_rebase = project_repository.l( - branch.head, - project_repository::LogUntil::Commit(commit.id()), - )?; + let mut ids_to_rebase = project_repository.l(branch.head, LogUntil::Commit(commit.id()))?; ids_to_rebase.insert( ids_to_rebase.len() - offset.unsigned_abs() as usize, @@ -2799,10 +2775,7 @@ pub fn reorder_commit( // get a list of the commits to rebase let mut ids_to_rebase: Vec = project_repository - .l( - branch.head, - project_repository::LogUntil::Commit(target_oid), - )? + .l(branch.head, LogUntil::Commit(target_oid))? .iter() .filter(|id| **id != commit_oid) .cloned() @@ -2942,10 +2915,8 @@ pub fn squash( let vb_state = project_repository.project().virtual_branches(); let mut branch = vb_state.get_branch(branch_id)?; let default_target = vb_state.get_default_target()?; - let branch_commit_oids = project_repository.l( - branch.head, - project_repository::LogUntil::Commit(default_target.sha), - )?; + let branch_commit_oids = + project_repository.l(branch.head, LogUntil::Commit(default_target.sha))?; if !branch_commit_oids.contains(&commit_id) { bail!("commit {commit_id} not in the branch") @@ -2962,12 +2933,7 @@ pub fn squash( let pushed_commit_oids = branch.upstream_head.map_or_else( || Ok(vec![]), - |upstream_head| { - project_repository.l( - upstream_head, - project_repository::LogUntil::Commit(default_target.sha), - ) - }, + |upstream_head| project_repository.l(upstream_head, LogUntil::Commit(default_target.sha)), )?; if pushed_commit_oids.contains(&parent_commit.id()) && !branch.allow_rebasing { @@ -3043,10 +3009,8 @@ pub fn update_commit_message( let default_target = vb_state.get_default_target()?; let mut branch = vb_state.get_branch(branch_id)?; - let branch_commit_oids = project_repository.l( - branch.head, - project_repository::LogUntil::Commit(default_target.sha), - )?; + let branch_commit_oids = + project_repository.l(branch.head, LogUntil::Commit(default_target.sha))?; if !branch_commit_oids.contains(&commit_id) { bail!("commit {commit_id} not in the branch"); @@ -3054,12 +3018,7 @@ pub fn update_commit_message( let pushed_commit_oids = branch.upstream_head.map_or_else( || Ok(vec![]), - |upstream_head| { - project_repository.l( - upstream_head, - project_repository::LogUntil::Commit(default_target.sha), - ) - }, + |upstream_head| project_repository.l(upstream_head, LogUntil::Commit(default_target.sha)), )?; if pushed_commit_oids.contains(&commit_id) && !branch.allow_rebasing { diff --git a/crates/gitbutler-core/src/project_repository/mod.rs b/crates/gitbutler-core/src/project_repository/mod.rs index 3aa8b3d24..019e0aff7 100644 --- a/crates/gitbutler-core/src/project_repository/mod.rs +++ b/crates/gitbutler-core/src/project_repository/mod.rs @@ -2,4 +2,4 @@ mod config; mod repository; pub use config::Config; -pub use repository::{LogUntil, ProjectRepo, RepoActions}; +pub use repository::ProjectRepo; diff --git a/crates/gitbutler-core/src/project_repository/repository.rs b/crates/gitbutler-core/src/project_repository/repository.rs index dd7ee2330..5076aaa47 100644 --- a/crates/gitbutler-core/src/project_repository/repository.rs +++ b/crates/gitbutler-core/src/project_repository/repository.rs @@ -1,18 +1,6 @@ -use std::str::FromStr; +use anyhow::Result; -use anyhow::{anyhow, Context, Result}; - -use crate::git::RepositoryExt; -use crate::{ - askpass, - git::{self}, - projects::{self, AuthKey}, - ssh, - virtual_branches::{Branch, BranchId}, -}; -use crate::{error::Code, git::CommitHeadersV2}; - -use super::Config; +use crate::projects; pub struct ProjectRepo { git_repository: git2::Repository, @@ -84,493 +72,3 @@ impl ProjectRepo { &self.git_repository } } - -pub trait RepoActions { - fn fetch( - &self, - remote_name: &str, - credentials: &git::credentials::Helper, - askpass: Option, - ) -> Result<()>; - fn push( - &self, - head: &git2::Oid, - branch: &git::RemoteRefname, - with_force: bool, - credentials: &git::credentials::Helper, - refspec: Option, - askpass_broker: Option>, - ) -> Result<()>; - fn commit( - &self, - message: &str, - tree: &git2::Tree, - parents: &[&git2::Commit], - commit_headers: Option, - ) -> Result; - fn distance(&self, from: git2::Oid, to: git2::Oid) -> Result; - fn log(&self, from: git2::Oid, to: LogUntil) -> Result>; - fn list_commits(&self, from: git2::Oid, to: git2::Oid) -> Result>; - fn list(&self, from: git2::Oid, to: git2::Oid) -> Result>; - fn l(&self, from: git2::Oid, to: LogUntil) -> Result>; - fn delete_branch_reference(&self, branch: &Branch) -> Result<()>; - fn add_branch_reference(&self, branch: &Branch) -> Result<()>; - fn git_test_push( - &self, - credentials: &git::credentials::Helper, - remote_name: &str, - branch_name: &str, - askpass: Option>, - ) -> Result<()>; -} - -impl RepoActions for ProjectRepo { - fn git_test_push( - &self, - credentials: &git::credentials::Helper, - remote_name: &str, - branch_name: &str, - askpass: Option>, - ) -> Result<()> { - let target_branch_refname = - git::Refname::from_str(&format!("refs/remotes/{}/{}", remote_name, branch_name))?; - let branch = self - .git_repository - .find_branch_by_refname(&target_branch_refname)? - .ok_or(anyhow!("failed to find branch {}", target_branch_refname))?; - - let commit_id: git2::Oid = branch.get().peel_to_commit()?.id(); - - let now = crate::time::now_ms(); - let branch_name = format!("test-push-{now}"); - - let refname = - git::RemoteRefname::from_str(&format!("refs/remotes/{remote_name}/{branch_name}",))?; - - match self.push(&commit_id, &refname, false, credentials, None, askpass) { - Ok(()) => Ok(()), - Err(e) => Err(anyhow::anyhow!(e.to_string())), - }?; - - let empty_refspec = Some(format!(":refs/heads/{}", branch_name)); - match self.push( - &commit_id, - &refname, - false, - credentials, - empty_refspec, - askpass, - ) { - Ok(()) => Ok(()), - Err(e) => Err(anyhow::anyhow!(e.to_string())), - }?; - - Ok(()) - } - - fn add_branch_reference(&self, branch: &Branch) -> Result<()> { - let (should_write, with_force) = match self - .git_repository - .find_reference(&branch.refname().to_string()) - { - Ok(reference) => match reference.target() { - Some(head_oid) => Ok((head_oid != branch.head, true)), - None => Ok((true, true)), - }, - Err(err) => match err.code() { - git2::ErrorCode::NotFound => Ok((true, false)), - _ => Err(err), - }, - } - .context("failed to lookup reference")?; - - if should_write { - self.git_repository - .reference( - &branch.refname().to_string(), - branch.head, - with_force, - "new vbranch", - ) - .context("failed to create branch reference")?; - } - - Ok(()) - } - - fn delete_branch_reference(&self, branch: &Branch) -> Result<()> { - match self - .git_repository - .find_reference(&branch.refname().to_string()) - { - Ok(mut reference) => { - reference - .delete() - .context("failed to delete branch reference")?; - Ok(()) - } - Err(err) => match err.code() { - git2::ErrorCode::NotFound => Ok(()), - _ => Err(err), - }, - } - .context("failed to lookup reference") - } - - // returns a list of commit oids from the first oid to the second oid - fn l(&self, from: git2::Oid, to: LogUntil) -> Result> { - match to { - LogUntil::Commit(oid) => { - let mut revwalk = self - .git_repository - .revwalk() - .context("failed to create revwalk")?; - revwalk - .push(from) - .context(format!("failed to push {}", from))?; - revwalk - .hide(oid) - .context(format!("failed to hide {}", oid))?; - revwalk - .map(|oid| oid.map(Into::into)) - .collect::, _>>() - } - LogUntil::Take(n) => { - let mut revwalk = self - .git_repository - .revwalk() - .context("failed to create revwalk")?; - revwalk - .push(from) - .context(format!("failed to push {}", from))?; - revwalk - .take(n) - .map(|oid| oid.map(Into::into)) - .collect::, _>>() - } - LogUntil::When(cond) => { - let mut revwalk = self - .git_repository - .revwalk() - .context("failed to create revwalk")?; - revwalk - .push(from) - .context(format!("failed to push {}", from))?; - let mut oids: Vec = vec![]; - for oid in revwalk { - let oid = oid.context("failed to get oid")?; - oids.push(oid); - - let commit = self - .git_repository - .find_commit(oid) - .context("failed to find commit")?; - - if cond(&commit).context("failed to check condition")? { - break; - } - } - Ok(oids) - } - LogUntil::End => { - let mut revwalk = self - .git_repository - .revwalk() - .context("failed to create revwalk")?; - revwalk - .push(from) - .context(format!("failed to push {}", from))?; - revwalk - .map(|oid| oid.map(Into::into)) - .collect::, _>>() - } - } - .context("failed to collect oids") - } - - // returns a list of oids from the first oid to the second oid - fn list(&self, from: git2::Oid, to: git2::Oid) -> Result> { - self.l(from, LogUntil::Commit(to)) - } - - fn list_commits(&self, from: git2::Oid, to: git2::Oid) -> Result> { - Ok(self - .list(from, to)? - .into_iter() - .map(|oid| self.git_repository.find_commit(oid)) - .collect::, _>>()?) - } - - // returns a list of commits from the first oid to the second oid - fn log(&self, from: git2::Oid, to: LogUntil) -> Result> { - self.l(from, to)? - .into_iter() - .map(|oid| self.git_repository.find_commit(oid)) - .collect::, _>>() - .context("failed to collect commits") - } - - // returns the number of commits between the first oid to the second oid - fn distance(&self, from: git2::Oid, to: git2::Oid) -> Result { - let oids = self.l(from, LogUntil::Commit(to))?; - Ok(oids.len().try_into()?) - } - - fn commit( - &self, - message: &str, - tree: &git2::Tree, - parents: &[&git2::Commit], - commit_headers: Option, - ) -> Result { - let (author, committer) = signatures(self).context("failed to get signatures")?; - self.repo() - .commit_with_signature( - None, - &author, - &committer, - message, - tree, - parents, - commit_headers, - ) - .context("failed to commit") - } - - fn push( - &self, - head: &git2::Oid, - branch: &git::RemoteRefname, - with_force: bool, - credentials: &git::credentials::Helper, - refspec: Option, - askpass_broker: Option>, - ) -> Result<()> { - let refspec = refspec.unwrap_or_else(|| { - if with_force { - format!("+{}:refs/heads/{}", head, branch.branch()) - } else { - format!("{}:refs/heads/{}", head, branch.branch()) - } - }); - - // NOTE(qix-): This is a nasty hack, however the codebase isn't structured - // NOTE(qix-): in a way that allows us to really incorporate new backends - // NOTE(qix-): without a lot of work. This is a temporary measure to - // NOTE(qix-): work around a time-sensitive change that was necessary - // NOTE(qix-): without having to refactor a large portion of the codebase. - if self.project.preferred_key == AuthKey::SystemExecutable { - let path = self.project.worktree_path(); - let remote = branch.remote().to_string(); - return std::thread::spawn(move || { - tokio::runtime::Runtime::new() - .unwrap() - .block_on(gitbutler_git::push( - path, - gitbutler_git::tokio::TokioExecutor, - &remote, - gitbutler_git::RefSpec::parse(refspec).unwrap(), - with_force, - handle_git_prompt_push, - askpass_broker, - )) - }) - .join() - .unwrap() - .map_err(Into::into); - } - - let auth_flows = credentials.help(self, branch.remote())?; - for (mut remote, callbacks) in auth_flows { - if let Some(url) = remote.url() { - if !self.project.omit_certificate_check.unwrap_or(false) { - let git_url = git::Url::from_str(url)?; - ssh::check_known_host(&git_url).context("failed to check known host")?; - } - } - let mut update_refs_error: Option = None; - for callback in callbacks { - let mut cbs: git2::RemoteCallbacks = callback.into(); - if self.project.omit_certificate_check.unwrap_or(false) { - cbs.certificate_check(|_, _| Ok(git2::CertificateCheckStatus::CertificateOk)); - } - cbs.push_update_reference(|_reference: &str, status: Option<&str>| { - if let Some(status) = status { - update_refs_error = Some(git2::Error::from_str(status)); - return Err(git2::Error::from_str(status)); - }; - Ok(()) - }); - - let push_result = remote.push( - &[refspec.as_str()], - Some(&mut git2::PushOptions::new().remote_callbacks(cbs)), - ); - match push_result { - Ok(()) => { - tracing::info!( - project_id = %self.project.id, - remote = %branch.remote(), - %head, - branch = branch.branch(), - "pushed git branch" - ); - return Ok(()); - } - Err(err) => match err.class() { - git2::ErrorClass::Net | git2::ErrorClass::Http => { - tracing::warn!(project_id = %self.project.id, ?err, "push failed due to network"); - continue; - } - _ => match err.code() { - git2::ErrorCode::Auth => { - tracing::warn!(project_id = %self.project.id, ?err, "push failed due to auth"); - continue; - } - _ => { - if let Some(update_refs_err) = update_refs_error { - return Err(update_refs_err).context(err); - } - return Err(err.into()); - } - }, - }, - } - } - } - - Err(anyhow!("authentication failed").context(Code::ProjectGitAuth)) - } - - fn fetch( - &self, - remote_name: &str, - credentials: &git::credentials::Helper, - askpass: Option, - ) -> Result<()> { - let refspec = format!("+refs/heads/*:refs/remotes/{}/*", remote_name); - - // NOTE(qix-): This is a nasty hack, however the codebase isn't structured - // NOTE(qix-): in a way that allows us to really incorporate new backends - // NOTE(qix-): without a lot of work. This is a temporary measure to - // NOTE(qix-): work around a time-sensitive change that was necessary - // NOTE(qix-): without having to refactor a large portion of the codebase. - if self.project.preferred_key == AuthKey::SystemExecutable { - let path = self.project.worktree_path(); - let remote = remote_name.to_string(); - return std::thread::spawn(move || { - tokio::runtime::Runtime::new() - .unwrap() - .block_on(gitbutler_git::fetch( - path, - gitbutler_git::tokio::TokioExecutor, - &remote, - gitbutler_git::RefSpec::parse(refspec).unwrap(), - handle_git_prompt_fetch, - askpass, - )) - }) - .join() - .unwrap() - .map_err(Into::into); - } - - let auth_flows = credentials.help(self, remote_name)?; - for (mut remote, callbacks) in auth_flows { - if let Some(url) = remote.url() { - if !self.project.omit_certificate_check.unwrap_or(false) { - let git_url = git::Url::from_str(url)?; - ssh::check_known_host(&git_url).context("failed to check known host")?; - } - } - for callback in callbacks { - let mut fetch_opts = git2::FetchOptions::new(); - let mut cbs: git2::RemoteCallbacks = callback.into(); - if self.project.omit_certificate_check.unwrap_or(false) { - cbs.certificate_check(|_, _| Ok(git2::CertificateCheckStatus::CertificateOk)); - } - fetch_opts.remote_callbacks(cbs); - fetch_opts.prune(git2::FetchPrune::On); - - match remote.fetch(&[&refspec], Some(&mut fetch_opts), None) { - Ok(()) => { - tracing::info!(project_id = %self.project.id, %refspec, "git fetched"); - return Ok(()); - } - Err(err) => match err.class() { - git2::ErrorClass::Net | git2::ErrorClass::Http => { - tracing::warn!(project_id = %self.project.id, ?err, "fetch failed due to network"); - continue; - } - _ => match err.code() { - git2::ErrorCode::Auth => { - tracing::warn!(project_id = %self.project.id, ?err, "fetch failed due to auth"); - continue; - } - _ => { - return Err(err.into()); - } - }, - }, - } - } - } - - Err(anyhow!("authentication failed")).context(Code::ProjectGitAuth) - } -} - -fn signatures(project_repo: &ProjectRepo) -> Result<(git2::Signature, git2::Signature)> { - let config: Config = project_repo.repo().into(); - - let author = match (config.user_name()?, config.user_email()?) { - (None, Some(email)) => git2::Signature::now(&email, &email)?, - (Some(name), None) => git2::Signature::now(&name, &format!("{}@example.com", &name))?, - (Some(name), Some(email)) => git2::Signature::now(&name, &email)?, - _ => git2::Signature::now("GitButler", "gitbutler@gitbutler.com")?, - }; - - let comitter = if config.user_real_comitter()? { - author.clone() - } else { - git2::Signature::now("GitButler", "gitbutler@gitbutler.com")? - }; - - Ok((author, comitter)) -} - -type OidFilter = dyn Fn(&git2::Commit) -> Result; - -pub enum LogUntil { - Commit(git2::Oid), - Take(usize), - When(Box), - End, -} - -async fn handle_git_prompt_push( - prompt: String, - askpass: Option>, -) -> Option { - if let Some(branch_id) = askpass { - tracing::info!("received prompt for branch push {branch_id:?}: {prompt:?}"); - askpass::get_broker() - .submit_prompt(prompt, askpass::Context::Push { branch_id }) - .await - } else { - tracing::warn!("received askpass push prompt but no broker was supplied; returning None"); - None - } -} - -async fn handle_git_prompt_fetch(prompt: String, askpass: Option) -> Option { - if let Some(action) = askpass { - tracing::info!("received prompt for fetch with action {action:?}: {prompt:?}"); - askpass::get_broker() - .submit_prompt(prompt, askpass::Context::Fetch { action }) - .await - } else { - tracing::warn!("received askpass fetch prompt but no broker was supplied; returning None"); - None - } -} diff --git a/crates/gitbutler-repo/Cargo.toml b/crates/gitbutler-repo/Cargo.toml index fa14ce85b..3b0f3775d 100644 --- a/crates/gitbutler-repo/Cargo.toml +++ b/crates/gitbutler-repo/Cargo.toml @@ -10,3 +10,6 @@ git2.workspace = true gitbutler-core.workspace = true anyhow = "1.0.86" bstr = "1.9.1" +tokio = { workspace = true, features = [ "rt-multi-thread", "rt", "macros" ] } +gitbutler-git.workspace = true +tracing = "0.1.40" diff --git a/crates/gitbutler-repo/src/lib.rs b/crates/gitbutler-repo/src/lib.rs index ce00534e8..484474187 100644 --- a/crates/gitbutler-repo/src/lib.rs +++ b/crates/gitbutler-repo/src/lib.rs @@ -1 +1,4 @@ pub mod rebase; + +mod repository; +pub use repository::{LogUntil, RepoActions}; diff --git a/crates/gitbutler-repo/src/rebase.rs b/crates/gitbutler-repo/src/rebase.rs index a1cc41331..6af2a8b69 100644 --- a/crates/gitbutler-repo/src/rebase.rs +++ b/crates/gitbutler-repo/src/rebase.rs @@ -1,9 +1,10 @@ use anyhow::{anyhow, Context, Result}; use bstr::ByteSlice; use gitbutler_core::git::HasCommitHeaders; -use gitbutler_core::project_repository::RepoActions; use gitbutler_core::{error::Marker, git::CommitExt, git::RepositoryExt, project_repository}; +use crate::{LogUntil, RepoActions}; + /// cherry-pick based rebase, which handles empty commits /// this function takes a commit range and generates a Vector of commit oids /// and then passes them to `cherry_rebase_group` to rebase them onto the target commit @@ -14,10 +15,8 @@ pub fn cherry_rebase( end_commit_oid: git2::Oid, ) -> Result> { // get a list of the commits to rebase - let mut ids_to_rebase = project_repository.l( - end_commit_oid, - project_repository::LogUntil::Commit(start_commit_oid), - )?; + let mut ids_to_rebase = + project_repository.l(end_commit_oid, LogUntil::Commit(start_commit_oid))?; if ids_to_rebase.is_empty() { return Ok(None); diff --git a/crates/gitbutler-repo/src/repository.rs b/crates/gitbutler-repo/src/repository.rs new file mode 100644 index 000000000..64e5256ac --- /dev/null +++ b/crates/gitbutler-repo/src/repository.rs @@ -0,0 +1,487 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Context, Result}; + +use gitbutler_core::git::RepositoryExt; +use gitbutler_core::{ + askpass, + error::Code, + git::{self, CommitHeadersV2}, + projects::AuthKey, + ssh, + virtual_branches::{Branch, BranchId}, +}; + +use gitbutler_core::project_repository::{Config, ProjectRepo}; +pub trait RepoActions { + fn fetch( + &self, + remote_name: &str, + credentials: &git::credentials::Helper, + askpass: Option, + ) -> Result<()>; + fn push( + &self, + head: &git2::Oid, + branch: &git::RemoteRefname, + with_force: bool, + credentials: &git::credentials::Helper, + refspec: Option, + askpass_broker: Option>, + ) -> Result<()>; + fn commit( + &self, + message: &str, + tree: &git2::Tree, + parents: &[&git2::Commit], + commit_headers: Option, + ) -> Result; + fn distance(&self, from: git2::Oid, to: git2::Oid) -> Result; + fn log(&self, from: git2::Oid, to: LogUntil) -> Result>; + fn list_commits(&self, from: git2::Oid, to: git2::Oid) -> Result>; + fn list(&self, from: git2::Oid, to: git2::Oid) -> Result>; + fn l(&self, from: git2::Oid, to: LogUntil) -> Result>; + fn delete_branch_reference(&self, branch: &Branch) -> Result<()>; + fn add_branch_reference(&self, branch: &Branch) -> Result<()>; + fn git_test_push( + &self, + credentials: &git::credentials::Helper, + remote_name: &str, + branch_name: &str, + askpass: Option>, + ) -> Result<()>; +} + +impl RepoActions for ProjectRepo { + fn git_test_push( + &self, + credentials: &git::credentials::Helper, + remote_name: &str, + branch_name: &str, + askpass: Option>, + ) -> Result<()> { + let target_branch_refname = + git::Refname::from_str(&format!("refs/remotes/{}/{}", remote_name, branch_name))?; + let branch = self + .repo() + .find_branch_by_refname(&target_branch_refname)? + .ok_or(anyhow!("failed to find branch {}", target_branch_refname))?; + + let commit_id: git2::Oid = branch.get().peel_to_commit()?.id(); + + let now = gitbutler_core::time::now_ms(); + let branch_name = format!("test-push-{now}"); + + let refname = + git::RemoteRefname::from_str(&format!("refs/remotes/{remote_name}/{branch_name}",))?; + + match self.push(&commit_id, &refname, false, credentials, None, askpass) { + Ok(()) => Ok(()), + Err(e) => Err(anyhow::anyhow!(e.to_string())), + }?; + + let empty_refspec = Some(format!(":refs/heads/{}", branch_name)); + match self.push( + &commit_id, + &refname, + false, + credentials, + empty_refspec, + askpass, + ) { + Ok(()) => Ok(()), + Err(e) => Err(anyhow::anyhow!(e.to_string())), + }?; + + Ok(()) + } + + fn add_branch_reference(&self, branch: &Branch) -> Result<()> { + let (should_write, with_force) = + match self.repo().find_reference(&branch.refname().to_string()) { + Ok(reference) => match reference.target() { + Some(head_oid) => Ok((head_oid != branch.head, true)), + None => Ok((true, true)), + }, + Err(err) => match err.code() { + git2::ErrorCode::NotFound => Ok((true, false)), + _ => Err(err), + }, + } + .context("failed to lookup reference")?; + + if should_write { + self.repo() + .reference( + &branch.refname().to_string(), + branch.head, + with_force, + "new vbranch", + ) + .context("failed to create branch reference")?; + } + + Ok(()) + } + + fn delete_branch_reference(&self, branch: &Branch) -> Result<()> { + match self.repo().find_reference(&branch.refname().to_string()) { + Ok(mut reference) => { + reference + .delete() + .context("failed to delete branch reference")?; + Ok(()) + } + Err(err) => match err.code() { + git2::ErrorCode::NotFound => Ok(()), + _ => Err(err), + }, + } + .context("failed to lookup reference") + } + + // returns a list of commit oids from the first oid to the second oid + fn l(&self, from: git2::Oid, to: LogUntil) -> Result> { + match to { + LogUntil::Commit(oid) => { + let mut revwalk = self.repo().revwalk().context("failed to create revwalk")?; + revwalk + .push(from) + .context(format!("failed to push {}", from))?; + revwalk + .hide(oid) + .context(format!("failed to hide {}", oid))?; + revwalk + .map(|oid| oid.map(Into::into)) + .collect::, _>>() + } + LogUntil::Take(n) => { + let mut revwalk = self.repo().revwalk().context("failed to create revwalk")?; + revwalk + .push(from) + .context(format!("failed to push {}", from))?; + revwalk + .take(n) + .map(|oid| oid.map(Into::into)) + .collect::, _>>() + } + LogUntil::When(cond) => { + let mut revwalk = self.repo().revwalk().context("failed to create revwalk")?; + revwalk + .push(from) + .context(format!("failed to push {}", from))?; + let mut oids: Vec = vec![]; + for oid in revwalk { + let oid = oid.context("failed to get oid")?; + oids.push(oid); + + let commit = self + .repo() + .find_commit(oid) + .context("failed to find commit")?; + + if cond(&commit).context("failed to check condition")? { + break; + } + } + Ok(oids) + } + LogUntil::End => { + let mut revwalk = self.repo().revwalk().context("failed to create revwalk")?; + revwalk + .push(from) + .context(format!("failed to push {}", from))?; + revwalk + .map(|oid| oid.map(Into::into)) + .collect::, _>>() + } + } + .context("failed to collect oids") + } + + // returns a list of oids from the first oid to the second oid + fn list(&self, from: git2::Oid, to: git2::Oid) -> Result> { + self.l(from, LogUntil::Commit(to)) + } + + fn list_commits(&self, from: git2::Oid, to: git2::Oid) -> Result> { + Ok(self + .list(from, to)? + .into_iter() + .map(|oid| self.repo().find_commit(oid)) + .collect::, _>>()?) + } + + // returns a list of commits from the first oid to the second oid + fn log(&self, from: git2::Oid, to: LogUntil) -> Result> { + self.l(from, to)? + .into_iter() + .map(|oid| self.repo().find_commit(oid)) + .collect::, _>>() + .context("failed to collect commits") + } + + // returns the number of commits between the first oid to the second oid + fn distance(&self, from: git2::Oid, to: git2::Oid) -> Result { + let oids = self.l(from, LogUntil::Commit(to))?; + Ok(oids.len().try_into()?) + } + + fn commit( + &self, + message: &str, + tree: &git2::Tree, + parents: &[&git2::Commit], + commit_headers: Option, + ) -> Result { + let (author, committer) = signatures(self).context("failed to get signatures")?; + self.repo() + .commit_with_signature( + None, + &author, + &committer, + message, + tree, + parents, + commit_headers, + ) + .context("failed to commit") + } + + fn push( + &self, + head: &git2::Oid, + branch: &git::RemoteRefname, + with_force: bool, + credentials: &git::credentials::Helper, + refspec: Option, + askpass_broker: Option>, + ) -> Result<()> { + let refspec = refspec.unwrap_or_else(|| { + if with_force { + format!("+{}:refs/heads/{}", head, branch.branch()) + } else { + format!("{}:refs/heads/{}", head, branch.branch()) + } + }); + + // NOTE(qix-): This is a nasty hack, however the codebase isn't structured + // NOTE(qix-): in a way that allows us to really incorporate new backends + // NOTE(qix-): without a lot of work. This is a temporary measure to + // NOTE(qix-): work around a time-sensitive change that was necessary + // NOTE(qix-): without having to refactor a large portion of the codebase. + if self.project().preferred_key == AuthKey::SystemExecutable { + let path = self.project().worktree_path(); + let remote = branch.remote().to_string(); + return std::thread::spawn(move || { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(gitbutler_git::push( + path, + gitbutler_git::tokio::TokioExecutor, + &remote, + gitbutler_git::RefSpec::parse(refspec).unwrap(), + with_force, + handle_git_prompt_push, + askpass_broker, + )) + }) + .join() + .unwrap() + .map_err(Into::into); + } + + let auth_flows = credentials.help(self, branch.remote())?; + for (mut remote, callbacks) in auth_flows { + if let Some(url) = remote.url() { + if !self.project().omit_certificate_check.unwrap_or(false) { + let git_url = git::Url::from_str(url)?; + ssh::check_known_host(&git_url).context("failed to check known host")?; + } + } + let mut update_refs_error: Option = None; + for callback in callbacks { + let mut cbs: git2::RemoteCallbacks = callback.into(); + if self.project().omit_certificate_check.unwrap_or(false) { + cbs.certificate_check(|_, _| Ok(git2::CertificateCheckStatus::CertificateOk)); + } + cbs.push_update_reference(|_reference: &str, status: Option<&str>| { + if let Some(status) = status { + update_refs_error = Some(git2::Error::from_str(status)); + return Err(git2::Error::from_str(status)); + }; + Ok(()) + }); + + let push_result = remote.push( + &[refspec.as_str()], + Some(&mut git2::PushOptions::new().remote_callbacks(cbs)), + ); + match push_result { + Ok(()) => { + tracing::info!( + project_id = %self.project().id, + remote = %branch.remote(), + %head, + branch = branch.branch(), + "pushed git branch" + ); + return Ok(()); + } + Err(err) => match err.class() { + git2::ErrorClass::Net | git2::ErrorClass::Http => { + tracing::warn!(project_id = %self.project().id, ?err, "push failed due to network"); + continue; + } + _ => match err.code() { + git2::ErrorCode::Auth => { + tracing::warn!(project_id = %self.project().id, ?err, "push failed due to auth"); + continue; + } + _ => { + if let Some(update_refs_err) = update_refs_error { + return Err(update_refs_err).context(err); + } + return Err(err.into()); + } + }, + }, + } + } + } + + Err(anyhow!("authentication failed").context(Code::ProjectGitAuth)) + } + + fn fetch( + &self, + remote_name: &str, + credentials: &git::credentials::Helper, + askpass: Option, + ) -> Result<()> { + let refspec = format!("+refs/heads/*:refs/remotes/{}/*", remote_name); + + // NOTE(qix-): This is a nasty hack, however the codebase isn't structured + // NOTE(qix-): in a way that allows us to really incorporate new backends + // NOTE(qix-): without a lot of work. This is a temporary measure to + // NOTE(qix-): work around a time-sensitive change that was necessary + // NOTE(qix-): without having to refactor a large portion of the codebase. + if self.project().preferred_key == AuthKey::SystemExecutable { + let path = self.project().worktree_path(); + let remote = remote_name.to_string(); + return std::thread::spawn(move || { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(gitbutler_git::fetch( + path, + gitbutler_git::tokio::TokioExecutor, + &remote, + gitbutler_git::RefSpec::parse(refspec).unwrap(), + handle_git_prompt_fetch, + askpass, + )) + }) + .join() + .unwrap() + .map_err(Into::into); + } + + let auth_flows = credentials.help(self, remote_name)?; + for (mut remote, callbacks) in auth_flows { + if let Some(url) = remote.url() { + if !self.project().omit_certificate_check.unwrap_or(false) { + let git_url = git::Url::from_str(url)?; + ssh::check_known_host(&git_url).context("failed to check known host")?; + } + } + for callback in callbacks { + let mut fetch_opts = git2::FetchOptions::new(); + let mut cbs: git2::RemoteCallbacks = callback.into(); + if self.project().omit_certificate_check.unwrap_or(false) { + cbs.certificate_check(|_, _| Ok(git2::CertificateCheckStatus::CertificateOk)); + } + fetch_opts.remote_callbacks(cbs); + fetch_opts.prune(git2::FetchPrune::On); + + match remote.fetch(&[&refspec], Some(&mut fetch_opts), None) { + Ok(()) => { + tracing::info!(project_id = %self.project().id, %refspec, "git fetched"); + return Ok(()); + } + Err(err) => match err.class() { + git2::ErrorClass::Net | git2::ErrorClass::Http => { + tracing::warn!(project_id = %self.project().id, ?err, "fetch failed due to network"); + continue; + } + _ => match err.code() { + git2::ErrorCode::Auth => { + tracing::warn!(project_id = %self.project().id, ?err, "fetch failed due to auth"); + continue; + } + _ => { + return Err(err.into()); + } + }, + }, + } + } + } + + Err(anyhow!("authentication failed")).context(Code::ProjectGitAuth) + } +} + +fn signatures(project_repo: &ProjectRepo) -> Result<(git2::Signature, git2::Signature)> { + let config: Config = project_repo.repo().into(); + + let author = match (config.user_name()?, config.user_email()?) { + (None, Some(email)) => git2::Signature::now(&email, &email)?, + (Some(name), None) => git2::Signature::now(&name, &format!("{}@example.com", &name))?, + (Some(name), Some(email)) => git2::Signature::now(&name, &email)?, + _ => git2::Signature::now("GitButler", "gitbutler@gitbutler.com")?, + }; + + let comitter = if config.user_real_comitter()? { + author.clone() + } else { + git2::Signature::now("GitButler", "gitbutler@gitbutler.com")? + }; + + Ok((author, comitter)) +} + +type OidFilter = dyn Fn(&git2::Commit) -> Result; + +pub enum LogUntil { + Commit(git2::Oid), + Take(usize), + When(Box), + End, +} + +async fn handle_git_prompt_push( + prompt: String, + askpass: Option>, +) -> Option { + if let Some(branch_id) = askpass { + tracing::info!("received prompt for branch push {branch_id:?}: {prompt:?}"); + askpass::get_broker() + .submit_prompt(prompt, askpass::Context::Push { branch_id }) + .await + } else { + tracing::warn!("received askpass push prompt but no broker was supplied; returning None"); + None + } +} + +async fn handle_git_prompt_fetch(prompt: String, askpass: Option) -> Option { + if let Some(action) = askpass { + tracing::info!("received prompt for fetch with action {action:?}: {prompt:?}"); + askpass::get_broker() + .submit_prompt(prompt, askpass::Context::Fetch { action }) + .await + } else { + tracing::warn!("received askpass fetch prompt but no broker was supplied; returning None"); + None + } +} diff --git a/crates/gitbutler-tauri/Cargo.toml b/crates/gitbutler-tauri/Cargo.toml index 0e5a88747..5edcff5e8 100644 --- a/crates/gitbutler-tauri/Cargo.toml +++ b/crates/gitbutler-tauri/Cargo.toml @@ -50,6 +50,7 @@ gitbutler-core.workspace = true gitbutler-watcher.workspace = true gitbutler-branch.workspace = true gitbutler-oplog.workspace = true +gitbutler-repo.workspace = true open = "5" [dependencies.tauri] diff --git a/crates/gitbutler-tauri/src/app.rs b/crates/gitbutler-tauri/src/app.rs index 554c42c49..bc4c2ed68 100644 --- a/crates/gitbutler-tauri/src/app.rs +++ b/crates/gitbutler-tauri/src/app.rs @@ -2,10 +2,11 @@ use anyhow::{Context, Result}; use gitbutler_branch::conflicts; use gitbutler_core::{ git::{self, RepositoryExt}, - project_repository::{self, RepoActions}, + project_repository, projects::{self, ProjectId}, virtual_branches::BranchId, }; +use gitbutler_repo::RepoActions; #[derive(Clone)] pub struct App {