mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-28 22:03:30 +03:00
merge stuff
This commit is contained in:
commit
4d542330c5
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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))]
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
@ -1,4 +1,5 @@
|
||||
pub mod entry;
|
||||
pub mod oplog;
|
||||
mod reflog;
|
||||
pub mod snapshot;
|
||||
mod state;
|
@ -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;
|
||||
|
90
crates/gitbutler-core/src/ops/snapshot.rs
Normal file
90
crates/gitbutler-core/src/ops/snapshot.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
||||
|
@ -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>,
|
||||
|
@ -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)?;
|
||||
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
|
@ -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() {
|
||||
|
@ -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(_))));
|
||||
}
|
@ -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"
|
||||
|
@ -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)?;
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
@ -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())),
|
||||
)?)
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
@ -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;
|
||||
|
@ -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(())
|
||||
|
Loading…
Reference in New Issue
Block a user