mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 20:54:50 +03:00
move RepoActions to gitbutler-repo crate
This commit is contained in:
parent
90670def3b
commit
f92df0cde7
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -2,4 +2,4 @@ mod config;
|
||||
mod repository;
|
||||
|
||||
pub use config::Config;
|
||||
pub use repository::{LogUntil, ProjectRepo, RepoActions};
|
||||
pub use repository::ProjectRepo;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -1 +1,4 @@
|
||||
pub mod rebase;
|
||||
|
||||
mod repository;
|
||||
pub use repository::{LogUntil, RepoActions};
|
||||
|
@ -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);
|
||||
|
487
crates/gitbutler-repo/src/repository.rs
Normal file
487
crates/gitbutler-repo/src/repository.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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]
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user