mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-22 19:14:31 +03:00
Split branch creation and deletion into their own home
This commit is contained in:
parent
f9043920c0
commit
9b59764db3
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -1970,6 +1970,7 @@ dependencies = [
|
||||
"gitbutler-id",
|
||||
"gitbutler-reference",
|
||||
"gitbutler-serde",
|
||||
"gitbutler-tagged-string",
|
||||
"hex",
|
||||
"itertools 0.13.0",
|
||||
"lazy_static",
|
||||
@ -2120,6 +2121,7 @@ dependencies = [
|
||||
"gitbutler-project",
|
||||
"gitbutler-repo",
|
||||
"gitbutler-serde",
|
||||
"gitbutler-tagged-string",
|
||||
"gix",
|
||||
"itertools 0.13.0",
|
||||
"pretty_assertions",
|
||||
@ -2271,6 +2273,7 @@ dependencies = [
|
||||
"gitbutler-repo",
|
||||
"gitbutler-secret",
|
||||
"gitbutler-storage",
|
||||
"gitbutler-tagged-string",
|
||||
"gitbutler-testsupport",
|
||||
"gitbutler-user",
|
||||
"gitbutler-virtual",
|
||||
@ -2368,6 +2371,7 @@ dependencies = [
|
||||
"gitbutler-reference",
|
||||
"gitbutler-repo",
|
||||
"gitbutler-serde",
|
||||
"gitbutler-tagged-string",
|
||||
"gitbutler-testsupport",
|
||||
"gitbutler-time",
|
||||
"gitbutler-url",
|
||||
|
41
Cargo.toml
41
Cargo.toml
@ -5,46 +5,52 @@ members = [
|
||||
"crates/gitbutler-watcher",
|
||||
"crates/gitbutler-watcher/vendor/debouncer",
|
||||
"crates/gitbutler-testsupport",
|
||||
"crates/gitbutler-cli",
|
||||
"crates/gitbutler-virtual",
|
||||
"crates/gitbutler-cli",
|
||||
"crates/gitbutler-virtual",
|
||||
"crates/gitbutler-sync",
|
||||
"crates/gitbutler-oplog",
|
||||
"crates/gitbutler-branchstate",
|
||||
"crates/gitbutler-oplog",
|
||||
"crates/gitbutler-branchstate",
|
||||
"crates/gitbutler-repo",
|
||||
"crates/gitbutler-command-context",
|
||||
"crates/gitbutler-feedback",
|
||||
"crates/gitbutler-command-context",
|
||||
"crates/gitbutler-feedback",
|
||||
"crates/gitbutler-config",
|
||||
"crates/gitbutler-project",
|
||||
"crates/gitbutler-user",
|
||||
"crates/gitbutler-user",
|
||||
"crates/gitbutler-branch",
|
||||
"crates/gitbutler-reference",
|
||||
"crates/gitbutler-error",
|
||||
"crates/gitbutler-serde",
|
||||
"crates/gitbutler-secret",
|
||||
"crates/gitbutler-id",
|
||||
"crates/gitbutler-storage",
|
||||
"crates/gitbutler-serde",
|
||||
"crates/gitbutler-secret",
|
||||
"crates/gitbutler-id",
|
||||
"crates/gitbutler-storage",
|
||||
"crates/gitbutler-fs",
|
||||
"crates/gitbutler-time",
|
||||
"crates/gitbutler-commit",
|
||||
"crates/gitbutler-tagged-string",
|
||||
"crates/gitbutler-time",
|
||||
"crates/gitbutler-commit",
|
||||
"crates/gitbutler-tagged-string",
|
||||
"crates/gitbutler-url",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
# Add the `tracing` or `tracing-detail` features to see more of gitoxide in the logs. Useful to see which programs it invokes.
|
||||
gix = { git = "https://github.com/Byron/gitoxide", rev = "55cbc1b9d6f210298a86502a7f20f9736c7e963e", default-features = false, features = [] }
|
||||
git2 = { version = "0.18.3", features = ["vendored-openssl", "vendored-libgit2"] }
|
||||
gix = { git = "https://github.com/Byron/gitoxide", rev = "55cbc1b9d6f210298a86502a7f20f9736c7e963e", default-features = false, features = [
|
||||
] }
|
||||
git2 = { version = "0.18.3", features = [
|
||||
"vendored-openssl",
|
||||
"vendored-libgit2",
|
||||
] }
|
||||
uuid = { version = "1.8.0", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0.61"
|
||||
tokio = { version = "1.38.0", default-features = false }
|
||||
keyring = "2.3.3"
|
||||
anyhow = "1.0.86"
|
||||
|
||||
gitbutler-id = { path = "crates/gitbutler-id" }
|
||||
gitbutler-git = { path = "crates/gitbutler-git" }
|
||||
gitbutler-watcher = { path = "crates/gitbutler-watcher" }
|
||||
gitbutler-testsupport = { path = "crates/gitbutler-testsupport" }
|
||||
gitbutler-cli ={ path = "crates/gitbutler-cli" }
|
||||
gitbutler-cli = { path = "crates/gitbutler-cli" }
|
||||
gitbutler-virtual = { path = "crates/gitbutler-virtual" }
|
||||
gitbutler-sync = { path = "crates/gitbutler-sync" }
|
||||
gitbutler-oplog = { path = "crates/gitbutler-oplog" }
|
||||
@ -60,7 +66,6 @@ gitbutler-reference = { path = "crates/gitbutler-reference" }
|
||||
gitbutler-error = { path = "crates/gitbutler-error" }
|
||||
gitbutler-serde = { path = "crates/gitbutler-serde" }
|
||||
gitbutler-secret = { path = "crates/gitbutler-secret" }
|
||||
gitbutler-id = { path = "crates/gitbutler-id" }
|
||||
gitbutler-storage = { path = "crates/gitbutler-storage" }
|
||||
gitbutler-fs = { path = "crates/gitbutler-fs" }
|
||||
gitbutler-time = { path = "crates/gitbutler-time" }
|
||||
|
@ -9,10 +9,11 @@ publish = false
|
||||
anyhow = "1.0.86"
|
||||
git2.workspace = true
|
||||
gitbutler-reference.workspace = true
|
||||
gitbutler-tagged-string.workspace = true
|
||||
gitbutler-serde.workspace = true
|
||||
gitbutler-id.workspace = true
|
||||
itertools = "0.13"
|
||||
serde = { workspace = true, features = ["std"]}
|
||||
serde = { workspace = true, features = ["std"] }
|
||||
bstr = "1.9.1"
|
||||
md5 = "0.7.0"
|
||||
hex = "0.4.3"
|
||||
@ -20,5 +21,5 @@ tracing = "0.1.40"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
[[test]]
|
||||
name="branch"
|
||||
name = "branch"
|
||||
path = "tests/mod.rs"
|
||||
|
@ -1,5 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
use gitbutler_reference::ReferenceName;
|
||||
use gitbutler_tagged_string::ReferenceName;
|
||||
|
||||
pub trait BranchExt {
|
||||
fn reference_name(&self) -> Result<ReferenceName>;
|
||||
|
@ -1,5 +1,6 @@
|
||||
pub mod branch;
|
||||
pub mod branch_ext;
|
||||
pub mod dedup;
|
||||
pub mod diff;
|
||||
pub mod file_ownership;
|
||||
pub mod hunk;
|
||||
|
@ -10,7 +10,7 @@ anyhow = "1.0.86"
|
||||
git2.workspace = true
|
||||
gitbutler-branchstate.workspace = true
|
||||
gitbutler-repo.workspace = true
|
||||
serde = { workspace = true, features = ["std"]}
|
||||
serde = { workspace = true, features = ["std"] }
|
||||
itertools = "0.13"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
@ -20,9 +20,10 @@ gitbutler-project.workspace = true
|
||||
gitbutler-branch.workspace = true
|
||||
gitbutler-serde.workspace = true
|
||||
gitbutler-fs.workspace = true
|
||||
gitbutler-tagged-string.workspace = true
|
||||
|
||||
[[test]]
|
||||
name="oplog"
|
||||
name = "oplog"
|
||||
path = "tests/mod.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use gitbutler_branch::branch::{Branch, BranchUpdateRequest};
|
||||
use gitbutler_project::Project;
|
||||
use gitbutler_tagged_string::ReferenceName;
|
||||
use std::vec;
|
||||
|
||||
use crate::{
|
||||
@ -14,7 +15,7 @@ pub trait Snapshot {
|
||||
fn snapshot_branch_unapplied(
|
||||
&self,
|
||||
snapshot_tree: git2::Oid,
|
||||
result: Result<&git2::Branch, &anyhow::Error>,
|
||||
result: Result<&ReferenceName, &anyhow::Error>,
|
||||
) -> anyhow::Result<()>;
|
||||
|
||||
fn snapshot_commit_undo(
|
||||
@ -48,9 +49,9 @@ impl Snapshot for Project {
|
||||
fn snapshot_branch_unapplied(
|
||||
&self,
|
||||
snapshot_tree: git2::Oid,
|
||||
result: Result<&git2::Branch, &anyhow::Error>,
|
||||
result: Result<&ReferenceName, &anyhow::Error>,
|
||||
) -> anyhow::Result<()> {
|
||||
let result = result.map(|o| o.name().ok().flatten().map(|s| s.to_string()));
|
||||
let result = result.map(|s| Some(s.to_string()));
|
||||
let details = SnapshotDetails::new(OperationKind::UnapplyBranch)
|
||||
.with_trailers(result_trailer(result, "name".to_string()));
|
||||
self.commit_snapshot(snapshot_tree, details)?;
|
||||
|
@ -1,5 +1,4 @@
|
||||
mod refname;
|
||||
use gitbutler_tagged_string::TaggedString;
|
||||
pub use refname::{LocalRefname, Refname, RemoteRefname, VirtualRefname};
|
||||
use regex::Regex;
|
||||
|
||||
@ -7,7 +6,3 @@ pub fn normalize_branch_name(name: &str) -> String {
|
||||
let pattern = Regex::new("[^A-Za-z0-9_/.#]+").unwrap();
|
||||
pattern.replace_all(name, "-").to_string()
|
||||
}
|
||||
|
||||
pub struct _ReferenceName;
|
||||
/// The name of a reference ie. `refs/heads/master`
|
||||
pub type ReferenceName = TaggedString<_ReferenceName>;
|
||||
|
@ -54,3 +54,7 @@ impl<T> fmt::Debug for TaggedString<T> {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct _ReferenceName;
|
||||
/// The name of a reference ie. `refs/heads/master`
|
||||
pub type ReferenceName = TaggedString<_ReferenceName>;
|
||||
|
@ -32,7 +32,7 @@ git2.workspace = true
|
||||
once_cell = "1.19"
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
serde.workspace = true
|
||||
serde_json = { version = "1.0", features = [ "std", "arbitrary_precision" ] }
|
||||
serde_json = { version = "1.0", features = ["std", "arbitrary_precision"] }
|
||||
tauri-plugin-context-menu = { git = "https://github.com/c2r0b/tauri-plugin-context-menu", branch = "main" }
|
||||
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
@ -41,7 +41,7 @@ tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", br
|
||||
log = "^0.4"
|
||||
thiserror.workspace = true
|
||||
# The features here optimize for performance.
|
||||
tokio = { workspace = true, features = [ "rt-multi-thread", "parking_lot" ] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "parking_lot"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = "0.3.17"
|
||||
@ -60,15 +60,23 @@ gitbutler-error.workspace = true
|
||||
gitbutler-secret.workspace = true
|
||||
gitbutler-id.workspace = true
|
||||
gitbutler-storage.workspace = true
|
||||
gitbutler-tagged-string.workspace = true
|
||||
open = "5"
|
||||
|
||||
[dependencies.tauri]
|
||||
version = "1.7.0"
|
||||
features = [
|
||||
"http-all", "os-all", "dialog-open", "fs-read-file",
|
||||
"path-all", "process-relaunch", "protocol-asset",
|
||||
"shell-open", "window-maximize", "window-start-dragging",
|
||||
"window-unmaximize"
|
||||
"http-all",
|
||||
"os-all",
|
||||
"dialog-open",
|
||||
"fs-read-file",
|
||||
"path-all",
|
||||
"process-relaunch",
|
||||
"protocol-asset",
|
||||
"shell-open",
|
||||
"window-maximize",
|
||||
"window-start-dragging",
|
||||
"window-unmaximize",
|
||||
]
|
||||
|
||||
[lints.clippy]
|
||||
@ -86,4 +94,4 @@ devtools = ["tauri/devtools"]
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
error-context = ["dep:backtrace" ]
|
||||
error-context = ["dep:backtrace"]
|
||||
|
@ -6,8 +6,8 @@ pub mod commands {
|
||||
use gitbutler_error::error::Code;
|
||||
use gitbutler_project as projects;
|
||||
use gitbutler_project::ProjectId;
|
||||
use gitbutler_reference::ReferenceName;
|
||||
use gitbutler_reference::{Refname, RemoteRefname};
|
||||
use gitbutler_tagged_string::ReferenceName;
|
||||
use gitbutler_virtual::assets;
|
||||
use gitbutler_virtual::base::BaseBranch;
|
||||
use gitbutler_virtual::files::RemoteBranchFile;
|
||||
|
@ -22,7 +22,8 @@ gitbutler-id.workspace = true
|
||||
gitbutler-time.workspace = true
|
||||
gitbutler-commit.workspace = true
|
||||
gitbutler-url.workspace = true
|
||||
serde = { workspace = true, features = ["std"]}
|
||||
gitbutler-tagged-string.workspace = true
|
||||
serde = { workspace = true, features = ["std"] }
|
||||
bstr = "1.9.1"
|
||||
diffy = "0.3.0"
|
||||
hex = "0.4.3"
|
||||
@ -38,7 +39,7 @@ urlencoding = "2.1.3"
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
|
||||
[[test]]
|
||||
name="virtual"
|
||||
name = "virtual"
|
||||
path = "tests/virtual_branches/mod.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -11,10 +11,11 @@ use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_project::FetchResult;
|
||||
use gitbutler_reference::{Refname, RemoteRefname};
|
||||
use gitbutler_repo::{LogUntil, RepoActions, RepositoryExt};
|
||||
use gitbutler_tagged_string::ReferenceName;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::r#virtual as vb;
|
||||
use super::r#virtual::convert_to_real_branch;
|
||||
use crate::branch_manager::BranchManagerAccess;
|
||||
use crate::conflicts::RepoConflicts;
|
||||
use crate::integration::{get_workspace_head, update_gitbutler_integration};
|
||||
use crate::remote::{commit_to_remote_commit, RemoteCommit};
|
||||
@ -328,7 +329,7 @@ fn _print_tree(repo: &git2::Repository, tree: &git2::Tree) -> Result<()> {
|
||||
// update the target sha
|
||||
pub fn update_base_branch(
|
||||
project_repository: &ProjectRepository,
|
||||
) -> anyhow::Result<Vec<git2::Branch<'_>>> {
|
||||
) -> anyhow::Result<Vec<ReferenceName>> {
|
||||
project_repository.assure_resolved()?;
|
||||
|
||||
// look up the target and see if there is a new oid
|
||||
@ -344,7 +345,7 @@ pub fn update_base_branch(
|
||||
.peel_to_commit()
|
||||
.context(format!("failed to peel branch {} to commit", target.branch))?;
|
||||
|
||||
let mut unapplied_branch_names: Vec<git2::Branch> = Vec::new();
|
||||
let mut unapplied_branch_names: Vec<ReferenceName> = Vec::new();
|
||||
|
||||
if new_target_commit.id() == target.sha {
|
||||
return Ok(unapplied_branch_names);
|
||||
@ -421,11 +422,9 @@ pub fn update_base_branch(
|
||||
|
||||
if branch_tree_merge_index.has_conflicts() {
|
||||
// branch tree conflicts with new target, unapply branch for now. we'll handle it later, when user applies it back.
|
||||
let unapplied_real_branch = convert_to_real_branch(
|
||||
project_repository,
|
||||
branch.id,
|
||||
Default::default(),
|
||||
)?;
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let unapplied_real_branch =
|
||||
branch_manager.convert_to_real_branch(branch.id, Default::default())?;
|
||||
|
||||
unapplied_branch_names.push(unapplied_real_branch);
|
||||
|
||||
@ -457,11 +456,9 @@ pub fn update_base_branch(
|
||||
if branch_head_merge_index.has_conflicts() {
|
||||
// branch commits conflict with new target, make sure the branch is
|
||||
// unapplied. conflicts witll be dealt with when applying it back.
|
||||
let unapplied_real_branch = convert_to_real_branch(
|
||||
project_repository,
|
||||
branch.id,
|
||||
Default::default(),
|
||||
)?;
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let unapplied_real_branch =
|
||||
branch_manager.convert_to_real_branch(branch.id, Default::default())?;
|
||||
unapplied_branch_names.push(unapplied_real_branch);
|
||||
|
||||
return Ok(None);
|
||||
|
724
crates/gitbutler-virtual/src/branch_manager.rs
Normal file
724
crates/gitbutler-virtual/src/branch_manager.rs
Normal file
@ -0,0 +1,724 @@
|
||||
use crate::{
|
||||
conflicts::{self, RepoConflicts},
|
||||
ensure_selected_for_changes, get_applied_status,
|
||||
integration::{get_integration_commiter, update_gitbutler_integration},
|
||||
set_ownership, undo_commit, write_tree, NameConflitResolution, VirtualBranchHunk,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use git2::build::TreeUpdateBuilder;
|
||||
use gitbutler_branch::{
|
||||
branch::{self, BranchCreateRequest, BranchId},
|
||||
branch_ext::BranchExt,
|
||||
dedup::dedup,
|
||||
diff,
|
||||
ownership::BranchOwnershipClaims,
|
||||
};
|
||||
use gitbutler_branchstate::VirtualBranchesAccess;
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_commit::commit_headers::{CommitHeadersV2, HasCommitHeaders};
|
||||
use gitbutler_error::error::Marker;
|
||||
use gitbutler_oplog::snapshot::Snapshot;
|
||||
use gitbutler_reference::{normalize_branch_name, Refname};
|
||||
use gitbutler_repo::{rebase::cherry_rebase, RepoActions, RepositoryExt};
|
||||
use gitbutler_tagged_string::ReferenceName;
|
||||
use gitbutler_time::time::now_since_unix_epoch_ms;
|
||||
|
||||
pub struct BranchManager<'l> {
|
||||
project_repository: &'l ProjectRepository,
|
||||
}
|
||||
|
||||
pub trait BranchManagerAccess {
|
||||
fn branch_manager(&self) -> BranchManager;
|
||||
}
|
||||
|
||||
impl BranchManagerAccess for ProjectRepository {
|
||||
fn branch_manager(&self) -> BranchManager {
|
||||
BranchManager {
|
||||
project_repository: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> BranchManager<'l> {
|
||||
pub fn create_virtual_branch(&self, create: &BranchCreateRequest) -> Result<branch::Branch> {
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
|
||||
let default_target = vb_state.get_default_target()?;
|
||||
|
||||
let commit = self
|
||||
.project_repository
|
||||
.repo()
|
||||
.find_commit(default_target.sha)
|
||||
.context("failed to find default target commit")?;
|
||||
|
||||
let tree = commit
|
||||
.tree()
|
||||
.context("failed to find defaut target commit tree")?;
|
||||
|
||||
let mut all_virtual_branches = vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to read virtual branches")?;
|
||||
|
||||
let name = dedup(
|
||||
&all_virtual_branches
|
||||
.iter()
|
||||
.map(|b| b.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
create
|
||||
.name
|
||||
.as_ref()
|
||||
.unwrap_or(&"Virtual branch".to_string()),
|
||||
);
|
||||
|
||||
_ = self
|
||||
.project_repository
|
||||
.project()
|
||||
.snapshot_branch_creation(name.clone());
|
||||
|
||||
all_virtual_branches.sort_by_key(|branch| branch.order);
|
||||
|
||||
let order = create.order.unwrap_or(vb_state.next_order_index()?);
|
||||
|
||||
let selected_for_changes = if let Some(selected_for_changes) = create.selected_for_changes {
|
||||
if selected_for_changes {
|
||||
for mut other_branch in vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to read virtual branches")?
|
||||
{
|
||||
other_branch.selected_for_changes = None;
|
||||
vb_state.set_branch(other_branch.clone())?;
|
||||
}
|
||||
Some(now_since_unix_epoch_ms())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
(!all_virtual_branches
|
||||
.iter()
|
||||
.any(|b| b.selected_for_changes.is_some()))
|
||||
.then_some(now_since_unix_epoch_ms())
|
||||
};
|
||||
|
||||
// make space for the new branch
|
||||
for (i, branch) in all_virtual_branches.iter().enumerate() {
|
||||
let mut branch = branch.clone();
|
||||
let new_order = if i < order { i } else { i + 1 };
|
||||
if branch.order != new_order {
|
||||
branch.order = new_order;
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
let now = gitbutler_time::time::now_ms();
|
||||
|
||||
let mut branch = branch::Branch {
|
||||
id: BranchId::generate(),
|
||||
name: name.clone(),
|
||||
notes: String::new(),
|
||||
upstream: None,
|
||||
upstream_head: None,
|
||||
tree: tree.id(),
|
||||
head: default_target.sha,
|
||||
created_timestamp_ms: now,
|
||||
updated_timestamp_ms: now,
|
||||
ownership: BranchOwnershipClaims::default(),
|
||||
order,
|
||||
selected_for_changes,
|
||||
allow_rebasing: self.project_repository.project().ok_with_force_push.into(),
|
||||
old_applied: true,
|
||||
in_workspace: true,
|
||||
not_in_workspace_wip_change_id: None,
|
||||
source_refname: None,
|
||||
};
|
||||
|
||||
if let Some(ownership) = &create.ownership {
|
||||
set_ownership(&vb_state, &mut branch, ownership).context("failed to set ownership")?;
|
||||
}
|
||||
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
self.project_repository.add_branch_reference(&branch)?;
|
||||
|
||||
Ok(branch)
|
||||
}
|
||||
|
||||
pub fn create_virtual_branch_from_branch(&self, upstream: &Refname) -> Result<BranchId> {
|
||||
// only set upstream if it's not the default target
|
||||
let upstream_branch = match upstream {
|
||||
Refname::Other(_) | Refname::Virtual(_) => {
|
||||
// we only support local or remote branches
|
||||
bail!("branch {upstream} must be a local or remote branch");
|
||||
}
|
||||
Refname::Remote(remote) => Some(remote.clone()),
|
||||
Refname::Local(local) => local.remote().cloned(),
|
||||
};
|
||||
|
||||
let branch_name = upstream
|
||||
.branch()
|
||||
.expect("always a branch reference")
|
||||
.to_string();
|
||||
|
||||
let _ = self
|
||||
.project_repository
|
||||
.project()
|
||||
.snapshot_branch_creation(branch_name.clone());
|
||||
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
|
||||
let default_target = vb_state.get_default_target()?;
|
||||
|
||||
if let Refname::Remote(remote_upstream) = upstream {
|
||||
if default_target.branch == *remote_upstream {
|
||||
bail!("cannot create a branch from default target")
|
||||
}
|
||||
}
|
||||
|
||||
let repo = self.project_repository.repo();
|
||||
let head_reference =
|
||||
repo.find_reference(&upstream.to_string())
|
||||
.map_err(|err| match err {
|
||||
err if err.code() == git2::ErrorCode::NotFound => {
|
||||
anyhow!("branch {upstream} was not found")
|
||||
}
|
||||
err => err.into(),
|
||||
})?;
|
||||
let head_commit = head_reference
|
||||
.peel_to_commit()
|
||||
.context("failed to peel to commit")?;
|
||||
let head_commit_tree = head_commit.tree().context("failed to find tree")?;
|
||||
|
||||
let virtual_branches = vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to read virtual branches")?
|
||||
.into_iter()
|
||||
.collect::<Vec<branch::Branch>>();
|
||||
|
||||
let order = vb_state.next_order_index()?;
|
||||
|
||||
let selected_for_changes = (!virtual_branches
|
||||
.iter()
|
||||
.any(|b| b.selected_for_changes.is_some()))
|
||||
.then_some(now_since_unix_epoch_ms());
|
||||
|
||||
let now = gitbutler_time::time::now_ms();
|
||||
|
||||
// add file ownership based off the diff
|
||||
let target_commit = repo.find_commit(default_target.sha)?;
|
||||
let merge_base_oid = repo.merge_base(target_commit.id(), head_commit.id())?;
|
||||
let merge_base_tree = repo.find_commit(merge_base_oid)?.tree()?;
|
||||
|
||||
// do a diff between the head of this branch and the target base
|
||||
let diff = diff::trees(
|
||||
self.project_repository.repo(),
|
||||
&merge_base_tree,
|
||||
&head_commit_tree,
|
||||
)?;
|
||||
|
||||
// assign ownership to the branch
|
||||
let ownership = diff.iter().fold(
|
||||
BranchOwnershipClaims::default(),
|
||||
|mut ownership, (file_path, file)| {
|
||||
for hunk in &file.hunks {
|
||||
ownership.put(
|
||||
format!(
|
||||
"{}:{}",
|
||||
file_path.display(),
|
||||
VirtualBranchHunk::gen_id(hunk.new_start, hunk.new_lines)
|
||||
)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
ownership
|
||||
},
|
||||
);
|
||||
|
||||
let branch = if let Ok(Some(mut branch)) =
|
||||
vb_state.find_by_source_refname_where_not_in_workspace(upstream)
|
||||
{
|
||||
branch.upstream_head = upstream_branch.is_some().then_some(head_commit.id());
|
||||
branch.upstream = upstream_branch;
|
||||
branch.tree = head_commit_tree.id();
|
||||
branch.head = head_commit.id();
|
||||
branch.ownership = ownership;
|
||||
branch.order = order;
|
||||
branch.selected_for_changes = selected_for_changes;
|
||||
branch.allow_rebasing = self.project_repository.project().ok_with_force_push.into();
|
||||
branch.old_applied = true;
|
||||
branch.in_workspace = true;
|
||||
|
||||
branch
|
||||
} else {
|
||||
branch::Branch {
|
||||
id: BranchId::generate(),
|
||||
name: branch_name.clone(),
|
||||
notes: String::new(),
|
||||
source_refname: Some(upstream.clone()),
|
||||
upstream_head: upstream_branch.is_some().then_some(head_commit.id()),
|
||||
upstream: upstream_branch,
|
||||
tree: head_commit_tree.id(),
|
||||
head: head_commit.id(),
|
||||
created_timestamp_ms: now,
|
||||
updated_timestamp_ms: now,
|
||||
ownership,
|
||||
order,
|
||||
selected_for_changes,
|
||||
allow_rebasing: self.project_repository.project().ok_with_force_push.into(),
|
||||
old_applied: true,
|
||||
in_workspace: true,
|
||||
not_in_workspace_wip_change_id: None,
|
||||
}
|
||||
};
|
||||
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
self.project_repository.add_branch_reference(&branch)?;
|
||||
|
||||
match self.apply_branch(branch.id) {
|
||||
Ok(_) => Ok(branch.id),
|
||||
Err(err)
|
||||
if err
|
||||
.downcast_ref()
|
||||
.map_or(false, |marker: &Marker| *marker == Marker::ProjectConflict) =>
|
||||
{
|
||||
// if branch conflicts with the workspace, it's ok. keep it unapplied
|
||||
Ok(branch.id)
|
||||
}
|
||||
Err(err) => Err(err).context("failed to apply"),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_branch(&self, branch_id: BranchId) -> Result<String> {
|
||||
self.project_repository.assure_resolved()?;
|
||||
self.project_repository.assure_unconflicted()?;
|
||||
let repo = self.project_repository.repo();
|
||||
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
let default_target = vb_state.get_default_target()?;
|
||||
|
||||
let mut branch = vb_state.get_branch_in_workspace(branch_id)?;
|
||||
|
||||
let target_commit = repo
|
||||
.find_commit(default_target.sha)
|
||||
.context("failed to find target commit")?;
|
||||
let target_tree = target_commit.tree().context("failed to get target tree")?;
|
||||
|
||||
// calculate the merge base and make sure it's the same as the target commit
|
||||
// if not, we need to merge or rebase the branch to get it up to date
|
||||
|
||||
let merge_base = repo
|
||||
.merge_base(default_target.sha, branch.head)
|
||||
.context(format!(
|
||||
"failed to find merge base between {} and {}",
|
||||
default_target.sha, branch.head
|
||||
))?;
|
||||
if merge_base != default_target.sha {
|
||||
// Branch is out of date, merge or rebase it
|
||||
let merge_base_tree = repo
|
||||
.find_commit(merge_base)
|
||||
.context(format!("failed to find merge base commit {}", merge_base))?
|
||||
.tree()
|
||||
.context("failed to find merge base tree")?;
|
||||
|
||||
let branch_tree = repo
|
||||
.find_tree(branch.tree)
|
||||
.context("failed to find branch tree")?;
|
||||
|
||||
let mut merge_index = repo
|
||||
.merge_trees(&merge_base_tree, &branch_tree, &target_tree, None)
|
||||
.context("failed to merge trees")?;
|
||||
|
||||
if merge_index.has_conflicts() {
|
||||
// currently we can only deal with the merge problem branch
|
||||
for branch in vb_state
|
||||
.list_branches_in_workspace()?
|
||||
.iter()
|
||||
.filter(|branch| branch.id != branch_id)
|
||||
{
|
||||
self.convert_to_real_branch(branch.id, Default::default())?;
|
||||
}
|
||||
|
||||
// apply the branch
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
|
||||
// checkout the conflicts
|
||||
repo.checkout_index_builder(&mut merge_index)
|
||||
.allow_conflicts()
|
||||
.conflict_style_merge()
|
||||
.force()
|
||||
.checkout()
|
||||
.context("failed to checkout index")?;
|
||||
|
||||
// mark conflicts
|
||||
let conflicts = merge_index
|
||||
.conflicts()
|
||||
.context("failed to get merge index conflicts")?;
|
||||
let mut merge_conflicts = Vec::new();
|
||||
for path in conflicts.flatten() {
|
||||
if let Some(ours) = path.our {
|
||||
let path = std::str::from_utf8(&ours.path)
|
||||
.context("failed to convert path to utf8")?
|
||||
.to_string();
|
||||
merge_conflicts.push(path);
|
||||
}
|
||||
}
|
||||
conflicts::mark(
|
||||
self.project_repository,
|
||||
&merge_conflicts,
|
||||
Some(default_target.sha),
|
||||
)?;
|
||||
|
||||
return Ok(branch.name);
|
||||
}
|
||||
|
||||
let head_commit = repo
|
||||
.find_commit(branch.head)
|
||||
.context("failed to find head commit")?;
|
||||
|
||||
let merged_branch_tree_oid = merge_index
|
||||
.write_tree_to(self.project_repository.repo())
|
||||
.context("failed to write tree")?;
|
||||
|
||||
let merged_branch_tree = repo
|
||||
.find_tree(merged_branch_tree_oid)
|
||||
.context("failed to find tree")?;
|
||||
|
||||
let ok_with_force_push = branch.allow_rebasing;
|
||||
if branch.upstream.is_some() && !ok_with_force_push {
|
||||
// branch was pushed to upstream, and user doesn't like force pushing.
|
||||
// create a merge commit to avoid the need of force pushing then.
|
||||
|
||||
let new_branch_head = self.project_repository.commit(
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
default_target.branch.remote(),
|
||||
default_target.branch.branch(),
|
||||
branch.name
|
||||
)
|
||||
.as_str(),
|
||||
&merged_branch_tree,
|
||||
&[&head_commit, &target_commit],
|
||||
None,
|
||||
)?;
|
||||
|
||||
// ok, update the virtual branch
|
||||
branch.head = new_branch_head;
|
||||
} else {
|
||||
let rebase = cherry_rebase(
|
||||
self.project_repository,
|
||||
target_commit.id(),
|
||||
target_commit.id(),
|
||||
branch.head,
|
||||
);
|
||||
let mut rebase_success = true;
|
||||
let mut last_rebase_head = branch.head;
|
||||
match rebase {
|
||||
Ok(rebase_oid) => {
|
||||
if let Some(oid) = rebase_oid {
|
||||
last_rebase_head = oid;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
rebase_success = false;
|
||||
}
|
||||
}
|
||||
|
||||
if rebase_success {
|
||||
// rebase worked out, rewrite the branch head
|
||||
branch.head = last_rebase_head;
|
||||
} else {
|
||||
// rebase failed, do a merge commit
|
||||
|
||||
// get tree from merge_tree_oid
|
||||
let merge_tree = repo
|
||||
.find_tree(merged_branch_tree_oid)
|
||||
.context("failed to find tree")?;
|
||||
|
||||
// commit the merge tree oid
|
||||
let new_branch_head = self
|
||||
.project_repository
|
||||
.commit(
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
default_target.branch.remote(),
|
||||
default_target.branch.branch(),
|
||||
branch.name
|
||||
)
|
||||
.as_str(),
|
||||
&merge_tree,
|
||||
&[&head_commit, &target_commit],
|
||||
None,
|
||||
)
|
||||
.context("failed to commit merge")?;
|
||||
|
||||
branch.head = new_branch_head;
|
||||
}
|
||||
}
|
||||
|
||||
branch.tree = repo
|
||||
.find_commit(branch.head)?
|
||||
.tree()
|
||||
.map_err(anyhow::Error::from)?
|
||||
.id();
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
}
|
||||
|
||||
let wd_tree = self.project_repository.repo().get_wd_tree()?;
|
||||
|
||||
let branch_tree = repo
|
||||
.find_tree(branch.tree)
|
||||
.context("failed to find branch tree")?;
|
||||
|
||||
// check index for conflicts
|
||||
let mut merge_index = repo
|
||||
.merge_trees(&target_tree, &wd_tree, &branch_tree, None)
|
||||
.context("failed to merge trees")?;
|
||||
|
||||
if merge_index.has_conflicts() {
|
||||
// mark conflicts
|
||||
let conflicts = merge_index
|
||||
.conflicts()
|
||||
.context("failed to get merge index conflicts")?;
|
||||
let mut merge_conflicts = Vec::new();
|
||||
for path in conflicts.flatten() {
|
||||
if let Some(ours) = path.our {
|
||||
let path = std::str::from_utf8(&ours.path)
|
||||
.context("failed to convert path to utf8")?
|
||||
.to_string();
|
||||
merge_conflicts.push(path);
|
||||
}
|
||||
}
|
||||
conflicts::mark(
|
||||
self.project_repository,
|
||||
&merge_conflicts,
|
||||
Some(default_target.sha),
|
||||
)?;
|
||||
}
|
||||
|
||||
// apply the branch
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
|
||||
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
|
||||
// checkout the merge index
|
||||
repo.checkout_index_builder(&mut merge_index)
|
||||
.force()
|
||||
.checkout()
|
||||
.context("failed to checkout index")?;
|
||||
|
||||
// Look for and handle the vbranch indicator commit
|
||||
// TODO: This is not unapplying the WIP commit for some unholy reason.
|
||||
// If you can figgure it out I'll buy you a beer.
|
||||
{
|
||||
if let Some(wip_commit_to_unapply) = branch.not_in_workspace_wip_change_id {
|
||||
let potential_wip_commit = repo.find_commit(branch.head)?;
|
||||
|
||||
if let Some(headers) = potential_wip_commit.gitbutler_headers() {
|
||||
if headers.change_id == wip_commit_to_unapply {
|
||||
undo_commit(self.project_repository, branch.id, branch.head)?;
|
||||
}
|
||||
}
|
||||
|
||||
branch.not_in_workspace_wip_change_id = None;
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
update_gitbutler_integration(&vb_state, self.project_repository)?;
|
||||
|
||||
Ok(branch.name)
|
||||
}
|
||||
|
||||
// to unapply a branch, we need to write the current tree out, then remove those file changes from the wd
|
||||
pub fn convert_to_real_branch(
|
||||
&self,
|
||||
branch_id: BranchId,
|
||||
name_conflict_resolution: NameConflitResolution,
|
||||
) -> Result<ReferenceName> {
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
|
||||
let mut target_branch = vb_state.get_branch_in_workspace(branch_id)?;
|
||||
|
||||
// Convert the vbranch to a real branch
|
||||
let real_branch = self.build_real_branch(&mut target_branch, name_conflict_resolution)?;
|
||||
|
||||
self.delete_branch(branch_id)?;
|
||||
|
||||
// If we were conflicting, it means that it was the only branch applied. Since we've now unapplied it we can clear all conflicts
|
||||
if conflicts::is_conflicting(self.project_repository, None)? {
|
||||
conflicts::clear(self.project_repository)?;
|
||||
}
|
||||
|
||||
vb_state.update_ordering()?;
|
||||
|
||||
// Ensure we still have a default target
|
||||
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
|
||||
|
||||
crate::integration::update_gitbutler_integration(&vb_state, self.project_repository)?;
|
||||
|
||||
real_branch.reference_name()
|
||||
}
|
||||
|
||||
fn build_real_branch(
|
||||
&self,
|
||||
vbranch: &mut branch::Branch,
|
||||
name_conflict_resolution: NameConflitResolution,
|
||||
) -> Result<git2::Branch<'_>> {
|
||||
let repo = self.project_repository.repo();
|
||||
let target_commit = repo.find_commit(vbranch.head)?;
|
||||
let branch_name = vbranch.name.clone();
|
||||
let branch_name = normalize_branch_name(&branch_name);
|
||||
|
||||
// Is there a name conflict?
|
||||
let branch_name = if repo
|
||||
.find_branch(branch_name.as_str(), git2::BranchType::Local)
|
||||
.is_ok()
|
||||
{
|
||||
match name_conflict_resolution {
|
||||
NameConflitResolution::Suffix => {
|
||||
let mut suffix = 1;
|
||||
loop {
|
||||
let new_branch_name = format!("{}-{}", branch_name, suffix);
|
||||
if repo
|
||||
.find_branch(new_branch_name.as_str(), git2::BranchType::Local)
|
||||
.is_err()
|
||||
{
|
||||
break new_branch_name;
|
||||
}
|
||||
suffix += 1;
|
||||
}
|
||||
}
|
||||
NameConflitResolution::Rename(new_name) => {
|
||||
if repo
|
||||
.find_branch(new_name.as_str(), git2::BranchType::Local)
|
||||
.is_ok()
|
||||
{
|
||||
Err(anyhow!("Branch with name {} already exists", new_name))?
|
||||
} else {
|
||||
new_name
|
||||
}
|
||||
}
|
||||
NameConflitResolution::Overwrite => branch_name,
|
||||
}
|
||||
} else {
|
||||
branch_name
|
||||
};
|
||||
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
let branch = repo.branch(&branch_name, &target_commit, true)?;
|
||||
vbranch.source_refname = Some(Refname::try_from(&branch)?);
|
||||
vb_state.set_branch(vbranch.clone())?;
|
||||
|
||||
self.build_metadata_commit(vbranch, &branch)?;
|
||||
|
||||
Ok(branch)
|
||||
}
|
||||
|
||||
fn build_metadata_commit(
|
||||
&self,
|
||||
vbranch: &mut branch::Branch,
|
||||
branch: &git2::Branch<'_>,
|
||||
) -> Result<git2::Oid> {
|
||||
let repo = self.project_repository.repo();
|
||||
|
||||
// Build wip tree as either any uncommitted changes or an empty tree
|
||||
let vbranch_wip_tree = repo.find_tree(vbranch.tree)?;
|
||||
let vbranch_head_tree = repo.find_commit(vbranch.head)?.tree()?;
|
||||
|
||||
let tree = if vbranch_head_tree.id() != vbranch_wip_tree.id() {
|
||||
vbranch_wip_tree
|
||||
} else {
|
||||
repo.find_tree(TreeUpdateBuilder::new().create_updated(repo, &vbranch_head_tree)?)?
|
||||
};
|
||||
|
||||
// Build commit message
|
||||
let mut message = "GitButler WIP Commit".to_string();
|
||||
message.push_str("\n\n");
|
||||
|
||||
// Commit wip commit
|
||||
let committer = get_integration_commiter()?;
|
||||
let parent = branch.get().peel_to_commit()?;
|
||||
|
||||
let commit_headers = CommitHeadersV2::new();
|
||||
|
||||
let commit_oid = repo.commit_with_signature(
|
||||
Some(&branch.try_into()?),
|
||||
&committer,
|
||||
&committer,
|
||||
&message,
|
||||
&tree,
|
||||
&[&parent],
|
||||
Some(commit_headers.clone()),
|
||||
)?;
|
||||
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
// vbranch.head = commit_oid;
|
||||
vbranch.not_in_workspace_wip_change_id = Some(commit_headers.change_id);
|
||||
vb_state.set_branch(vbranch.clone())?;
|
||||
|
||||
Ok(commit_oid)
|
||||
}
|
||||
|
||||
pub fn delete_branch(&self, branch_id: BranchId) -> Result<()> {
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
let Some(branch) = vb_state.try_branch_in_workspace(branch_id)? else {
|
||||
return Ok(());
|
||||
};
|
||||
_ = self
|
||||
.project_repository
|
||||
.project()
|
||||
.snapshot_branch_deletion(branch.name.clone());
|
||||
|
||||
let repo = self.project_repository.repo();
|
||||
|
||||
let integration_commit = repo.integration_commit()?;
|
||||
let target_commit = repo.target_commit()?;
|
||||
let base_tree = target_commit.tree().context("failed to get target tree")?;
|
||||
|
||||
let virtual_branches = vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to read virtual branches")?;
|
||||
|
||||
let (applied_statuses, _) = get_applied_status(
|
||||
self.project_repository,
|
||||
&integration_commit.id(),
|
||||
&target_commit.id(),
|
||||
virtual_branches,
|
||||
)
|
||||
.context("failed to get status by branch")?;
|
||||
|
||||
// go through the other applied branches and merge them into the final tree
|
||||
// then check that out into the working directory
|
||||
let final_tree = applied_statuses
|
||||
.into_iter()
|
||||
.filter(|(branch, _)| branch.id != branch_id)
|
||||
.fold(
|
||||
target_commit.tree().context("failed to get target tree"),
|
||||
|final_tree, status| {
|
||||
let final_tree = final_tree?;
|
||||
let branch = status.0;
|
||||
let tree_oid = write_tree(self.project_repository, &branch.head, status.1)?;
|
||||
let branch_tree = repo.find_tree(tree_oid)?;
|
||||
let mut result =
|
||||
repo.merge_trees(&base_tree, &final_tree, &branch_tree, None)?;
|
||||
let final_tree_oid = result.write_tree_to(repo)?;
|
||||
repo.find_tree(final_tree_oid)
|
||||
.context("failed to find tree")
|
||||
},
|
||||
)?;
|
||||
|
||||
// checkout final_tree into the working directory
|
||||
repo.checkout_tree_builder(&final_tree)
|
||||
.force()
|
||||
.remove_untracked()
|
||||
.checkout()
|
||||
.context("failed to checkout tree")?;
|
||||
|
||||
vb_state
|
||||
.mark_as_not_in_workspace(branch.id)
|
||||
.context("Failed to remove branch")?;
|
||||
|
||||
self.project_repository.delete_branch_reference(&branch)?;
|
||||
|
||||
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use gitbutler_branch::{
|
||||
branch::{BranchCreateRequest, BranchId, BranchUpdateRequest},
|
||||
branch_ext::BranchExt,
|
||||
diff,
|
||||
ownership::BranchOwnershipClaims,
|
||||
};
|
||||
@ -13,8 +12,9 @@ use gitbutler_oplog::{
|
||||
snapshot::Snapshot,
|
||||
};
|
||||
use gitbutler_project::{FetchResult, Project};
|
||||
use gitbutler_reference::{ReferenceName, Refname, RemoteRefname};
|
||||
use gitbutler_reference::{Refname, RemoteRefname};
|
||||
use gitbutler_repo::{credentials::Helper, RepoActions, RepositoryExt};
|
||||
use gitbutler_tagged_string::ReferenceName;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use tokio::sync::Semaphore;
|
||||
@ -24,6 +24,7 @@ use crate::{
|
||||
get_base_branch_data, set_base_branch, set_target_push_remote, update_base_branch,
|
||||
BaseBranch,
|
||||
},
|
||||
branch_manager::BranchManagerAccess,
|
||||
remote::{get_branch_data, list_remote_branches, RemoteBranch, RemoteBranchData},
|
||||
};
|
||||
|
||||
@ -103,21 +104,11 @@ impl Controller {
|
||||
self.permit(project.ignore_project_semaphore).await;
|
||||
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let branch_id = branch::create_virtual_branch(&project_repository, create)?.id;
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch_id = branch_manager.create_virtual_branch(create)?.id;
|
||||
Ok(branch_id)
|
||||
}
|
||||
|
||||
pub async fn create_virtual_branch_from_branch(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch: &Refname,
|
||||
) -> Result<BranchId> {
|
||||
self.permit(project.ignore_project_semaphore).await;
|
||||
|
||||
let project_repository = open_with_verify(project)?;
|
||||
branch::create_virtual_branch_from_branch(&project_repository, branch).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn get_base_branch_data(&self, project: &Project) -> Result<BaseBranch> {
|
||||
let project_repository = ProjectRepository::open(project)?;
|
||||
get_base_branch_data(&project_repository)
|
||||
@ -171,14 +162,7 @@ impl Controller {
|
||||
let _ = project_repository
|
||||
.project()
|
||||
.create_snapshot(SnapshotDetails::new(OperationKind::UpdateWorkspaceBase));
|
||||
update_base_branch(&project_repository)
|
||||
.map(|unapplied_branches| {
|
||||
unapplied_branches
|
||||
.iter()
|
||||
.filter_map(|unapplied_branch| unapplied_branch.reference_name().ok())
|
||||
.collect()
|
||||
})
|
||||
.map_err(Into::into)
|
||||
update_base_branch(&project_repository).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn update_virtual_branch(
|
||||
@ -214,7 +198,8 @@ impl Controller {
|
||||
self.permit(project.ignore_project_semaphore).await;
|
||||
|
||||
let project_repository = open_with_verify(project)?;
|
||||
branch::delete_branch(&project_repository, branch_id)
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
branch_manager.delete_branch(branch_id)
|
||||
}
|
||||
|
||||
pub async fn unapply_ownership(
|
||||
@ -362,18 +347,16 @@ impl Controller {
|
||||
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let snapshot_tree = project_repository.project().prepare_snapshot();
|
||||
let result = branch::convert_to_real_branch(
|
||||
&project_repository,
|
||||
branch_id,
|
||||
name_conflict_resolution,
|
||||
)
|
||||
.map_err(Into::into);
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let result = branch_manager.convert_to_real_branch(branch_id, name_conflict_resolution);
|
||||
|
||||
let _ = snapshot_tree.and_then(|snapshot_tree| {
|
||||
project_repository
|
||||
.project()
|
||||
.snapshot_branch_unapplied(snapshot_tree, result.as_ref())
|
||||
});
|
||||
result.and_then(|b| b.reference_name())
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn push_virtual_branch(
|
||||
@ -492,9 +475,23 @@ impl Controller {
|
||||
branch::move_commit(&project_repository, target_branch_id, commit_oid).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn create_virtual_branch_from_branch(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch: &Refname,
|
||||
) -> Result<BranchId> {
|
||||
self.permit(project.ignore_project_semaphore).await;
|
||||
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
branch_manager
|
||||
.create_virtual_branch_from_branch(branch)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn permit(&self, ignore: bool) {
|
||||
if !ignore {
|
||||
let _permit = self.semaphore.acquire().await;
|
||||
let _ = self.semaphore.acquire().await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ use gitbutler_commit::commit_ext::CommitExt;
|
||||
use gitbutler_error::error::Marker;
|
||||
use gitbutler_repo::{LogUntil, RepoActions, RepositoryExt};
|
||||
|
||||
use crate::branch_manager::BranchManagerAccess;
|
||||
use crate::conflicts;
|
||||
|
||||
const WORKSPACE_HEAD: &str = "Workspace Head";
|
||||
@ -358,16 +359,15 @@ impl Verify for ProjectRepository {
|
||||
)
|
||||
.context("failed to reset to integration commit")?;
|
||||
|
||||
let mut new_branch = super::create_virtual_branch(
|
||||
self,
|
||||
&BranchCreateRequest {
|
||||
let branch_manager = self.branch_manager();
|
||||
let mut new_branch = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest {
|
||||
name: extra_commits
|
||||
.last()
|
||||
.map(|commit| commit.message_bstr().to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.context("failed to create virtual branch")?;
|
||||
})
|
||||
.context("failed to create virtual branch")?;
|
||||
|
||||
// rebasing the extra commits onto the new branch
|
||||
let vb_state = self.project().virtual_branches();
|
||||
|
@ -5,6 +5,8 @@ pub use controller::Controller;
|
||||
pub mod r#virtual;
|
||||
pub use r#virtual::*;
|
||||
|
||||
pub mod branch_manager;
|
||||
|
||||
pub mod assets;
|
||||
|
||||
pub mod base;
|
||||
@ -18,4 +20,3 @@ pub mod remote;
|
||||
pub mod conflicts;
|
||||
|
||||
mod author;
|
||||
mod dedup;
|
||||
|
@ -1,4 +1,5 @@
|
||||
use gitbutler_branch::branch::{self, Branch, BranchCreateRequest, BranchId};
|
||||
use gitbutler_branch::dedup::{dedup, dedup_fmt};
|
||||
use gitbutler_branch::diff::{self, diff_files_into_hunks, trees, FileDiff, GitHunk};
|
||||
use gitbutler_branch::file_ownership::OwnershipClaim;
|
||||
use gitbutler_branch::hunk::{Hunk, HunkHash};
|
||||
@ -6,8 +7,7 @@ use gitbutler_branch::ownership::{reconcile_claims, BranchOwnershipClaims};
|
||||
use gitbutler_branchstate::{VirtualBranchesAccess, VirtualBranchesHandle};
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_commit::commit_ext::CommitExt;
|
||||
use gitbutler_commit::commit_headers::{CommitHeadersV2, HasCommitHeaders};
|
||||
use gitbutler_oplog::snapshot::Snapshot;
|
||||
use gitbutler_commit::commit_headers::HasCommitHeaders;
|
||||
use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname};
|
||||
use gitbutler_repo::credentials::Helper;
|
||||
use gitbutler_repo::{LogUntil, RepoActions, RepositoryExt};
|
||||
@ -24,16 +24,15 @@ use std::{
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use bstr::{BString, ByteSlice, ByteVec};
|
||||
use diffy::{apply_bytes as diffy_apply, Line, Patch};
|
||||
use git2::build::TreeUpdateBuilder;
|
||||
use git2::ErrorCode;
|
||||
use git2_hooks::HookResult;
|
||||
use hex::ToHex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::author::Author;
|
||||
use crate::branch_manager::BranchManagerAccess;
|
||||
use crate::conflicts::{self, RepoConflicts};
|
||||
use crate::dedup::{dedup, dedup_fmt};
|
||||
use crate::integration::{get_integration_commiter, get_workspace_head};
|
||||
use crate::integration::get_workspace_head;
|
||||
use crate::remote::{branch_to_remote_branch, RemoteBranch};
|
||||
use gitbutler_branch::target;
|
||||
use gitbutler_error::error::Code;
|
||||
@ -164,7 +163,7 @@ pub struct VirtualBranchHunk {
|
||||
|
||||
/// Lifecycle
|
||||
impl VirtualBranchHunk {
|
||||
pub(crate) fn gen_id(new_start: u32, new_lines: u32) -> String {
|
||||
pub fn gen_id(new_start: u32, new_lines: u32) -> String {
|
||||
format!("{}-{}", new_start, new_start + new_lines)
|
||||
}
|
||||
fn from_git_hunk(
|
||||
@ -322,139 +321,6 @@ pub fn reset_files(project_repository: &ProjectRepository, files: &Vec<String>)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// to unapply a branch, we need to write the current tree out, then remove those file changes from the wd
|
||||
pub fn convert_to_real_branch(
|
||||
project_repository: &ProjectRepository,
|
||||
branch_id: BranchId,
|
||||
name_conflict_resolution: NameConflitResolution,
|
||||
) -> Result<git2::Branch<'_>> {
|
||||
fn build_real_branch<'l>(
|
||||
project_repository: &'l ProjectRepository,
|
||||
vbranch: &mut branch::Branch,
|
||||
name_conflict_resolution: NameConflitResolution,
|
||||
) -> Result<git2::Branch<'l>> {
|
||||
let repo = project_repository.repo();
|
||||
let target_commit = repo.find_commit(vbranch.head)?;
|
||||
let branch_name = vbranch.name.clone();
|
||||
let branch_name = normalize_branch_name(&branch_name);
|
||||
|
||||
// Is there a name conflict?
|
||||
let branch_name = if repo
|
||||
.find_branch(branch_name.as_str(), git2::BranchType::Local)
|
||||
.is_ok()
|
||||
{
|
||||
match name_conflict_resolution {
|
||||
NameConflitResolution::Suffix => {
|
||||
let mut suffix = 1;
|
||||
loop {
|
||||
let new_branch_name = format!("{}-{}", branch_name, suffix);
|
||||
if repo
|
||||
.find_branch(new_branch_name.as_str(), git2::BranchType::Local)
|
||||
.is_err()
|
||||
{
|
||||
break new_branch_name;
|
||||
}
|
||||
suffix += 1;
|
||||
}
|
||||
}
|
||||
NameConflitResolution::Rename(new_name) => {
|
||||
if repo
|
||||
.find_branch(new_name.as_str(), git2::BranchType::Local)
|
||||
.is_ok()
|
||||
{
|
||||
Err(anyhow!("Branch with name {} already exists", new_name))?
|
||||
} else {
|
||||
new_name
|
||||
}
|
||||
}
|
||||
NameConflitResolution::Overwrite => branch_name,
|
||||
}
|
||||
} else {
|
||||
branch_name
|
||||
};
|
||||
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
let branch = repo.branch(&branch_name, &target_commit, true)?;
|
||||
vbranch.source_refname = Some(Refname::try_from(&branch)?);
|
||||
vb_state.set_branch(vbranch.clone())?;
|
||||
|
||||
build_metadata_commit(project_repository, vbranch, &branch)?;
|
||||
|
||||
Ok(branch)
|
||||
}
|
||||
fn build_metadata_commit<'l>(
|
||||
project_repository: &'l ProjectRepository,
|
||||
vbranch: &mut branch::Branch,
|
||||
branch: &git2::Branch<'l>,
|
||||
) -> Result<git2::Oid> {
|
||||
let repo = project_repository.repo();
|
||||
|
||||
// Build wip tree as either any uncommitted changes or an empty tree
|
||||
let vbranch_wip_tree = repo.find_tree(vbranch.tree)?;
|
||||
let vbranch_head_tree = repo.find_commit(vbranch.head)?.tree()?;
|
||||
|
||||
let tree = if vbranch_head_tree.id() != vbranch_wip_tree.id() {
|
||||
vbranch_wip_tree
|
||||
} else {
|
||||
repo.find_tree(TreeUpdateBuilder::new().create_updated(repo, &vbranch_head_tree)?)?
|
||||
};
|
||||
|
||||
// Build commit message
|
||||
let mut message = "GitButler WIP Commit".to_string();
|
||||
message.push_str("\n\n");
|
||||
|
||||
// Commit wip commit
|
||||
let committer = get_integration_commiter()?;
|
||||
let parent = branch.get().peel_to_commit()?;
|
||||
|
||||
let commit_headers = CommitHeadersV2::new();
|
||||
|
||||
let commit_oid = repo.commit_with_signature(
|
||||
Some(&branch.try_into()?),
|
||||
&committer,
|
||||
&committer,
|
||||
&message,
|
||||
&tree,
|
||||
&[&parent],
|
||||
Some(commit_headers.clone()),
|
||||
)?;
|
||||
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
// vbranch.head = commit_oid;
|
||||
vbranch.not_in_workspace_wip_change_id = Some(commit_headers.change_id);
|
||||
vb_state.set_branch(vbranch.clone())?;
|
||||
|
||||
Ok(commit_oid)
|
||||
}
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
|
||||
let mut target_branch = vb_state.get_branch_in_workspace(branch_id)?;
|
||||
|
||||
// Convert the vbranch to a real branch
|
||||
let real_branch = build_real_branch(
|
||||
project_repository,
|
||||
&mut target_branch,
|
||||
name_conflict_resolution,
|
||||
)?;
|
||||
|
||||
delete_branch(project_repository, branch_id)?;
|
||||
|
||||
// If we were conflicting, it means that it was the only branch applied. Since we've now unapplied it we can clear all conflicts
|
||||
if conflicts::is_conflicting(project_repository, None)? {
|
||||
conflicts::clear(project_repository)?;
|
||||
}
|
||||
|
||||
vb_state.update_ordering()?;
|
||||
|
||||
// Ensure we still have a default target
|
||||
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
|
||||
|
||||
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
|
||||
Ok(real_branch)
|
||||
}
|
||||
|
||||
fn find_base_tree<'a>(
|
||||
repo: &'a git2::Repository,
|
||||
branch_commit: &'a git2::Commit<'a>,
|
||||
@ -487,9 +353,11 @@ fn resolve_old_applied_state(
|
||||
) -> Result<()> {
|
||||
let branches = vb_state.list_all_branches()?;
|
||||
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
|
||||
for mut branch in branches {
|
||||
if !branch.old_applied && branch.in_workspace {
|
||||
convert_to_real_branch(project_repository, branch.id, Default::default())?;
|
||||
branch_manager.convert_to_real_branch(branch.id, Default::default())?;
|
||||
} else {
|
||||
branch.old_applied = branch.in_workspace;
|
||||
vb_state.set_branch(branch)?;
|
||||
@ -764,108 +632,6 @@ fn commit_to_vbranch_commit(
|
||||
Ok(commit)
|
||||
}
|
||||
|
||||
pub fn create_virtual_branch(
|
||||
project_repository: &ProjectRepository,
|
||||
create: &BranchCreateRequest,
|
||||
) -> Result<branch::Branch> {
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
|
||||
let default_target = vb_state.get_default_target()?;
|
||||
|
||||
let commit = project_repository
|
||||
.repo()
|
||||
.find_commit(default_target.sha)
|
||||
.context("failed to find default target commit")?;
|
||||
|
||||
let tree = commit
|
||||
.tree()
|
||||
.context("failed to find defaut target commit tree")?;
|
||||
|
||||
let mut all_virtual_branches = vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to read virtual branches")?;
|
||||
|
||||
let name = dedup(
|
||||
&all_virtual_branches
|
||||
.iter()
|
||||
.map(|b| b.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
create
|
||||
.name
|
||||
.as_ref()
|
||||
.unwrap_or(&"Virtual branch".to_string()),
|
||||
);
|
||||
|
||||
_ = project_repository
|
||||
.project()
|
||||
.snapshot_branch_creation(name.clone());
|
||||
|
||||
all_virtual_branches.sort_by_key(|branch| branch.order);
|
||||
|
||||
let order = create.order.unwrap_or(vb_state.next_order_index()?);
|
||||
|
||||
let selected_for_changes = if let Some(selected_for_changes) = create.selected_for_changes {
|
||||
if selected_for_changes {
|
||||
for mut other_branch in vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to read virtual branches")?
|
||||
{
|
||||
other_branch.selected_for_changes = None;
|
||||
vb_state.set_branch(other_branch.clone())?;
|
||||
}
|
||||
Some(now_since_unix_epoch_ms())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
(!all_virtual_branches
|
||||
.iter()
|
||||
.any(|b| b.selected_for_changes.is_some()))
|
||||
.then_some(now_since_unix_epoch_ms())
|
||||
};
|
||||
|
||||
// make space for the new branch
|
||||
for (i, branch) in all_virtual_branches.iter().enumerate() {
|
||||
let mut branch = branch.clone();
|
||||
let new_order = if i < order { i } else { i + 1 };
|
||||
if branch.order != new_order {
|
||||
branch.order = new_order;
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
let now = gitbutler_time::time::now_ms();
|
||||
|
||||
let mut branch = Branch {
|
||||
id: BranchId::generate(),
|
||||
name: name.clone(),
|
||||
notes: String::new(),
|
||||
upstream: None,
|
||||
upstream_head: None,
|
||||
tree: tree.id(),
|
||||
head: default_target.sha,
|
||||
created_timestamp_ms: now,
|
||||
updated_timestamp_ms: now,
|
||||
ownership: BranchOwnershipClaims::default(),
|
||||
order,
|
||||
selected_for_changes,
|
||||
allow_rebasing: project_repository.project().ok_with_force_push.into(),
|
||||
old_applied: true,
|
||||
in_workspace: true,
|
||||
not_in_workspace_wip_change_id: None,
|
||||
source_refname: None,
|
||||
};
|
||||
|
||||
if let Some(ownership) = &create.ownership {
|
||||
set_ownership(&vb_state, &mut branch, ownership).context("failed to set ownership")?;
|
||||
}
|
||||
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
project_repository.add_branch_reference(&branch)?;
|
||||
|
||||
Ok(branch)
|
||||
}
|
||||
|
||||
/// Integrates upstream work from a remote branch.
|
||||
///
|
||||
/// First we determine strategy based on preferences and branch state. If you
|
||||
@ -1166,71 +932,7 @@ pub fn update_branch(
|
||||
Ok(branch)
|
||||
}
|
||||
|
||||
pub fn delete_branch(project_repository: &ProjectRepository, branch_id: BranchId) -> Result<()> {
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
let Some(branch) = vb_state.try_branch_in_workspace(branch_id)? else {
|
||||
return Ok(());
|
||||
};
|
||||
_ = project_repository
|
||||
.project()
|
||||
.snapshot_branch_deletion(branch.name.clone());
|
||||
|
||||
let repo = project_repository.repo();
|
||||
|
||||
let integration_commit = repo.integration_commit()?;
|
||||
let target_commit = repo.target_commit()?;
|
||||
let base_tree = target_commit.tree().context("failed to get target tree")?;
|
||||
|
||||
let virtual_branches = vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to read virtual branches")?;
|
||||
|
||||
let (applied_statuses, _) = get_applied_status(
|
||||
project_repository,
|
||||
&integration_commit.id(),
|
||||
&target_commit.id(),
|
||||
virtual_branches,
|
||||
)
|
||||
.context("failed to get status by branch")?;
|
||||
|
||||
// go through the other applied branches and merge them into the final tree
|
||||
// then check that out into the working directory
|
||||
let final_tree = applied_statuses
|
||||
.into_iter()
|
||||
.filter(|(branch, _)| branch.id != branch_id)
|
||||
.fold(
|
||||
target_commit.tree().context("failed to get target tree"),
|
||||
|final_tree, status| {
|
||||
let final_tree = final_tree?;
|
||||
let branch = status.0;
|
||||
let tree_oid = write_tree(project_repository, &branch.head, status.1)?;
|
||||
let branch_tree = repo.find_tree(tree_oid)?;
|
||||
let mut result = repo.merge_trees(&base_tree, &final_tree, &branch_tree, None)?;
|
||||
let final_tree_oid = result.write_tree_to(repo)?;
|
||||
repo.find_tree(final_tree_oid)
|
||||
.context("failed to find tree")
|
||||
},
|
||||
)?;
|
||||
|
||||
// checkout final_tree into the working directory
|
||||
repo.checkout_tree_builder(&final_tree)
|
||||
.force()
|
||||
.remove_untracked()
|
||||
.checkout()
|
||||
.context("failed to checkout tree")?;
|
||||
|
||||
vb_state
|
||||
.mark_as_not_in_workspace(branch.id)
|
||||
.context("Failed to remove branch")?;
|
||||
|
||||
project_repository.delete_branch_reference(&branch)?;
|
||||
|
||||
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> {
|
||||
pub fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> {
|
||||
let mut virtual_branches = vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to list branches")?;
|
||||
@ -1255,7 +957,7 @@ fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_ownership(
|
||||
pub fn set_ownership(
|
||||
vb_state: &VirtualBranchesHandle,
|
||||
target_branch: &mut branch::Branch,
|
||||
ownership: &gitbutler_branch::ownership::BranchOwnershipClaims,
|
||||
@ -1527,7 +1229,7 @@ fn compute_locks(
|
||||
|
||||
// Returns branches and their associated file changes, in addition to a list
|
||||
// of skipped files.
|
||||
fn get_applied_status(
|
||||
pub(crate) fn get_applied_status(
|
||||
project_repository: &ProjectRepository,
|
||||
integration_commit: &git2::Oid,
|
||||
target_sha: &git2::Oid,
|
||||
@ -1547,12 +1249,12 @@ fn get_applied_status(
|
||||
// sort by order, so that the default branch is first (left in the ui)
|
||||
virtual_branches.sort_by(|a, b| a.order.cmp(&b.order));
|
||||
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
|
||||
if virtual_branches.is_empty() && !base_diffs.is_empty() {
|
||||
virtual_branches =
|
||||
vec![
|
||||
create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
.context("failed to create default branch")?,
|
||||
];
|
||||
virtual_branches = vec![branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.context("failed to create default branch")?];
|
||||
}
|
||||
|
||||
let mut diffs_by_branch: HashMap<BranchId, BranchStatus> = virtual_branches
|
||||
@ -3152,392 +2854,6 @@ pub fn move_commit(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_virtual_branch_from_branch(
|
||||
project_repository: &ProjectRepository,
|
||||
upstream: &Refname,
|
||||
) -> Result<BranchId> {
|
||||
fn apply_branch(project_repository: &ProjectRepository, branch_id: BranchId) -> Result<String> {
|
||||
project_repository.assure_resolved()?;
|
||||
project_repository.assure_unconflicted()?;
|
||||
let repo = project_repository.repo();
|
||||
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
let default_target = vb_state.get_default_target()?;
|
||||
|
||||
let mut branch = vb_state.get_branch_in_workspace(branch_id)?;
|
||||
|
||||
let target_commit = repo
|
||||
.find_commit(default_target.sha)
|
||||
.context("failed to find target commit")?;
|
||||
let target_tree = target_commit.tree().context("failed to get target tree")?;
|
||||
|
||||
// calculate the merge base and make sure it's the same as the target commit
|
||||
// if not, we need to merge or rebase the branch to get it up to date
|
||||
|
||||
let merge_base = repo
|
||||
.merge_base(default_target.sha, branch.head)
|
||||
.context(format!(
|
||||
"failed to find merge base between {} and {}",
|
||||
default_target.sha, branch.head
|
||||
))?;
|
||||
if merge_base != default_target.sha {
|
||||
// Branch is out of date, merge or rebase it
|
||||
let merge_base_tree = repo
|
||||
.find_commit(merge_base)
|
||||
.context(format!("failed to find merge base commit {}", merge_base))?
|
||||
.tree()
|
||||
.context("failed to find merge base tree")?;
|
||||
|
||||
let branch_tree = repo
|
||||
.find_tree(branch.tree)
|
||||
.context("failed to find branch tree")?;
|
||||
|
||||
let mut merge_index = repo
|
||||
.merge_trees(&merge_base_tree, &branch_tree, &target_tree, None)
|
||||
.context("failed to merge trees")?;
|
||||
|
||||
if merge_index.has_conflicts() {
|
||||
// currently we can only deal with the merge problem branch
|
||||
for branch in vb_state
|
||||
.list_branches_in_workspace()?
|
||||
.iter()
|
||||
.filter(|branch| branch.id != branch_id)
|
||||
{
|
||||
convert_to_real_branch(project_repository, branch.id, Default::default())?;
|
||||
}
|
||||
|
||||
// apply the branch
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
|
||||
// checkout the conflicts
|
||||
repo.checkout_index_builder(&mut merge_index)
|
||||
.allow_conflicts()
|
||||
.conflict_style_merge()
|
||||
.force()
|
||||
.checkout()
|
||||
.context("failed to checkout index")?;
|
||||
|
||||
// mark conflicts
|
||||
let conflicts = merge_index
|
||||
.conflicts()
|
||||
.context("failed to get merge index conflicts")?;
|
||||
let mut merge_conflicts = Vec::new();
|
||||
for path in conflicts.flatten() {
|
||||
if let Some(ours) = path.our {
|
||||
let path = std::str::from_utf8(&ours.path)
|
||||
.context("failed to convert path to utf8")?
|
||||
.to_string();
|
||||
merge_conflicts.push(path);
|
||||
}
|
||||
}
|
||||
conflicts::mark(
|
||||
project_repository,
|
||||
&merge_conflicts,
|
||||
Some(default_target.sha),
|
||||
)?;
|
||||
|
||||
return Ok(branch.name);
|
||||
}
|
||||
|
||||
let head_commit = repo
|
||||
.find_commit(branch.head)
|
||||
.context("failed to find head commit")?;
|
||||
|
||||
let merged_branch_tree_oid = merge_index
|
||||
.write_tree_to(project_repository.repo())
|
||||
.context("failed to write tree")?;
|
||||
|
||||
let merged_branch_tree = repo
|
||||
.find_tree(merged_branch_tree_oid)
|
||||
.context("failed to find tree")?;
|
||||
|
||||
let ok_with_force_push = branch.allow_rebasing;
|
||||
if branch.upstream.is_some() && !ok_with_force_push {
|
||||
// branch was pushed to upstream, and user doesn't like force pushing.
|
||||
// create a merge commit to avoid the need of force pushing then.
|
||||
|
||||
let new_branch_head = project_repository.commit(
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
default_target.branch.remote(),
|
||||
default_target.branch.branch(),
|
||||
branch.name
|
||||
)
|
||||
.as_str(),
|
||||
&merged_branch_tree,
|
||||
&[&head_commit, &target_commit],
|
||||
None,
|
||||
)?;
|
||||
|
||||
// ok, update the virtual branch
|
||||
branch.head = new_branch_head;
|
||||
} else {
|
||||
let rebase = cherry_rebase(
|
||||
project_repository,
|
||||
target_commit.id(),
|
||||
target_commit.id(),
|
||||
branch.head,
|
||||
);
|
||||
let mut rebase_success = true;
|
||||
let mut last_rebase_head = branch.head;
|
||||
match rebase {
|
||||
Ok(rebase_oid) => {
|
||||
if let Some(oid) = rebase_oid {
|
||||
last_rebase_head = oid;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
rebase_success = false;
|
||||
}
|
||||
}
|
||||
|
||||
if rebase_success {
|
||||
// rebase worked out, rewrite the branch head
|
||||
branch.head = last_rebase_head;
|
||||
} else {
|
||||
// rebase failed, do a merge commit
|
||||
|
||||
// get tree from merge_tree_oid
|
||||
let merge_tree = repo
|
||||
.find_tree(merged_branch_tree_oid)
|
||||
.context("failed to find tree")?;
|
||||
|
||||
// commit the merge tree oid
|
||||
let new_branch_head = project_repository
|
||||
.commit(
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
default_target.branch.remote(),
|
||||
default_target.branch.branch(),
|
||||
branch.name
|
||||
)
|
||||
.as_str(),
|
||||
&merge_tree,
|
||||
&[&head_commit, &target_commit],
|
||||
None,
|
||||
)
|
||||
.context("failed to commit merge")?;
|
||||
|
||||
branch.head = new_branch_head;
|
||||
}
|
||||
}
|
||||
|
||||
branch.tree = repo
|
||||
.find_commit(branch.head)?
|
||||
.tree()
|
||||
.map_err(anyhow::Error::from)?
|
||||
.id();
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
}
|
||||
|
||||
let wd_tree = project_repository.repo().get_wd_tree()?;
|
||||
|
||||
let branch_tree = repo
|
||||
.find_tree(branch.tree)
|
||||
.context("failed to find branch tree")?;
|
||||
|
||||
// check index for conflicts
|
||||
let mut merge_index = repo
|
||||
.merge_trees(&target_tree, &wd_tree, &branch_tree, None)
|
||||
.context("failed to merge trees")?;
|
||||
|
||||
if merge_index.has_conflicts() {
|
||||
// mark conflicts
|
||||
let conflicts = merge_index
|
||||
.conflicts()
|
||||
.context("failed to get merge index conflicts")?;
|
||||
let mut merge_conflicts = Vec::new();
|
||||
for path in conflicts.flatten() {
|
||||
if let Some(ours) = path.our {
|
||||
let path = std::str::from_utf8(&ours.path)
|
||||
.context("failed to convert path to utf8")?
|
||||
.to_string();
|
||||
merge_conflicts.push(path);
|
||||
}
|
||||
}
|
||||
conflicts::mark(
|
||||
project_repository,
|
||||
&merge_conflicts,
|
||||
Some(default_target.sha),
|
||||
)?;
|
||||
}
|
||||
|
||||
// apply the branch
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
|
||||
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
|
||||
// checkout the merge index
|
||||
repo.checkout_index_builder(&mut merge_index)
|
||||
.force()
|
||||
.checkout()
|
||||
.context("failed to checkout index")?;
|
||||
|
||||
// Look for and handle the vbranch indicator commit
|
||||
// TODO: This is not unapplying the WIP commit for some unholy reason.
|
||||
// If you can figgure it out I'll buy you a beer.
|
||||
{
|
||||
if let Some(wip_commit_to_unapply) = branch.not_in_workspace_wip_change_id {
|
||||
let potential_wip_commit = repo.find_commit(branch.head)?;
|
||||
|
||||
if let Some(headers) = potential_wip_commit.gitbutler_headers() {
|
||||
if headers.change_id == wip_commit_to_unapply {
|
||||
undo_commit(project_repository, branch.id, branch.head)?;
|
||||
}
|
||||
}
|
||||
|
||||
branch.not_in_workspace_wip_change_id = None;
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
|
||||
Ok(branch.name)
|
||||
}
|
||||
|
||||
// only set upstream if it's not the default target
|
||||
let upstream_branch = match upstream {
|
||||
Refname::Other(_) | Refname::Virtual(_) => {
|
||||
// we only support local or remote branches
|
||||
bail!("branch {upstream} must be a local or remote branch");
|
||||
}
|
||||
Refname::Remote(remote) => Some(remote.clone()),
|
||||
Refname::Local(local) => local.remote().cloned(),
|
||||
};
|
||||
|
||||
let branch_name = upstream
|
||||
.branch()
|
||||
.expect("always a branch reference")
|
||||
.to_string();
|
||||
|
||||
let _ = project_repository
|
||||
.project()
|
||||
.snapshot_branch_creation(branch_name.clone());
|
||||
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
|
||||
let default_target = vb_state.get_default_target()?;
|
||||
|
||||
if let Refname::Remote(remote_upstream) = upstream {
|
||||
if default_target.branch == *remote_upstream {
|
||||
bail!("cannot create a branch from default target")
|
||||
}
|
||||
}
|
||||
|
||||
let repo = project_repository.repo();
|
||||
let head_reference = repo
|
||||
.find_reference(&upstream.to_string())
|
||||
.map_err(|err| match err {
|
||||
err if err.code() == git2::ErrorCode::NotFound => {
|
||||
anyhow!("branch {upstream} was not found")
|
||||
}
|
||||
err => err.into(),
|
||||
})?;
|
||||
let head_commit = head_reference
|
||||
.peel_to_commit()
|
||||
.context("failed to peel to commit")?;
|
||||
let head_commit_tree = head_commit.tree().context("failed to find tree")?;
|
||||
|
||||
let virtual_branches = vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to read virtual branches")?
|
||||
.into_iter()
|
||||
.collect::<Vec<branch::Branch>>();
|
||||
|
||||
let order = vb_state.next_order_index()?;
|
||||
|
||||
let selected_for_changes = (!virtual_branches
|
||||
.iter()
|
||||
.any(|b| b.selected_for_changes.is_some()))
|
||||
.then_some(now_since_unix_epoch_ms());
|
||||
|
||||
let now = gitbutler_time::time::now_ms();
|
||||
|
||||
// add file ownership based off the diff
|
||||
let target_commit = repo.find_commit(default_target.sha)?;
|
||||
let merge_base_oid = repo.merge_base(target_commit.id(), head_commit.id())?;
|
||||
let merge_base_tree = repo.find_commit(merge_base_oid)?.tree()?;
|
||||
|
||||
// do a diff between the head of this branch and the target base
|
||||
let diff = diff::trees(
|
||||
project_repository.repo(),
|
||||
&merge_base_tree,
|
||||
&head_commit_tree,
|
||||
)?;
|
||||
|
||||
// assign ownership to the branch
|
||||
let ownership = diff.iter().fold(
|
||||
BranchOwnershipClaims::default(),
|
||||
|mut ownership, (file_path, file)| {
|
||||
for hunk in &file.hunks {
|
||||
ownership.put(
|
||||
format!(
|
||||
"{}:{}",
|
||||
file_path.display(),
|
||||
VirtualBranchHunk::gen_id(hunk.new_start, hunk.new_lines)
|
||||
)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
ownership
|
||||
},
|
||||
);
|
||||
|
||||
let branch = if let Ok(Some(mut branch)) =
|
||||
vb_state.find_by_source_refname_where_not_in_workspace(upstream)
|
||||
{
|
||||
branch.upstream_head = upstream_branch.is_some().then_some(head_commit.id());
|
||||
branch.upstream = upstream_branch;
|
||||
branch.tree = head_commit_tree.id();
|
||||
branch.head = head_commit.id();
|
||||
branch.ownership = ownership;
|
||||
branch.order = order;
|
||||
branch.selected_for_changes = selected_for_changes;
|
||||
branch.allow_rebasing = project_repository.project().ok_with_force_push.into();
|
||||
branch.old_applied = true;
|
||||
branch.in_workspace = true;
|
||||
|
||||
branch
|
||||
} else {
|
||||
branch::Branch {
|
||||
id: BranchId::generate(),
|
||||
name: branch_name.clone(),
|
||||
notes: String::new(),
|
||||
source_refname: Some(upstream.clone()),
|
||||
upstream_head: upstream_branch.is_some().then_some(head_commit.id()),
|
||||
upstream: upstream_branch,
|
||||
tree: head_commit_tree.id(),
|
||||
head: head_commit.id(),
|
||||
created_timestamp_ms: now,
|
||||
updated_timestamp_ms: now,
|
||||
ownership,
|
||||
order,
|
||||
selected_for_changes,
|
||||
allow_rebasing: project_repository.project().ok_with_force_push.into(),
|
||||
old_applied: true,
|
||||
in_workspace: true,
|
||||
not_in_workspace_wip_change_id: None,
|
||||
}
|
||||
};
|
||||
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
project_repository.add_branch_reference(&branch)?;
|
||||
|
||||
match apply_branch(project_repository, branch.id) {
|
||||
Ok(_) => Ok(branch.id),
|
||||
Err(err)
|
||||
if err
|
||||
.downcast_ref()
|
||||
.map_or(false, |marker: &Marker| *marker == Marker::ProjectConflict) =>
|
||||
{
|
||||
// if branch conflicts with the workspace, it's ok. keep it unapplied
|
||||
Ok(branch.id)
|
||||
}
|
||||
Err(err) => Err(err).context("failed to apply"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Just like [`diffy::apply()`], but on error it will attach hashes of the input `base_image` and `patch`.
|
||||
pub fn apply<S: AsRef<[u8]>>(base_image: S, patch: &Patch<'_, [u8]>) -> Result<BString> {
|
||||
fn md5_hash_hex(b: impl AsRef<[u8]>) -> String {
|
||||
|
@ -2,6 +2,7 @@ use std::{
|
||||
collections::HashMap,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
#[cfg(target_family = "unix")]
|
||||
use std::{
|
||||
@ -20,12 +21,12 @@ use gitbutler_branchstate::VirtualBranchesAccess;
|
||||
use gitbutler_commit::{commit_ext::CommitExt, commit_headers::CommitHeadersV2};
|
||||
use gitbutler_reference::{Refname, RemoteRefname};
|
||||
use gitbutler_repo::RepositoryExt;
|
||||
use gitbutler_virtual::integration;
|
||||
use gitbutler_virtual::r#virtual as virtual_branches;
|
||||
use gitbutler_virtual::r#virtual::{
|
||||
commit, create_virtual_branch, create_virtual_branch_from_branch, integrate_upstream_commits,
|
||||
is_remote_branch_mergeable, list_virtual_branches, unapply_ownership, update_branch,
|
||||
commit, integrate_upstream_commits, is_remote_branch_mergeable, list_virtual_branches,
|
||||
unapply_ownership, update_branch,
|
||||
};
|
||||
use gitbutler_virtual::{branch_manager::BranchManagerAccess, integration};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use gitbutler_testsupport::{commit_all, virtual_branches::set_test_target, Case, Suite};
|
||||
@ -44,7 +45,9 @@ fn commit_on_branch_then_change_file_then_get_status() -> Result<()> {
|
||||
|
||||
set_test_target(project_repository)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch1_id = project_repository
|
||||
.branch_manager()
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -113,7 +116,9 @@ fn track_binary_files() -> Result<()> {
|
||||
|
||||
set_test_target(project_repository)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch1_id = project_repository
|
||||
.branch_manager()
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -207,7 +212,9 @@ fn create_branch_with_ownership() -> Result<()> {
|
||||
let file_path = Path::new("test.txt");
|
||||
std::fs::write(Path::new(&project.path).join(file_path), "line1\nline2\n").unwrap();
|
||||
|
||||
let branch0 = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch0 = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch");
|
||||
|
||||
virtual_branches::get_status_by_branch(project_repository, None).expect("failed to get status");
|
||||
@ -215,14 +222,12 @@ fn create_branch_with_ownership() -> Result<()> {
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
let branch0 = vb_state.get_branch_in_workspace(branch0.id).unwrap();
|
||||
|
||||
let branch1 = create_virtual_branch(
|
||||
project_repository,
|
||||
&BranchCreateRequest {
|
||||
let branch1 = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest {
|
||||
ownership: Some(branch0.ownership),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("failed to create virtual branch");
|
||||
})
|
||||
.expect("failed to create virtual branch");
|
||||
|
||||
let statuses = virtual_branches::get_status_by_branch(project_repository, None)
|
||||
.expect("failed to get status")
|
||||
@ -249,18 +254,19 @@ fn create_branch_in_the_middle() -> Result<()> {
|
||||
|
||||
set_test_target(project_repository)?;
|
||||
|
||||
create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch");
|
||||
create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch");
|
||||
create_virtual_branch(
|
||||
project_repository,
|
||||
&BranchCreateRequest {
|
||||
branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest {
|
||||
order: Some(1),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("failed to create virtual branch");
|
||||
})
|
||||
.expect("failed to create virtual branch");
|
||||
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
let mut branches = vb_state
|
||||
@ -284,7 +290,9 @@ fn create_branch_no_arguments() -> Result<()> {
|
||||
|
||||
set_test_target(project_repository)?;
|
||||
|
||||
create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch");
|
||||
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
@ -313,10 +321,13 @@ fn hunk_expantion() -> Result<()> {
|
||||
let file_path = Path::new("test.txt");
|
||||
std::fs::write(Path::new(&project.path).join(file_path), "line1\nline2\n")?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch2_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -404,10 +415,13 @@ fn get_status_files_by_branch() -> Result<()> {
|
||||
let file_path = Path::new("test.txt");
|
||||
std::fs::write(Path::new(&project.path).join(file_path), "line1\nline2\n")?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch2_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -440,13 +454,17 @@ fn move_hunks_multiple_sources() -> Result<()> {
|
||||
|
||||
set_test_target(project_repository)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch2_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
let branch3_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch3_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -539,11 +557,14 @@ fn move_hunks_partial_explicitly() -> Result<()> {
|
||||
"line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\n",
|
||||
)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch2_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -621,7 +642,9 @@ fn add_new_hunk_to_the_end() -> Result<()> {
|
||||
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15\n",
|
||||
)?;
|
||||
|
||||
create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch");
|
||||
|
||||
let statuses = virtual_branches::get_status_by_branch(project_repository, None)
|
||||
@ -791,7 +814,9 @@ fn merge_vbranch_upstream_clean_rebase() -> Result<()> {
|
||||
integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
|
||||
let remote_branch: RemoteRefname = "refs/remotes/origin/master".parse().unwrap();
|
||||
let mut branch = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let mut branch = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch");
|
||||
branch.upstream = Some(remote_branch.clone());
|
||||
branch.head = last_push;
|
||||
@ -888,7 +913,9 @@ async fn merge_vbranch_upstream_conflict() -> Result<()> {
|
||||
)?;
|
||||
|
||||
let remote_branch: RemoteRefname = "refs/remotes/origin/master".parse().unwrap();
|
||||
let mut branch = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let mut branch = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch");
|
||||
branch.upstream = Some(remote_branch.clone());
|
||||
branch.head = last_push;
|
||||
@ -979,7 +1006,9 @@ fn unapply_ownership_partial() -> Result<()> {
|
||||
"line1\nline2\nline3\nline4\nbranch1\n",
|
||||
)?;
|
||||
|
||||
create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch");
|
||||
|
||||
let (branches, _) = virtual_branches::list_virtual_branches(project_repository)?;
|
||||
@ -1033,10 +1062,13 @@ fn unapply_branch() -> Result<()> {
|
||||
let file_path2 = Path::new("test2.txt");
|
||||
std::fs::write(Path::new(&project.path).join(file_path2), "line5\nline6\n")?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch2_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -1062,11 +1094,8 @@ fn unapply_branch() -> Result<()> {
|
||||
assert_eq!(branch.files.len(), 1);
|
||||
assert!(branch.active);
|
||||
|
||||
let real_branch = virtual_branches::convert_to_real_branch(
|
||||
project_repository,
|
||||
branch1_id,
|
||||
Default::default(),
|
||||
)?;
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let real_branch = branch_manager.convert_to_real_branch(branch1_id, Default::default())?;
|
||||
|
||||
let contents = std::fs::read(Path::new(&project.path).join(file_path))?;
|
||||
assert_eq!("line1\nline2\nline3\nline4\n", String::from_utf8(contents)?);
|
||||
@ -1076,10 +1105,9 @@ fn unapply_branch() -> Result<()> {
|
||||
let (branches, _) = virtual_branches::list_virtual_branches(project_repository)?;
|
||||
assert!(!branches.iter().any(|b| b.id == branch1_id));
|
||||
|
||||
let branch1_id = virtual_branches::create_virtual_branch_from_branch(
|
||||
project_repository,
|
||||
&Refname::try_from(&real_branch)?,
|
||||
)?;
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id =
|
||||
branch_manager.create_virtual_branch_from_branch(&Refname::from_str(&real_branch)?)?;
|
||||
let contents = std::fs::read(Path::new(&project.path).join(file_path))?;
|
||||
assert_eq!(
|
||||
"line1\nline2\nline3\nline4\nbranch1\n",
|
||||
@ -1120,10 +1148,13 @@ fn apply_unapply_added_deleted_files() -> Result<()> {
|
||||
let file_path3 = Path::new("test3.txt");
|
||||
std::fs::write(Path::new(&project.path).join(file_path3), "file3\n")?;
|
||||
|
||||
let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch2_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
let branch3_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch3_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -1146,38 +1177,27 @@ fn apply_unapply_added_deleted_files() -> Result<()> {
|
||||
|
||||
list_virtual_branches(project_repository).unwrap();
|
||||
|
||||
let real_branch_2 = virtual_branches::convert_to_real_branch(
|
||||
project_repository,
|
||||
branch2_id,
|
||||
Default::default(),
|
||||
)?;
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let real_branch_2 = branch_manager.convert_to_real_branch(branch2_id, Default::default())?;
|
||||
|
||||
// check that file2 is back
|
||||
let contents = std::fs::read(Path::new(&project.path).join(file_path2))?;
|
||||
assert_eq!("file2\n", String::from_utf8(contents)?);
|
||||
|
||||
let real_branch_3 = virtual_branches::convert_to_real_branch(
|
||||
project_repository,
|
||||
branch3_id,
|
||||
Default::default(),
|
||||
)?;
|
||||
let real_branch_3 = branch_manager.convert_to_real_branch(branch3_id, Default::default())?;
|
||||
// check that file3 is gone
|
||||
assert!(!Path::new(&project.path).join(file_path3).exists());
|
||||
|
||||
create_virtual_branch_from_branch(
|
||||
project_repository,
|
||||
&Refname::try_from(&real_branch_2).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
branch_manager
|
||||
.create_virtual_branch_from_branch(&Refname::from_str(&real_branch_2).unwrap())
|
||||
.unwrap();
|
||||
|
||||
// check that file2 is gone
|
||||
assert!(!Path::new(&project.path).join(file_path2).exists());
|
||||
|
||||
create_virtual_branch_from_branch(
|
||||
project_repository,
|
||||
&Refname::try_from(&real_branch_3).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
branch_manager
|
||||
.create_virtual_branch_from_branch(&Refname::from_str(&real_branch_3).unwrap())
|
||||
.unwrap();
|
||||
|
||||
// check that file3 is back
|
||||
let contents = std::fs::read(Path::new(&project.path).join(file_path3))?;
|
||||
@ -1213,10 +1233,13 @@ fn detect_mergeable_branch() -> Result<()> {
|
||||
let file_path4 = Path::new("test4.txt");
|
||||
std::fs::write(Path::new(&project.path).join(file_path4), "line5\nline6\n")?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch2_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -1231,8 +1254,9 @@ fn detect_mergeable_branch() -> Result<()> {
|
||||
.expect("failed to update branch");
|
||||
|
||||
// unapply both branches and create some conflicting ones
|
||||
virtual_branches::convert_to_real_branch(project_repository, branch1_id, Default::default())?;
|
||||
virtual_branches::convert_to_real_branch(project_repository, branch2_id, Default::default())?;
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
branch_manager.convert_to_real_branch(branch1_id, Default::default())?;
|
||||
branch_manager.convert_to_real_branch(branch2_id, Default::default())?;
|
||||
|
||||
project_repository.repo().set_head("refs/heads/master")?;
|
||||
project_repository
|
||||
@ -1279,9 +1303,12 @@ fn detect_mergeable_branch() -> Result<()> {
|
||||
.checkout_head(Some(&mut git2::build::CheckoutBuilder::default().force()))?;
|
||||
|
||||
// create branches that conflict with our earlier branches
|
||||
create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch");
|
||||
let branch4_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch4_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -1379,13 +1406,17 @@ fn upstream_integrated_vbranch() -> Result<()> {
|
||||
integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
|
||||
// create vbranches, one integrated, one not
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
let branch2_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch2_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
let branch3_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch3_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -1479,7 +1510,9 @@ fn commit_same_hunk_twice() -> Result<()> {
|
||||
|
||||
set_test_target(project_repository)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -1570,7 +1603,9 @@ fn commit_same_file_twice() -> Result<()> {
|
||||
|
||||
set_test_target(project_repository)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -1661,7 +1696,9 @@ fn commit_partial_by_hunk() -> Result<()> {
|
||||
|
||||
set_test_target(project_repository)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -1739,7 +1776,9 @@ fn commit_partial_by_file() -> Result<()> {
|
||||
let file_path3 = Path::new("test3.txt");
|
||||
std::fs::write(Path::new(&project.path).join(file_path3), "file3\n")?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -1797,7 +1836,9 @@ fn commit_add_and_delete_files() -> Result<()> {
|
||||
let file_path3 = Path::new("test3.txt");
|
||||
std::fs::write(Path::new(&project.path).join(file_path3), "file3\n")?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -1861,7 +1902,9 @@ fn commit_executable_and_symlinks() -> Result<()> {
|
||||
let new_permissions = Permissions::from_mode(permissions.mode() | 0o111); // Add execute permission
|
||||
std::fs::set_permissions(&exec, new_permissions)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -2020,7 +2063,9 @@ fn pre_commit_hook_rejection() -> Result<()> {
|
||||
|
||||
set_test_target(project_repository)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -2058,7 +2103,9 @@ fn post_commit_hook() -> Result<()> {
|
||||
|
||||
set_test_target(project_repository)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
@ -2107,7 +2154,9 @@ fn commit_msg_hook_rejection() -> Result<()> {
|
||||
|
||||
set_test_target(project_repository)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(project_repository, &BranchCreateRequest::default())
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch1_id = branch_manager
|
||||
.create_virtual_branch(&BranchCreateRequest::default())
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user