move RepoActions to gitbutler-repo crate

This commit is contained in:
Kiril Videlov 2024-07-08 00:45:04 +02:00
parent 90670def3b
commit f92df0cde7
No known key found for this signature in database
GPG Key ID: A4C733025427C471
14 changed files with 538 additions and 584 deletions

4
Cargo.lock generated
View File

@ -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",

View File

@ -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)

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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<git2::Oid> = 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 {

View File

@ -2,4 +2,4 @@ mod config;
mod repository;
pub use config::Config;
pub use repository::{LogUntil, ProjectRepo, RepoActions};
pub use repository::ProjectRepo;

View File

@ -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<String>,
) -> Result<()>;
fn push(
&self,
head: &git2::Oid,
branch: &git::RemoteRefname,
with_force: bool,
credentials: &git::credentials::Helper,
refspec: Option<String>,
askpass_broker: Option<Option<BranchId>>,
) -> Result<()>;
fn commit(
&self,
message: &str,
tree: &git2::Tree,
parents: &[&git2::Commit],
commit_headers: Option<CommitHeadersV2>,
) -> Result<git2::Oid>;
fn distance(&self, from: git2::Oid, to: git2::Oid) -> Result<u32>;
fn log(&self, from: git2::Oid, to: LogUntil) -> Result<Vec<git2::Commit>>;
fn list_commits(&self, from: git2::Oid, to: git2::Oid) -> Result<Vec<git2::Commit>>;
fn list(&self, from: git2::Oid, to: git2::Oid) -> Result<Vec<git2::Oid>>;
fn l(&self, from: git2::Oid, to: LogUntil) -> Result<Vec<git2::Oid>>;
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<Option<BranchId>>,
) -> Result<()>;
}
impl RepoActions for ProjectRepo {
fn git_test_push(
&self,
credentials: &git::credentials::Helper,
remote_name: &str,
branch_name: &str,
askpass: Option<Option<BranchId>>,
) -> 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<Vec<git2::Oid>> {
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::<Result<Vec<_>, _>>()
}
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::<Result<Vec<_>, _>>()
}
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<git2::Oid> = 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::<Result<Vec<_>, _>>()
}
}
.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<Vec<git2::Oid>> {
self.l(from, LogUntil::Commit(to))
}
fn list_commits(&self, from: git2::Oid, to: git2::Oid) -> Result<Vec<git2::Commit>> {
Ok(self
.list(from, to)?
.into_iter()
.map(|oid| self.git_repository.find_commit(oid))
.collect::<Result<Vec<_>, _>>()?)
}
// returns a list of commits from the first oid to the second oid
fn log(&self, from: git2::Oid, to: LogUntil) -> Result<Vec<git2::Commit>> {
self.l(from, to)?
.into_iter()
.map(|oid| self.git_repository.find_commit(oid))
.collect::<Result<Vec<_>, _>>()
.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<u32> {
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<CommitHeadersV2>,
) -> Result<git2::Oid> {
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<String>,
askpass_broker: Option<Option<BranchId>>,
) -> 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<git2::Error> = 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<String>,
) -> 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<bool>;
pub enum LogUntil {
Commit(git2::Oid),
Take(usize),
When(Box<OidFilter>),
End,
}
async fn handle_git_prompt_push(
prompt: String,
askpass: Option<Option<BranchId>>,
) -> Option<String> {
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<String>) -> Option<String> {
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
}
}

View File

@ -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"

View File

@ -1 +1,4 @@
pub mod rebase;
mod repository;
pub use repository::{LogUntil, RepoActions};

View File

@ -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<Option<git2::Oid>> {
// 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);

View File

@ -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<String>,
) -> Result<()>;
fn push(
&self,
head: &git2::Oid,
branch: &git::RemoteRefname,
with_force: bool,
credentials: &git::credentials::Helper,
refspec: Option<String>,
askpass_broker: Option<Option<BranchId>>,
) -> Result<()>;
fn commit(
&self,
message: &str,
tree: &git2::Tree,
parents: &[&git2::Commit],
commit_headers: Option<CommitHeadersV2>,
) -> Result<git2::Oid>;
fn distance(&self, from: git2::Oid, to: git2::Oid) -> Result<u32>;
fn log(&self, from: git2::Oid, to: LogUntil) -> Result<Vec<git2::Commit>>;
fn list_commits(&self, from: git2::Oid, to: git2::Oid) -> Result<Vec<git2::Commit>>;
fn list(&self, from: git2::Oid, to: git2::Oid) -> Result<Vec<git2::Oid>>;
fn l(&self, from: git2::Oid, to: LogUntil) -> Result<Vec<git2::Oid>>;
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<Option<BranchId>>,
) -> Result<()>;
}
impl RepoActions for ProjectRepo {
fn git_test_push(
&self,
credentials: &git::credentials::Helper,
remote_name: &str,
branch_name: &str,
askpass: Option<Option<BranchId>>,
) -> 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<Vec<git2::Oid>> {
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::<Result<Vec<_>, _>>()
}
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::<Result<Vec<_>, _>>()
}
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<git2::Oid> = 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::<Result<Vec<_>, _>>()
}
}
.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<Vec<git2::Oid>> {
self.l(from, LogUntil::Commit(to))
}
fn list_commits(&self, from: git2::Oid, to: git2::Oid) -> Result<Vec<git2::Commit>> {
Ok(self
.list(from, to)?
.into_iter()
.map(|oid| self.repo().find_commit(oid))
.collect::<Result<Vec<_>, _>>()?)
}
// returns a list of commits from the first oid to the second oid
fn log(&self, from: git2::Oid, to: LogUntil) -> Result<Vec<git2::Commit>> {
self.l(from, to)?
.into_iter()
.map(|oid| self.repo().find_commit(oid))
.collect::<Result<Vec<_>, _>>()
.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<u32> {
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<CommitHeadersV2>,
) -> Result<git2::Oid> {
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<String>,
askpass_broker: Option<Option<BranchId>>,
) -> 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<git2::Error> = 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<String>,
) -> 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<bool>;
pub enum LogUntil {
Commit(git2::Oid),
Take(usize),
When(Box<OidFilter>),
End,
}
async fn handle_git_prompt_push(
prompt: String,
askpass: Option<Option<BranchId>>,
) -> Option<String> {
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<String>) -> Option<String> {
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
}
}

View File

@ -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]

View File

@ -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 {