merge stuff

This commit is contained in:
Scott Chacon 2024-05-11 11:43:06 +02:00
commit 4d542330c5
31 changed files with 419 additions and 172 deletions

View File

@ -45,6 +45,14 @@
});
return resp;
}
async function getSnapshotDiff(projectId: string, sha: string) {
const resp = await invoke<string>('snapshot_diff', {
projectId: projectId,
sha: sha
});
console.log(JSON.stringify(resp));
return resp;
}
async function restoreSnapshot(projectId: string, sha: string) {
await invoke<string>('restore_snapshot', {
projectId: projectId,
@ -80,6 +88,16 @@
<div style="display: flex; align-items: center;">
<div>id: {entry.id.slice(0, 7)}</div>
<div style="flex-grow: 1;" />
<div>
{#if entry.linesAdded + entry.linesRemoved > 0}
<Button
style="pop"
size="tag"
icon="docs-filled"
on:click={async () => await getSnapshotDiff(projectId, entry.id)}>diff</Button
>
{/if}
</div>
<div>
{#if idx != 0}
<Button
@ -104,6 +122,15 @@
restored_from: {entry.details?.trailers
.find((t) => t.key === 'restored_from')
?.value?.slice(0, 7)}
{:else if entry.details?.operation === 'DeleteBranch'}
name: {entry.details?.trailers.find((t) => t.key === 'name')?.value}
{:else if ['ReorderBranches', 'UpdateBranchName', 'SelectDefaultVirtualBranch', 'UpdateBranchRemoteName'].includes(entry.details?.operation || '')}
<div>
before: {entry.details?.trailers.find((t) => t.key === 'before')?.value}
</div>
<div>
after: {entry.details?.trailers.find((t) => t.key === 'after')?.value}
</div>
{/if}
</div>
<div>

View File

@ -79,6 +79,8 @@
});
</script>
<svelte:window on:drop={(e) => e.preventDefault()} on:dragover={(e) => e.preventDefault()} />
<div
data-tauri-drag-region
class="app-root"

View File

@ -1,5 +1,5 @@
use anyhow::Result;
use gitbutler_core::{projects::Project, snapshots::snapshot::Oplog};
use gitbutler_core::{ops::oplog::Oplog, projects::Project};
use clap::{arg, Command};
#[cfg(not(windows))]

View File

@ -1,10 +1,35 @@
use std::{collections::HashMap, sync::Arc};
use std::{collections::HashMap, path::Path, sync::Arc};
use serde::Serialize;
use tokio::sync::{oneshot, Mutex};
use crate::{id::Id, virtual_branches::BranchId};
static mut GLOBAL_ASKPASS_BROKER: Option<AskpassBroker> = None;
/// Initialize the global askpass broker.
///
/// # Safety
/// This function **must** be called **at least once**, from only one thread at a time,
/// before any other function from this module is called. **Calls to [`get`] before [`init`] will panic.**
///
/// This function is **NOT** thread safe.
pub unsafe fn init(submit_prompt: impl Fn(PromptEvent<Context>) + Send + Sync + 'static) {
GLOBAL_ASKPASS_BROKER.replace(AskpassBroker::init(submit_prompt));
}
/// Get the global askpass broker.
///
/// # Panics
/// Will panic if [`init`] was not called before this function.
pub fn get_broker() -> &'static AskpassBroker {
unsafe {
GLOBAL_ASKPASS_BROKER
.as_ref()
.expect("askpass broker not initialized")
}
}
pub struct AskpassRequest {
sender: oneshot::Sender<Option<String>>,
}
@ -15,6 +40,7 @@ pub struct AskpassRequest {
pub enum Context {
Push { branch_id: Option<BranchId> },
Fetch { action: String },
SignedCommit { branch_id: Option<BranchId> },
}
#[derive(Clone)]
@ -60,3 +86,43 @@ impl AskpassBroker {
}
}
}
async fn handle_git_prompt_commit_sign_sync(
prompt: String,
branch_id: Option<BranchId>,
) -> Option<String> {
tracing::info!("received prompt for synchronous signed commit {branch_id:?}: {prompt:?}");
get_broker()
.submit_prompt(prompt, Context::SignedCommit { branch_id })
.await
}
/// Utility to synchronously sign a commit.
/// Uses the Tokio runner to run the async function,
/// and the global askpass broker to handle any prompts.
pub fn sign_commit_sync(
repo_path: impl AsRef<Path>,
base_commitish: impl AsRef<str>,
branch_id: Option<BranchId>,
) -> Result<String, impl std::error::Error> {
let repo_path = repo_path.as_ref().to_path_buf();
let base_commitish: &str = base_commitish.as_ref();
let base_commitish = base_commitish.to_string();
// Run as sync
let handle = std::thread::spawn(move || {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(gitbutler_git::sign_commit(
&repo_path,
gitbutler_git::tokio::TokioExecutor,
base_commitish,
handle_git_prompt_commit_sign_sync,
branch_id,
))
});
tokio::task::block_in_place(|| handle.join().unwrap())
}

View File

@ -218,7 +218,7 @@ pub fn without_large_files(
/// `repository` should be `None` if there is no reason to access the workdir, which it will do to
/// keep the binary data in the object database, which otherwise would be lost to the system
/// (it's not reconstructable from the delta, or it's not attempted).
fn hunks_by_filepath(repo: Option<&Repository>, diff: &git2::Diff) -> Result<DiffByPathMap> {
pub fn hunks_by_filepath(repo: Option<&Repository>, diff: &git2::Diff) -> Result<DiffByPathMap> {
enum LineOrHexHash<'a> {
Line(Cow<'a, BStr>),
HexHashOfBinaryBlob(String),

View File

@ -25,12 +25,12 @@ pub mod git;
pub mod id;
pub mod keys;
pub mod lock;
pub mod ops;
pub mod path;
pub mod project_repository;
pub mod projects;
pub mod reader;
pub mod sessions;
pub mod snapshots;
pub mod ssh;
pub mod storage;
pub mod time;

View File

@ -59,6 +59,10 @@ impl SnapshotDetails {
trailers: vec![],
}
}
pub fn with_trailers(mut self, trailers: Vec<Trailer>) -> Self {
self.trailers = trailers;
self
}
}
impl FromStr for SnapshotDetails {

View File

@ -1,4 +1,5 @@
pub mod entry;
pub mod oplog;
mod reflog;
pub mod snapshot;
mod state;

View File

@ -1,12 +1,14 @@
use anyhow::anyhow;
use git2::FileMode;
use itertools::Itertools;
use std::fs;
use std::collections::HashMap;
use std::str::FromStr;
use std::{fs, path::PathBuf};
use anyhow::Result;
use crate::projects::Project;
use crate::git::diff::FileDiff;
use crate::{git::diff::hunks_by_filepath, projects::Project};
use super::{
entry::{OperationType, Snapshot, SnapshotDetails, Trailer},
@ -54,6 +56,10 @@ pub trait Oplog {
///
/// If there are no snapshots, 0 is returned.
fn lines_since_snapshot(&self) -> Result<usize>;
/// Returns the diff of the snapshot and it's parent. It only includes the workdir changes.
///
/// This is useful to show what has changed in this particular snapshot
fn snapshot_diff(&self, sha: String) -> Result<HashMap<PathBuf, FileDiff>>;
}
impl Oplog for Project {
@ -110,6 +116,13 @@ impl Oplog for Project {
let tree_id = tree_builder.write()?;
let tree = repo.find_tree(tree_id)?;
// Check if there is a difference between the tree and the parent tree, and if not, return so that we dont create noop snapshots
let parent_tree = oplog_head_commit.tree()?;
let diff = repo.diff_tree_to_tree(Some(&parent_tree), Some(&tree), None)?;
if diff.deltas().count() == 0 {
return Ok(None);
}
// Construct a new commit
let name = "GitButler";
let email = "gitbutler@gitbutler.com";
@ -317,6 +330,46 @@ impl Oplog for Project {
let stats = diff?.stats()?;
Ok(stats.deletions() + stats.insertions())
}
fn snapshot_diff(&self, sha: String) -> Result<HashMap<PathBuf, FileDiff>> {
let repo_path = self.path.as_path();
let repo = git2::Repository::init(repo_path)?;
let commit = repo.find_commit(git2::Oid::from_str(&sha)?)?;
// Top tree
let tree = commit.tree()?;
let old_tree = commit.parent(0)?.tree()?;
let wd_tree_entry = tree
.get_name("workdir")
.ok_or(anyhow!("failed to get workdir tree entry"))?;
let old_wd_tree_entry = old_tree
.get_name("workdir")
.ok_or(anyhow!("failed to get old workdir tree entry"))?;
// workdir tree
let wd_tree = repo.find_tree(wd_tree_entry.id())?;
let old_wd_tree = repo.find_tree(old_wd_tree_entry.id())?;
// Exclude files that are larger than the limit (eg. database.sql which may never be intended to be committed)
let files_to_exclude = get_exclude_list(&repo)?;
// In-memory, libgit2 internal ignore rule
repo.add_ignore_rule(&files_to_exclude)?;
let mut diff_opts = git2::DiffOptions::new();
diff_opts
.recurse_untracked_dirs(true)
.include_untracked(true)
.show_binary(true)
.ignore_submodules(true)
.show_untracked_content(true);
let diff =
repo.diff_tree_to_tree(Some(&old_wd_tree), Some(&wd_tree), Some(&mut diff_opts))?;
let hunks = hunks_by_filepath(None, &diff)?;
Ok(hunks)
}
}
fn restore_conflicts_tree(
@ -417,7 +470,7 @@ fn get_exclude_list(repo: &git2::Repository) -> Result<String> {
#[cfg(test)]
mod tests {
use std::{io::Write, path::PathBuf};
use std::io::Write;
use crate::virtual_branches::Branch;

View File

@ -0,0 +1,90 @@
use std::vec;
use crate::{
ops::entry::{OperationType, SnapshotDetails},
virtual_branches::{branch::BranchUpdateRequest, Branch},
};
use super::{entry::Trailer, oplog::Oplog};
pub trait Snapshoter {
fn snapshot_deletion(&self, oplog: &dyn Oplog) -> anyhow::Result<()>;
fn snapshot_update(&self, oplog: &dyn Oplog, update: BranchUpdateRequest)
-> anyhow::Result<()>;
}
impl Snapshoter for Branch {
fn snapshot_deletion(&self, oplog: &dyn Oplog) -> anyhow::Result<()> {
let details =
SnapshotDetails::new(OperationType::DeleteBranch).with_trailers(vec![Trailer {
key: "name".to_string(),
value: self.name.to_string(),
}]);
oplog.create_snapshot(details)?;
Ok(())
}
fn snapshot_update(
&self,
oplog: &dyn Oplog,
update: BranchUpdateRequest,
) -> anyhow::Result<()> {
let details = if update.ownership.is_some() {
SnapshotDetails::new(OperationType::MoveHunk)
} else if let Some(name) = update.name {
SnapshotDetails::new(OperationType::UpdateBranchName).with_trailers(vec![
Trailer {
key: "before".to_string(),
value: self.name.to_string(),
},
Trailer {
key: "after".to_string(),
value: name,
},
])
} else if update.notes.is_some() {
SnapshotDetails::new(OperationType::UpdateBranchNotes)
} else if let Some(order) = update.order {
SnapshotDetails::new(OperationType::ReorderBranches).with_trailers(vec![
Trailer {
key: "before".to_string(),
value: self.order.to_string(),
},
Trailer {
key: "after".to_string(),
value: order.to_string(),
},
])
} else if let Some(selected_for_changes) = update.selected_for_changes {
SnapshotDetails::new(OperationType::SelectDefaultVirtualBranch).with_trailers(vec![
Trailer {
key: "before".to_string(),
value: self.selected_for_changes.unwrap_or_default().to_string(),
},
Trailer {
key: "after".to_string(),
value: selected_for_changes.to_string(),
},
])
} else if let Some(upstream) = update.upstream {
SnapshotDetails::new(OperationType::UpdateBranchRemoteName).with_trailers(vec![
Trailer {
key: "before".to_string(),
value: self
.upstream
.clone()
.map(|r| r.to_string())
.unwrap_or("".to_string()),
},
Trailer {
key: "after".to_string(),
value: upstream,
},
])
} else {
SnapshotDetails::new(OperationType::GenericBranchUpdate)
};
oplog.create_snapshot(details)?;
Ok(())
}
}

View File

@ -9,9 +9,7 @@ use anyhow::{Context, Result};
use super::conflicts;
use crate::error::{AnyhowContextExt, Code, ErrorWithContext};
use crate::{
askpass,
askpass::AskpassBroker,
error,
askpass, error,
git::{self, credentials::HelpError, Url},
projects::{self, AuthKey},
ssh, users,
@ -170,7 +168,7 @@ impl Repository {
credentials: &git::credentials::Helper,
remote_name: &str,
branch_name: &str,
askpass: Option<(AskpassBroker, Option<BranchId>)>,
askpass: Option<Option<BranchId>>,
) -> Result<()> {
let target_branch_refname =
git::Refname::from_str(&format!("refs/remotes/{}/{}", remote_name, branch_name))?;
@ -183,14 +181,7 @@ impl Repository {
let refname =
git::RemoteRefname::from_str(&format!("refs/remotes/{remote_name}/{branch_name}",))?;
match self.push(
&commit_id,
&refname,
false,
credentials,
None,
askpass.clone(),
) {
match self.push(&commit_id, &refname, false, credentials, None, askpass) {
Ok(()) => Ok(()),
Err(e) => Err(anyhow::anyhow!(e.to_string())),
}?;
@ -439,7 +430,7 @@ impl Repository {
with_force: bool,
credentials: &git::credentials::Helper,
refspec: Option<String>,
askpass_broker: Option<(AskpassBroker, Option<BranchId>)>,
askpass_broker: Option<Option<BranchId>>,
) -> Result<(), RemoteError> {
let refspec = refspec.unwrap_or_else(|| {
if with_force {
@ -538,7 +529,7 @@ impl Repository {
&self,
remote_name: &str,
credentials: &git::credentials::Helper,
askpass: Option<(AskpassBroker, String)>,
askpass: Option<String>,
) -> Result<(), RemoteError> {
let refspec = format!("+refs/heads/*:refs/remotes/{}/*", remote_name);
@ -651,11 +642,11 @@ pub enum LogUntil {
async fn handle_git_prompt_push(
prompt: String,
askpass: Option<(AskpassBroker, Option<BranchId>)>,
askpass: Option<Option<BranchId>>,
) -> Option<String> {
if let Some((askpass_broker, branch_id)) = askpass {
if let Some(branch_id) = askpass {
tracing::info!("received prompt for branch push {branch_id:?}: {prompt:?}");
askpass_broker
askpass::get_broker()
.submit_prompt(prompt, askpass::Context::Push { branch_id })
.await
} else {
@ -664,13 +655,10 @@ async fn handle_git_prompt_push(
}
}
async fn handle_git_prompt_fetch(
prompt: String,
askpass: Option<(AskpassBroker, String)>,
) -> Option<String> {
if let Some((askpass_broker, action)) = askpass {
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_broker
askpass::get_broker()
.submit_prompt(prompt, askpass::Context::Fetch { action })
.await
} else {

View File

@ -87,12 +87,7 @@ pub struct Project {
#[serde(default)]
pub enable_snapshots: Option<bool>,
// The number of changed lines that will trigger a snapshot
#[serde(default = "default_snapshot_lines_threshold")]
pub snapshot_lines_threshold: usize,
}
fn default_snapshot_lines_threshold() -> usize {
20
pub snapshot_lines_threshold: Option<usize>,
}
impl Project {
@ -118,4 +113,8 @@ impl Project {
pub fn virtual_branches(&self) -> VirtualBranchesHandle {
VirtualBranchesHandle::new(self.gb_dir())
}
pub fn snapshot_lines_threshold(&self) -> usize {
self.snapshot_lines_threshold.unwrap_or(20)
}
}

View File

@ -606,8 +606,8 @@ pub fn update_base_branch(
..target
})?;
// Rewriting the integration commit is necessary after changing target sha.
super::integration::update_gitbutler_integration(&vb_state, project_repository)?;
Ok(())
}

View File

@ -69,7 +69,7 @@ impl Branch {
}
}
#[derive(Debug, Serialize, Deserialize, Default)]
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct BranchUpdateRequest {
pub id: BranchId,
pub name: Option<String>,

View File

@ -1,8 +1,8 @@
use crate::{
error::Error,
snapshots::{
ops::{
entry::{OperationType, SnapshotDetails},
snapshot::Oplog,
oplog::Oplog,
},
};
use std::{collections::HashMap, path::Path, sync::Arc};
@ -16,7 +16,6 @@ use super::{
target, target_to_base_branch, BaseBranch, RemoteBranchFile, VirtualBranchesHandle,
};
use crate::{
askpass::AskpassBroker,
git, project_repository,
projects::{self, ProjectId},
users,
@ -331,7 +330,7 @@ impl Controller {
project_id: &ProjectId,
branch_id: &BranchId,
with_force: bool,
askpass: Option<(AskpassBroker, Option<BranchId>)>,
askpass: Option<Option<BranchId>>,
) -> Result<(), Error> {
self.inner(project_id)
.await
@ -398,7 +397,7 @@ impl Controller {
pub async fn fetch_from_target(
&self,
project_id: &ProjectId,
askpass: Option<(AskpassBroker, String)>,
askpass: Option<String>,
) -> Result<BaseBranch, Error> {
self.inner(project_id)
.await
@ -619,23 +618,7 @@ impl ControllerInner {
let _permit = self.semaphore.acquire().await;
self.with_verify_branch(project_id, |project_repository, _| {
let details = if branch_update.ownership.is_some() {
SnapshotDetails::new(OperationType::MoveHunk)
} else if branch_update.name.is_some() {
SnapshotDetails::new(OperationType::UpdateBranchName)
} else if branch_update.notes.is_some() {
SnapshotDetails::new(OperationType::UpdateBranchNotes)
} else if branch_update.order.is_some() {
SnapshotDetails::new(OperationType::ReorderBranches)
} else if branch_update.selected_for_changes.is_some() {
SnapshotDetails::new(OperationType::SelectDefaultVirtualBranch)
} else if branch_update.upstream.is_some() {
SnapshotDetails::new(OperationType::UpdateBranchRemoteName)
} else {
SnapshotDetails::new(OperationType::GenericBranchUpdate)
};
super::update_branch(project_repository, branch_update)?;
let _ = project_repository.project().create_snapshot(details);
Ok(())
})
}
@ -648,11 +631,7 @@ impl ControllerInner {
let _permit = self.semaphore.acquire().await;
self.with_verify_branch(project_id, |project_repository, _| {
super::delete_branch(project_repository, branch_id)?;
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationType::DeleteBranch));
Ok(())
super::delete_branch(project_repository, branch_id)
})
}
@ -849,7 +828,7 @@ impl ControllerInner {
project_id: &ProjectId,
branch_id: &BranchId,
with_force: bool,
askpass: Option<(AskpassBroker, Option<BranchId>)>,
askpass: Option<Option<BranchId>>,
) -> Result<(), Error> {
let _permit = self.semaphore.acquire().await;
let helper = self.helper.clone();
@ -945,7 +924,7 @@ impl ControllerInner {
pub async fn fetch_from_target(
&self,
project_id: &ProjectId,
askpass: Option<(AskpassBroker, String)>,
askpass: Option<String>,
) -> Result<BaseBranch, Error> {
let project = self.projects.get(project_id)?;
let mut project_repository = project_repository::Repository::open(&project)?;

View File

@ -65,9 +65,13 @@ pub enum VerifyError {
DetachedHead,
#[error("head is {0}")]
InvalidHead(String),
#[error("head not found")]
HeadNotFound,
#[error("integration commit not found")]
NoIntegrationCommit,
#[error(transparent)]
GitError(#[from] git::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
@ -93,6 +97,12 @@ impl ErrorWithContext for VerifyError {
Code::ProjectHead,
"GibButler's integration commit not found on head.",
),
VerifyError::HeadNotFound => {
error::Context::new_static(Code::Validation, "Repo HEAD is unavailable")
}
VerifyError::GitError(error) => {
error::Context::new(Code::Validation, error.to_string())
}
VerifyError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}

View File

@ -4,7 +4,7 @@ use anyhow::{anyhow, Context, Result};
use bstr::ByteSlice;
use lazy_static::lazy_static;
use super::{errors, VirtualBranchesHandle};
use super::{errors::VerifyError, VirtualBranchesHandle};
use crate::{
git::{self},
project_repository::{self, LogUntil},
@ -125,14 +125,6 @@ fn write_integration_file(head: &git::Reference, path: PathBuf) -> Result<()> {
pub fn update_gitbutler_integration(
vb_state: &VirtualBranchesHandle,
project_repository: &project_repository::Repository,
) -> Result<git::Oid> {
update_gitbutler_integration_with_commit(vb_state, project_repository, None)
}
pub fn update_gitbutler_integration_with_commit(
vb_state: &VirtualBranchesHandle,
project_repository: &project_repository::Repository,
integration_commit_id: Option<git::Oid>,
) -> Result<git::Oid> {
let target = vb_state
.get_default_target()
@ -140,14 +132,6 @@ pub fn update_gitbutler_integration_with_commit(
let repo = &project_repository.git_repository;
// write the currrent target sha to a temp branch as a parent
repo.reference(
&GITBUTLER_INTEGRATION_REFERENCE.clone().into(),
target.sha,
true,
"update target",
)?;
// get commit object from target.sha
let target_commit = repo.find_commit(target.sha)?;
@ -167,10 +151,6 @@ pub fn update_gitbutler_integration_with_commit(
}
}
// commit index to temp head for the merge
repo.set_head(&GITBUTLER_INTEGRATION_REFERENCE.clone().into())
.context("failed to set head")?;
let vb_state = project_repository.project().virtual_branches();
// get all virtual branches, we need to try to update them all
@ -183,11 +163,8 @@ pub fn update_gitbutler_integration_with_commit(
.filter(|branch| branch.applied)
.collect::<Vec<_>>();
let integration_commit_id = match integration_commit_id {
Some(commit_id) => commit_id,
_ => get_workspace_head(&vb_state, project_repository)?,
};
let integration_commit = repo.find_commit(integration_commit_id).unwrap();
let integration_commit =
repo.find_commit(get_workspace_head(&vb_state, project_repository)?)?;
let integration_tree = integration_commit.tree()?;
// message that says how to get back to where they were
@ -234,8 +211,10 @@ pub fn update_gitbutler_integration_with_commit(
let committer = get_committer()?;
// It would be nice if we could pass an `update_ref` parameter to this function, but that
// requires committing to the tip of the branch, and we're mostly replacing the tip.
let final_commit = repo.commit(
Some(&"refs/heads/gitbutler/integration".parse().unwrap()),
None,
&committer,
&committer,
&message,
@ -244,7 +223,15 @@ pub fn update_gitbutler_integration_with_commit(
None,
)?;
// write final_tree as the current index
// Create or replace the integration branch reference, then set as HEAD.
repo.reference(
&GITBUTLER_INTEGRATION_REFERENCE.clone().into(),
final_commit,
true,
"updated integration commit",
)?;
repo.set_head(&GITBUTLER_INTEGRATION_REFERENCE.clone().into())?;
let mut index = repo.index()?;
index.read_tree(&integration_tree)?;
index.write()?;
@ -290,7 +277,8 @@ pub fn update_gitbutler_integration_with_commit(
pub fn verify_branch(
project_repository: &project_repository::Repository,
) -> Result<(), errors::VerifyError> {
) -> Result<(), VerifyError> {
verify_current_branch_name(project_repository)?;
verify_head_is_set(project_repository)?;
verify_head_is_clean(project_repository)?;
Ok(())
@ -298,7 +286,7 @@ pub fn verify_branch(
fn verify_head_is_clean(
project_repository: &project_repository::Repository,
) -> Result<(), errors::VerifyError> {
) -> Result<(), VerifyError> {
let head_commit = project_repository
.git_repository
.head()
@ -319,7 +307,7 @@ fn verify_head_is_clean(
if integration_commit.is_none() {
// no integration commit found
return Err(errors::VerifyError::NoIntegrationCommit);
return Err(VerifyError::NoIntegrationCommit);
}
if extra_commits.is_empty() {
@ -395,17 +383,32 @@ fn verify_head_is_clean(
fn verify_head_is_set(
project_repository: &project_repository::Repository,
) -> Result<(), errors::VerifyError> {
) -> Result<(), VerifyError> {
match project_repository
.get_head()
.context("failed to get head")
.map_err(errors::VerifyError::Other)?
.map_err(VerifyError::Other)?
.name()
{
Some(refname) if refname.to_string() == GITBUTLER_INTEGRATION_REFERENCE.to_string() => {
Ok(())
}
None => Err(errors::VerifyError::DetachedHead),
Some(head_name) => Err(errors::VerifyError::InvalidHead(head_name.to_string())),
None => Err(VerifyError::DetachedHead),
Some(head_name) => Err(VerifyError::InvalidHead(head_name.to_string())),
}
}
// Returns an error if repo head is not pointing to the integration branch.
pub fn verify_current_branch_name(
project_repository: &project_repository::Repository,
) -> Result<bool, VerifyError> {
match project_repository.get_head()?.name() {
Some(head) => {
if head.to_string() != GITBUTLER_INTEGRATION_REFERENCE.to_string() {
return Err(VerifyError::InvalidHead(head.to_string()));
}
Ok(true)
}
None => Err(VerifyError::HeadNotFound),
}
}

View File

@ -25,9 +25,9 @@ use super::{
branch_to_remote_branch, errors, target, RemoteBranch, VirtualBranchesHandle,
};
use crate::git::diff::{diff_files_into_hunks, trees, FileDiff};
use crate::ops::snapshot::Snapshoter;
use crate::virtual_branches::branch::HunkHash;
use crate::{
askpass::AskpassBroker,
dedup::{dedup, dedup_fmt},
git::{
self,
@ -100,7 +100,6 @@ pub struct VirtualBranchCommit {
pub branch_id: BranchId,
pub change_id: Option<String>,
pub is_signed: bool,
pub stack_points: Option<Vec<HashMap<String, String>>>,
}
// this struct is a mapping to the view `File` type in Typescript
@ -795,12 +794,6 @@ pub fn list_virtual_branches(
.find_commit(integration_commit_id)
.unwrap();
super::integration::update_gitbutler_integration_with_commit(
&vb_state,
project_repository,
Some(integration_commit_id),
)?;
let (statuses, skipped_files) =
get_status_by_branch(project_repository, Some(&integration_commit.id()))?;
let max_selected_for_changes = statuses
@ -1050,7 +1043,6 @@ fn commit_to_vbranch_commit(
branch_id: branch.id,
change_id: commit.change_id(),
is_signed: commit.is_signed(),
stack_points: Some(stack_points),
};
Ok(commit)
@ -1398,11 +1390,11 @@ pub fn update_branch(
_ => errors::UpdateBranchError::Other(error.into()),
})?;
if let Some(ownership) = branch_update.ownership {
set_ownership(&vb_state, &mut branch, &ownership).context("failed to set ownership")?;
if let Some(ownership) = &branch_update.ownership {
set_ownership(&vb_state, &mut branch, ownership).context("failed to set ownership")?;
}
if let Some(name) = branch_update.name {
if let Some(name) = &branch_update.name {
let all_virtual_branches = vb_state
.list_branches()
.context("failed to read virtual branches")?;
@ -1414,13 +1406,13 @@ pub fn update_branch(
.iter()
.map(|b| b.name.as_str())
.collect::<Vec<_>>(),
&name,
name,
);
project_repository.add_branch_reference(&branch)?;
};
if let Some(updated_upstream) = branch_update.upstream {
if let Some(updated_upstream) = &branch_update.upstream {
let Some(default_target) = vb_state
.try_get_default_target()
.context("failed to get default target")?
@ -1440,14 +1432,14 @@ pub fn update_branch(
let remote_branch = format!(
"refs/remotes/{}/{}",
upstream_remote,
normalize_branch_name(&updated_upstream)
normalize_branch_name(updated_upstream)
)
.parse::<git::RemoteRefname>()
.unwrap();
branch.upstream = Some(remote_branch);
};
if let Some(notes) = branch_update.notes {
if let Some(notes) = branch_update.notes.clone() {
branch.notes = notes;
};
@ -1476,6 +1468,7 @@ pub fn update_branch(
.set_branch(branch.clone())
.context("failed to write target branch")?;
_ = branch.snapshot_update(project_repository.project(), branch_update);
Ok(branch)
}
@ -1492,6 +1485,7 @@ pub fn delete_branch(
.context("failed to read branch")?;
if branch.applied && unapply_branch(project_repository, branch_id)?.is_none() {
_ = branch.snapshot_deletion(project_repository.project());
return Ok(());
}
@ -1503,6 +1497,7 @@ pub fn delete_branch(
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
_ = branch.snapshot_deletion(project_repository.project());
Ok(())
}
@ -2443,7 +2438,7 @@ pub fn push(
branch_id: &BranchId,
with_force: bool,
credentials: &git::credentials::Helper,
askpass: Option<(AskpassBroker, Option<BranchId>)>,
askpass: Option<Option<BranchId>>,
) -> Result<(), errors::PushError> {
let vb_state = project_repository.project().virtual_branches();
@ -2507,7 +2502,7 @@ pub fn push(
with_force,
credentials,
None,
askpass.clone(),
askpass,
)?;
vbranch.upstream = Some(remote_branch.clone());
@ -2518,7 +2513,7 @@ pub fn push(
project_repository.fetch(
remote_branch.remote(),
credentials,
askpass.map(|(broker, _)| (broker, "modal".to_string())),
askpass.map(|_| "modal".to_string()),
)?;
Ok(())

View File

@ -71,6 +71,7 @@ mod undo_commit;
mod update_base_branch;
mod update_commit_message;
mod upstream;
mod verify_branch;
#[tokio::test]
async fn resolve_conflict_flow() {

View File

@ -0,0 +1,25 @@
use gitbutler_core::virtual_branches::errors::VerifyError;
use super::*;
// Ensures that `verify_branch` returns an error when not on the integration branch.
#[tokio::test]
async fn should_fail_on_incorrect_branch() {
let Test {
repository,
project_id,
controller,
..
} = &Test::default();
let branch_name: git::LocalRefname = "refs/heads/somebranch".parse().unwrap();
repository.checkout(&branch_name);
let result = controller.list_virtual_branches(project_id).await;
let error = result.err();
assert!(&error.is_some());
let error = error.unwrap();
let error = error.downcast_ref::<VerifyError>();
assert!(matches!(error, Some(VerifyError::InvalidHead(_))));
}

View File

@ -28,8 +28,14 @@ test-askpass-path = []
[dependencies]
thiserror.workspace = true
serde = { workspace = true, optional = true }
tokio = { workspace = true, optional = true, features = ["process", "time", "io-util", "net", "fs"] }
uuid.workspace = true
tokio = { workspace = true, optional = true, features = [
"process",
"time",
"io-util",
"net",
"fs",
] }
uuid = { workspace = true, features = ["v4", "fast-rng"] }
rand = "0.8.5"
futures = "0.3.30"
sysinfo = "0.30.11"

View File

@ -3,7 +3,6 @@ use std::{collections::HashMap, path};
use anyhow::{Context, Result};
use gitbutler_core::error::Error as CoreError;
use gitbutler_core::{
askpass::AskpassBroker,
gb_repository, git,
project_repository::{self, conflicts},
projects::{self, ProjectId},
@ -92,7 +91,7 @@ impl App {
remote_name: &str,
branch_name: &str,
credentials: &git::credentials::Helper,
askpass: Option<(AskpassBroker, Option<BranchId>)>,
askpass: Option<Option<BranchId>>,
) -> Result<(), CoreError> {
let project = self.projects.get(project_id)?;
let project_repository = project_repository::Repository::open(&project)?;
@ -104,7 +103,7 @@ impl App {
project_id: &ProjectId,
remote_name: &str,
credentials: &git::credentials::Helper,
askpass: Option<(AskpassBroker, String)>,
askpass: Option<String>,
) -> Result<(), CoreError> {
let project = self.projects.get(project_id)?;
let project_repository = project_repository::Repository::open(&project)?;

View File

@ -1,21 +1,16 @@
pub mod commands {
use gitbutler_core::{
askpass::{AskpassBroker, AskpassRequest},
askpass::{self, AskpassRequest},
id::Id,
};
use tauri::{AppHandle, Manager};
#[tauri::command(async)]
#[tracing::instrument(skip(handle, response))]
#[tracing::instrument(skip(response))]
pub async fn submit_prompt_response(
handle: AppHandle,
id: Id<AskpassRequest>,
response: Option<String>,
) -> Result<(), ()> {
handle
.state::<AskpassBroker>()
.handle_response(id, response)
.await;
askpass::get_broker().handle_response(id, response).await;
Ok(())
}
}

View File

@ -48,16 +48,13 @@ pub async fn git_test_push(
) -> Result<(), Error> {
let app = handle.state::<app::App>();
let helper = handle.state::<gitbutler_core::git::credentials::Helper>();
let askpass_broker = handle
.state::<gitbutler_core::askpass::AskpassBroker>()
.inner()
.clone();
Ok(app.git_test_push(
&project_id,
remote_name,
branch_name,
&helper,
Some((askpass_broker, None)),
// Run askpass, but don't pass any action
Some(None),
)?)
}
@ -71,15 +68,11 @@ pub async fn git_test_fetch(
) -> Result<(), Error> {
let app = handle.state::<app::App>();
let helper = handle.state::<gitbutler_core::git::credentials::Helper>();
let askpass_broker = handle
.state::<gitbutler_core::askpass::AskpassBroker>()
.inner()
.clone();
Ok(app.git_test_fetch(
&project_id,
remote_name,
&helper,
Some((askpass_broker, action.unwrap_or_else(|| "test".to_string()))),
Some(action.unwrap_or_else(|| "test".to_string())),
)?)
}

View File

@ -27,7 +27,7 @@ pub mod github;
pub mod keys;
pub mod projects;
pub mod sessions;
pub mod snapshots;
pub mod undo;
pub mod users;
pub mod virtual_branches;
pub mod zip;

View File

@ -17,7 +17,7 @@ use std::path::PathBuf;
use gitbutler_core::{assets, database, git, storage};
use gitbutler_tauri::{
app, askpass, commands, deltas, github, keys, logs, menu, projects, sessions, snapshots, users,
app, askpass, commands, deltas, github, keys, logs, menu, projects, sessions, undo, users,
virtual_branches, watcher, zip,
};
use tauri::{generate_context, Manager};
@ -68,6 +68,17 @@ fn main() {
logs::init(&app_handle);
// SAFETY(qix-): This is safe because we're initializing the askpass broker here,
// SAFETY(qix-): before any other threads would ever access it.
unsafe {
gitbutler_core::askpass::init({
let handle = app_handle.clone();
move |event| {
handle.emit_all("git_prompt", event).expect("tauri event emission doesn't fail in practice")
}
});
}
let app_data_dir = app_handle.path_resolver().app_data_dir().expect("missing app data dir");
let app_cache_dir = app_handle.path_resolver().app_cache_dir().expect("missing app cache dir");
let app_log_dir = app_handle.path_resolver().app_log_dir().expect("missing app log dir");
@ -77,14 +88,6 @@ fn main() {
tracing::info!(version = %app_handle.package_info().version, name = %app_handle.package_info().name, "starting app");
let askpass_broker = gitbutler_core::askpass::AskpassBroker::init({
let handle = app_handle.clone();
move |event| {
handle.emit_all("git_prompt", event).expect("tauri event emission doesn't fail in practice")
}
});
app_handle.manage(askpass_broker);
let storage_controller = storage::Storage::new(&app_data_dir);
app_handle.manage(storage_controller.clone());
@ -228,8 +231,9 @@ fn main() {
virtual_branches::commands::squash_branch_commit,
virtual_branches::commands::fetch_from_target,
virtual_branches::commands::move_commit,
snapshots::list_snapshots,
snapshots::restore_snapshot,
undo::list_snapshots,
undo::restore_snapshot,
undo::snapshot_diff,
menu::menu_item_set_enabled,
keys::commands::get_public_key,
github::commands::init_device_oauth,

View File

@ -1,9 +1,12 @@
use crate::error::Error;
use anyhow::Context;
use gitbutler_core::git::diff::FileDiff;
use gitbutler_core::{
ops::{entry::Snapshot, oplog::Oplog},
projects::{self, ProjectId},
snapshots::{entry::Snapshot, snapshot::Oplog},
};
use std::collections::HashMap;
use std::path::PathBuf;
use tauri::Manager;
use tracing::instrument;
@ -37,3 +40,18 @@ pub async fn restore_snapshot(
project.restore_snapshot(sha)?;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn snapshot_diff(
handle: tauri::AppHandle,
project_id: ProjectId,
sha: String,
) -> Result<HashMap<PathBuf, FileDiff>, Error> {
let project = handle
.state::<projects::Controller>()
.get(&project_id)
.context("failed to get project")?;
let diff = project.snapshot_diff(sha)?;
Ok(diff)
}

View File

@ -2,7 +2,6 @@ pub mod commands {
use crate::error::Error;
use anyhow::Context;
use gitbutler_core::{
askpass::AskpassBroker,
assets,
error::Code,
git, projects,
@ -266,15 +265,9 @@ pub mod commands {
branch_id: BranchId,
with_force: bool,
) -> Result<(), Error> {
let askpass_broker = handle.state::<AskpassBroker>();
handle
.state::<Controller>()
.push_virtual_branch(
&project_id,
&branch_id,
with_force,
Some((askpass_broker.inner().clone(), Some(branch_id))),
)
.push_virtual_branch(&project_id, &branch_id, with_force, Some(Some(branch_id)))
.await
.map_err(|err| err.context(Code::Unknown))?;
emit_vbranches(&handle, &project_id).await;
@ -499,15 +492,11 @@ pub mod commands {
project_id: ProjectId,
action: Option<String>,
) -> Result<BaseBranch, Error> {
let askpass_broker = handle.state::<AskpassBroker>().inner().clone();
let base_branch = handle
.state::<Controller>()
.fetch_from_target(
&project_id,
Some((
askpass_broker,
action.unwrap_or_else(|| "unknown".to_string()),
)),
Some(action.unwrap_or_else(|| "unknown".to_string())),
)
.await?;
emit_vbranches(&handle, &project_id).await;

View File

@ -7,10 +7,10 @@ use std::sync::Arc;
use std::{path, time};
use anyhow::{bail, Context, Result};
use gitbutler_core::ops::entry::{OperationType, SnapshotDetails};
use gitbutler_core::ops::oplog::Oplog;
use gitbutler_core::projects::ProjectId;
use gitbutler_core::sessions::SessionId;
use gitbutler_core::snapshots::entry::{OperationType, SnapshotDetails};
use gitbutler_core::snapshots::snapshot::Oplog;
use gitbutler_core::virtual_branches::VirtualBranches;
use gitbutler_core::{
assets, deltas, gb_repository, git, project_repository, projects, reader, sessions, users,
@ -301,7 +301,7 @@ impl Handler {
.get(&project_id)
.context("failed to get project")?;
let changed_lines = project.lines_since_snapshot()?;
if changed_lines > project.snapshot_lines_threshold {
if changed_lines > project.snapshot_lines_threshold() {
project.create_snapshot(SnapshotDetails::new(OperationType::FileChanges))?;
}
Ok(())