Merge branch 'master' into ndom91/create-gitbutler-ui-package

This commit is contained in:
Nico Domino 2024-07-02 10:04:46 +02:00 committed by GitHub
commit 4fab3ec200
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1375 additions and 2533 deletions

8
Cargo.lock generated
View File

@ -6382,7 +6382,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-log"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5e3900e682e13f3759b439116ae2f77a6d389ca2"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#862c42a457edcbb48e8e3e23696f311f655ff01f"
dependencies = [
"byte-unit",
"fern",
@ -6397,7 +6397,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-single-instance"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5e3900e682e13f3759b439116ae2f77a6d389ca2"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#862c42a457edcbb48e8e3e23696f311f655ff01f"
dependencies = [
"log",
"serde",
@ -6411,7 +6411,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-store"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5e3900e682e13f3759b439116ae2f77a6d389ca2"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#862c42a457edcbb48e8e3e23696f311f655ff01f"
dependencies = [
"log",
"serde",
@ -6423,7 +6423,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-window-state"
version = "0.1.1"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5e3900e682e13f3759b439116ae2f77a6d389ca2"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#862c42a457edcbb48e8e3e23696f311f655ff01f"
dependencies = [
"bincode",
"bitflags 2.5.0",

View File

@ -1,15 +1,19 @@
import { startSpan, setUser, type Span, init } from '@sentry/sveltekit';
import * as Sentry from '@sentry/sveltekit';
import { type Span } from '@sentry/sveltekit';
import type { User } from '$lib/stores/user';
import { dev } from '$app/environment';
import { PUBLIC_SENTRY_ENVIRONMENT } from '$env/static/public';
const { startSpan, setUser, init, rewriteFramesIntegration } = Sentry;
export function initSentry() {
init({
enabled: !dev,
dsn: 'https://a35bbd6688a3a8f76e4956c6871f414a@o4504644069687296.ingest.sentry.io/4505976067129344',
environment: PUBLIC_SENTRY_ENVIRONMENT,
tracesSampleRate: 0.1,
tracePropagationTargets: ['localhost', /gitbutler\.com/i]
tracePropagationTargets: ['localhost', /gitbutler\.com/i],
integrations: [rewriteFramesIntegration()]
});
}

View File

@ -7,13 +7,15 @@
import { projectAiGenEnabled } from '$lib/config/config';
import Button from '$lib/shared/Button.svelte';
import Modal from '$lib/shared/Modal.svelte';
import Select from '$lib/shared/Select.svelte';
import SelectItem from '$lib/shared/SelectItem.svelte';
import TextBox from '$lib/shared/TextBox.svelte';
import Toggle from '$lib/shared/Toggle.svelte';
import { User } from '$lib/stores/user';
import { normalizeBranchName } from '$lib/utils/branch';
import { getContext, getContextStore } from '$lib/utils/context';
import { BranchController } from '$lib/vbranches/branchController';
import { Branch } from '$lib/vbranches/types';
import { Branch, type NameConflictResolution } from '$lib/vbranches/types';
import { createEventDispatcher } from 'svelte';
export let visible: boolean;
@ -51,8 +53,96 @@
function close() {
visible = false;
}
let unapplyBranchModal: Modal;
type ResolutionVariants = NameConflictResolution['type'];
const resolutions: { value: ResolutionVariants; label: string }[] = [
{
value: 'overwrite',
label: 'Overwrite the existing branch'
},
{
value: 'suffix',
label: 'Suffix the branch name'
},
{
value: 'rename',
label: 'Use a new name'
}
];
let selectedResolution: ResolutionVariants = resolutions[0].value;
let newBranchName = '';
function unapplyBranchWithSelectedResolution() {
let resolution: NameConflictResolution | undefined;
if (selectedResolution === 'rename') {
resolution = {
type: selectedResolution,
value: newBranchName
};
} else {
resolution = {
type: selectedResolution,
value: undefined
};
}
branchController.convertToRealBranch(branch.id, resolution);
unapplyBranchModal.close();
}
const remoteBranches = branchController.remoteBranchService.branches$;
function tryUnapplyBranch() {
if ($remoteBranches.find((b) => b.name.endsWith(normalizeBranchName(branch.name)))) {
unapplyBranchModal.show();
} else {
// No resolution required
branchController.convertToRealBranch(branch.id);
}
}
</script>
<Modal bind:this={unapplyBranchModal}>
<div class="flow">
<div class="modal-copy">
<p class="text-base-15">There is already branch with the name</p>
<Button size="tag" clickable={false}>{normalizeBranchName(branch.name)}</Button>
<p class="text-base-15">.</p>
<p class="text-base-15">Please choose how you want to resolve this:</p>
</div>
<Select
items={resolutions}
itemId={'value'}
labelId={'label'}
bind:selectedItemId={selectedResolution}
>
<SelectItem slot="template" let:item let:selected {selected}>
{item.label}
</SelectItem>
</Select>
{#if selectedResolution === 'rename'}
<TextBox
label="New branch name"
id="newBranchName"
bind:value={newBranchName}
placeholder="Enter new branch name"
/>
{/if}
</div>
{#snippet controls()}
<Button style="ghost" outline on:click={() => unapplyBranchModal.close()}>Cancel</Button>
<Button style="pop" kind="solid" grow on:click={unapplyBranchWithSelectedResolution}
>Submit</Button
>
{/snippet}
</Modal>
{#if visible}
<ContextMenu>
<ContextMenuSection>
@ -71,7 +161,7 @@
<ContextMenuItem
label="Unapply"
on:click={() => {
if (branch.id) branchController.unapplyBranch(branch.id);
tryUnapplyBranch();
close();
}}
/>
@ -186,3 +276,16 @@
</Button>
{/snippet}
</Modal>
<style lang="postcss">
.flow {
display: flex;
flex-direction: column;
gap: 16px;
}
.modal-copy {
& > * {
display: inline;
}
}
</style>

View File

@ -4,7 +4,7 @@ import * as toasts from '$lib/utils/toasts';
import posthog from 'posthog-js';
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
import type { BaseBranchService } from './baseBranch';
import type { Branch, Hunk, LocalFile } from './types';
import type { Branch, Hunk, LocalFile, NameConflictResolution } from './types';
import type { VirtualBranchService } from './virtualBranch';
export class BranchController {
@ -180,10 +180,17 @@ export class BranchController {
}
}
async unapplyBranch(branchId: string) {
async convertToRealBranch(
branchId: string,
nameConflictResolution: NameConflictResolution = { type: 'suffix', value: undefined }
) {
try {
// TODO: make this optimistic again.
await invoke<void>('unapply_branch', { projectId: this.projectId, branch: branchId });
await invoke<void>('convert_to_real_branch', {
projectId: this.projectId,
branch: branchId,
nameConflictResolution
});
this.remoteBranchService.reload();
} catch (err) {
showError('Failed to unapply branch', err);
}
@ -284,25 +291,6 @@ You can find them in the 'Branches' sidebar in order to resolve conflicts.`;
}
}
async cherryPick(branchId: string, targetCommitOid: string) {
try {
await invoke<void>('cherry_pick_onto_virtual_branch', {
projectId: this.projectId,
branchId,
targetCommitOid
});
} catch (err: any) {
// TODO: Probably we wanna have error code checking in a more generic way
if (err.code === 'errors.commit.signing_failed') {
showSignError(err);
} else {
showError('Failed to cherry-pick commit', err);
}
} finally {
this.targetBranchService.reload();
}
}
async markResolved(path: string) {
try {
await invoke<void>('mark_resolved', { projectId: this.projectId, path });

View File

@ -461,3 +461,17 @@ export class BaseBranch {
return this.repoBaseUrl.includes('gitlab.com');
}
}
export type NameConflictResolution =
| {
type: 'suffix';
value: undefined;
}
| {
type: 'overwrite';
value: undefined;
}
| {
type: 'rename';
value: string;
};

View File

@ -58,10 +58,8 @@ export class VirtualBranchService {
tap((branches) => {
branches.forEach((branch) => {
branch.files.sort((a) => (a.conflicted ? -1 : 0));
branch.isMergeable = invoke<boolean>('can_apply_virtual_branch', {
projectId: projectId,
branchId: branch.id
});
// This is always true now
branch.isMergeable = Promise.resolve(true);
});
this.fresh$.next(); // Notification for fresh reload
}),

View File

@ -5,6 +5,7 @@ edition = "2021"
authors = ["GitButler <gitbutler@gitbutler.com>"]
publish = false
[dev-dependencies]
once_cell = "1.19"
pretty_assertions = "1.4"

View File

@ -0,0 +1,15 @@
use anyhow::{Context, Result};
use crate::types::ReferenceName;
pub trait BranchExt {
fn reference_name(&self) -> Result<ReferenceName>;
}
impl<'repo> BranchExt for git2::Branch<'repo> {
fn reference_name(&self) -> Result<ReferenceName> {
let name = self.get().name().context("Failed to get branch name")?;
Ok(name.into())
}
}

View File

@ -0,0 +1,91 @@
use bstr::{BStr, BString, ByteSlice, ByteVec};
use core::str;
use super::CommitHeadersV2;
pub struct CommitBuffer {
heading: Vec<(BString, BString)>,
message: BString,
}
impl CommitBuffer {
pub fn new(buffer: &[u8]) -> Self {
let buffer = BStr::new(buffer);
if let Some((heading, message)) = buffer.split_once_str("\n\n") {
let heading = heading
.lines()
.filter_map(|line| line.split_once_str(" "))
.map(|(key, value)| (key.into(), value.into()))
.collect();
Self {
heading,
message: message.into(),
}
} else {
Self {
heading: vec![],
message: buffer.into(),
}
}
}
pub fn set_header(&mut self, key: &str, value: &str) {
let mut set_heading = false;
self.heading.iter_mut().for_each(|(k, v)| {
if k == key {
*v = value.into();
set_heading = true;
}
});
if !set_heading {
self.heading.push((key.into(), value.into()));
}
}
/// Defers to the CommitHeadersV2 struct about which headers should be injected.
/// If `commit_headers: None` is provided, a default set of headers, including a generated change-id will be used
pub fn set_gitbutler_headers(&mut self, commit_headers: Option<CommitHeadersV2>) {
if let Some(commit_headers) = commit_headers {
commit_headers.inject_into(self)
} else {
CommitHeadersV2::inject_default(self)
}
}
pub fn as_bstring(&self) -> BString {
let mut output = BString::new(vec![]);
for (key, value) in &self.heading {
output.push_str(key);
output.push_str(" ");
output.push_str(value);
output.push_str("\n");
}
output.push_str("\n");
output.push_str(&self.message);
output
}
}
impl From<git2::Buf> for CommitBuffer {
fn from(git2_buffer: git2::Buf) -> Self {
Self::new(&git2_buffer)
}
}
impl From<BString> for CommitBuffer {
fn from(s: BString) -> Self {
Self::new(s.as_bytes())
}
}
impl From<CommitBuffer> for BString {
fn from(buffer: CommitBuffer) -> BString {
buffer.as_bstring()
}
}

View File

@ -1,6 +1,7 @@
// use anyhow::Result;
use bstr::BStr;
use super::HasCommitHeaders;
/// Extension trait for `git2::Commit`.
///
/// For now, it collects useful methods from `gitbutler-core::git::Commit`
@ -15,13 +16,9 @@ impl<'repo> CommitExt for git2::Commit<'repo> {
fn message_bstr(&self) -> &BStr {
self.message_bytes().as_ref()
}
fn change_id(&self) -> Option<String> {
let cid = self.header_field_bytes("change-id").ok()?;
if cid.is_empty() {
None
} else {
String::from_utf8(cid.to_owned()).ok()
}
self.gitbutler_headers().map(|headers| headers.change_id)
}
fn is_signed(&self) -> bool {
self.header_field_bytes("gpgsig").is_ok()

View File

@ -0,0 +1,132 @@
use bstr::{BStr, BString};
use uuid::Uuid;
use super::CommitBuffer;
/// Header used to determine which version of the headers is in use. This should never be changed
const HEADERS_VERSION_HEADER: &str = "gitbutler-headers-version";
const V1_CHANGE_ID_HEADER: &str = "change-id";
/// Used to represent the old commit headers layout. This should not be used in new code
#[derive(Debug)]
struct CommitHeadersV1 {
change_id: String,
}
/// The version number used to represent the V2 headers
const V2_HEADERS_VERSION: &str = "2";
const V2_CHANGE_ID_HEADER: &str = "gitbutler-change-id";
const V2_IS_UNAPPLIED_HEADER_COMMIT_HEADER: &str = "gitbutler-is-unapplied-header-commit";
const V2_VBRANCH_NAME_HEADER: &str = "gitbutler-vbranch-name";
#[derive(Debug)]
pub struct CommitHeadersV2 {
pub change_id: String,
pub is_unapplied_header_commit: bool,
pub vbranch_name: Option<String>,
}
impl Default for CommitHeadersV2 {
fn default() -> Self {
CommitHeadersV2 {
// Change ID using base16 encoding
change_id: Uuid::new_v4().to_string(),
is_unapplied_header_commit: false,
vbranch_name: None,
}
}
}
impl From<CommitHeadersV1> for CommitHeadersV2 {
fn from(commit_headers_v1: CommitHeadersV1) -> CommitHeadersV2 {
CommitHeadersV2 {
change_id: commit_headers_v1.change_id,
is_unapplied_header_commit: false,
vbranch_name: None,
}
}
}
pub trait HasCommitHeaders {
fn gitbutler_headers(&self) -> Option<CommitHeadersV2>;
}
impl HasCommitHeaders for git2::Commit<'_> {
fn gitbutler_headers(&self) -> Option<CommitHeadersV2> {
if let Ok(header) = self.header_field_bytes(HEADERS_VERSION_HEADER) {
let version_number = BString::new(header.to_owned());
// Parse v2 headers
if version_number == BStr::new(V2_HEADERS_VERSION) {
let change_id = self.header_field_bytes(V2_CHANGE_ID_HEADER).ok()?;
// We can safely assume that the change id should be UTF8
let change_id = change_id.as_str()?.to_string();
// We can rationalize about is unapplied header commit with a bstring
let is_wip_commit = self
.header_field_bytes(V2_IS_UNAPPLIED_HEADER_COMMIT_HEADER)
.ok()?;
let is_wip_commit = BString::new(is_wip_commit.to_owned());
// We can safely assume that the vbranch name should be UTF8
let vbranch_name = self
.header_field_bytes(V2_VBRANCH_NAME_HEADER)
.ok()
.and_then(|buffer| Some(buffer.as_str()?.to_string()));
Some(CommitHeadersV2 {
change_id,
is_unapplied_header_commit: is_wip_commit == "true",
vbranch_name,
})
} else {
// Must be for a version we don't recognise
None
}
} else {
// Parse v1 headers
let change_id = self.header_field_bytes(V1_CHANGE_ID_HEADER).ok()?;
// We can safely assume that the change id should be UTF8
let change_id = change_id.as_str()?.to_string();
let headers = CommitHeadersV1 { change_id };
Some(headers.into())
}
}
}
impl CommitHeadersV2 {
/// Used to create a CommitHeadersV2. This does not allow a change_id to be
/// provided in order to ensure a consistent format.
pub fn new(is_unapplied_header_commit: bool, vbranch_name: Option<String>) -> CommitHeadersV2 {
CommitHeadersV2 {
is_unapplied_header_commit,
vbranch_name,
..Default::default()
}
}
pub fn inject_default(commit_buffer: &mut CommitBuffer) {
CommitHeadersV2::default().inject_into(commit_buffer)
}
pub fn inject_into(&self, commit_buffer: &mut CommitBuffer) {
commit_buffer.set_header(HEADERS_VERSION_HEADER, V2_HEADERS_VERSION);
commit_buffer.set_header(V2_CHANGE_ID_HEADER, &self.change_id);
let is_unapplied_header_commit = if self.is_unapplied_header_commit {
"true"
} else {
"false"
};
commit_buffer.set_header(
V2_IS_UNAPPLIED_HEADER_COMMIT_HEADER,
is_unapplied_header_commit,
);
if let Some(vbranch_name) = &self.vbranch_name {
commit_buffer.set_header(V2_VBRANCH_NAME_HEADER, vbranch_name);
};
}
}

View File

@ -15,3 +15,12 @@ pub use tree_ext::*;
mod commit_ext;
pub use commit_ext::*;
mod commit_buffer;
pub use commit_buffer::*;
mod commit_headers;
pub use commit_headers::*;
mod branch_ext;
pub use branch_ext::*;

View File

@ -1,4 +1,5 @@
use anyhow::{anyhow, bail, Context, Result};
use bstr::BString;
use git2::{BlameOptions, Repository, Tree};
use std::{path::Path, process::Stdio, str};
use tracing::instrument;
@ -8,7 +9,7 @@ use crate::{
error::Code,
};
use super::Refname;
use super::{CommitBuffer, CommitHeadersV2, Refname};
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
@ -26,6 +27,13 @@ pub trait RepositoryExt {
fn in_memory<T, F>(&self, f: F) -> Result<T>
where
F: FnOnce(&git2::Repository) -> Result<T>;
/// Fetches the integration commit from the gitbutler/integration branch
fn integration_commit(&self) -> Result<git2::Commit<'_>>;
/// Fetches the target commit by finding the parent of the integration commit
fn target_commit(&self) -> Result<git2::Commit<'_>>;
/// Takes a CommitBuffer and returns it after being signed by by your git signing configuration
fn sign_buffer(&self, buffer: &CommitBuffer) -> Result<BString>;
fn checkout_index_builder<'a>(&'a self, index: &'a mut git2::Index) -> CheckoutIndexBuilder;
fn checkout_index_path_builder<P: AsRef<Path>>(&self, path: P) -> Result<()>;
fn checkout_tree_builder<'a>(&'a self, tree: &'a git2::Tree<'a>) -> CheckoutTreeBuidler;
@ -40,8 +48,6 @@ pub trait RepositoryExt {
/// This is for safety to assure the repository actually is in 'gitbutler mode'.
fn integration_ref_from_head(&self) -> Result<git2::Reference<'_>>;
fn target_commit(&self) -> Result<git2::Commit<'_>>;
#[allow(clippy::too_many_arguments)]
fn commit_with_signature(
&self,
@ -51,7 +57,7 @@ pub trait RepositoryExt {
message: &str,
tree: &git2::Tree<'_>,
parents: &[&git2::Commit<'_>],
change_id: Option<&str>,
commit_headers: Option<CommitHeadersV2>,
) -> Result<git2::Oid>;
fn blame(
@ -62,8 +68,6 @@ pub trait RepositoryExt {
oldest_commit: git2::Oid,
newest_commit: git2::Oid,
) -> Result<git2::Blame, git2::Error>;
fn sign_buffer(&self, buffer: &str) -> Result<String>;
}
impl RepositoryExt for Repository {
@ -141,10 +145,13 @@ impl RepositoryExt for Repository {
}
}
fn target_commit(&self) -> Result<git2::Commit<'_>> {
fn integration_commit(&self) -> Result<git2::Commit<'_>> {
let integration_ref = self.integration_ref_from_head()?;
let integration_commit = integration_ref.peel_to_commit()?;
Ok(integration_commit.parent(0)?)
Ok(integration_ref.peel_to_commit()?)
}
fn target_commit(&self) -> Result<git2::Commit<'_>> {
Ok(self.integration_commit()?.parent(0)?)
}
#[allow(clippy::too_many_arguments)]
@ -156,17 +163,34 @@ impl RepositoryExt for Repository {
message: &str,
tree: &git2::Tree<'_>,
parents: &[&git2::Commit<'_>],
change_id: Option<&str>,
commit_headers: Option<CommitHeadersV2>,
) -> Result<git2::Oid> {
let commit_buffer = self.commit_create_buffer(author, committer, message, tree, parents)?;
fn commit_buffer(
repository: &git2::Repository,
commit_buffer: &CommitBuffer,
) -> Result<git2::Oid> {
let oid = repository
.odb()?
.write(git2::ObjectType::Commit, &commit_buffer.as_bstring())?;
let commit_buffer = inject_change_id(&commit_buffer, change_id)?;
Ok(oid)
}
let mut buffer: CommitBuffer = self
.commit_create_buffer(author, committer, message, tree, parents)?
.into();
buffer.set_gitbutler_headers(commit_headers);
let oid = if self.gb_config()?.sign_commits.unwrap_or(false) {
let signature = sign_buffer(self, &commit_buffer);
let signature = self.sign_buffer(&buffer);
match signature {
Ok(signature) => self
.commit_signed(&commit_buffer, &signature, None)
.commit_signed(
buffer.as_bstring().to_string().as_str(),
signature.to_string().as_str(),
None,
)
.map_err(Into::into),
Err(e) => {
// If signing fails, set the "gitbutler.signCommits" config to false before erroring out
@ -178,9 +202,7 @@ impl RepositoryExt for Repository {
}
}
} else {
self.odb()?
.write(git2::ObjectType::Commit, commit_buffer.as_bytes())
.map_err(Into::into)
commit_buffer(self, &buffer)
}?;
// update reference
if let Some(refname) = update_ref {
@ -206,140 +228,136 @@ impl RepositoryExt for Repository {
self.blame_file(path, Some(&mut opts))
}
fn sign_buffer(&self, buffer: &str) -> Result<String> {
sign_buffer(self, &buffer.to_string())
fn sign_buffer(&self, buffer: &CommitBuffer) -> Result<BString> {
// check git config for gpg.signingkey
// TODO: support gpg.ssh.defaultKeyCommand to get the signing key if this value doesn't exist
let signing_key = self.config()?.get_string("user.signingkey");
if let Ok(signing_key) = signing_key {
let sign_format = self.config()?.get_string("gpg.format");
let is_ssh = if let Ok(sign_format) = sign_format {
sign_format == "ssh"
} else {
false
};
if is_ssh {
// write commit data to a temp file so we can sign it
let mut signature_storage = tempfile::NamedTempFile::new()?;
signature_storage.write_all(&buffer.as_bstring())?;
let buffer_file_to_sign_path = signature_storage.into_temp_path();
let gpg_program = self.config()?.get_string("gpg.ssh.program");
let mut gpg_program = gpg_program.unwrap_or("ssh-keygen".to_string());
// if cmd is "", use gpg
if gpg_program.is_empty() {
gpg_program = "ssh-keygen".to_string();
}
let mut cmd = std::process::Command::new(gpg_program);
cmd.args(["-Y", "sign", "-n", "git", "-f"]);
#[cfg(windows)]
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
let output;
// support literal ssh key
if let (true, signing_key) = is_literal_ssh_key(&signing_key) {
// write the key to a temp file
let mut key_storage = tempfile::NamedTempFile::new()?;
key_storage.write_all(signing_key.as_bytes())?;
// if on unix
#[cfg(unix)]
{
// make sure the tempfile permissions are acceptable for a private ssh key
let mut permissions = key_storage.as_file().metadata()?.permissions();
permissions.set_mode(0o600);
key_storage.as_file().set_permissions(permissions)?;
}
let key_file_path = key_storage.into_temp_path();
cmd.arg(&key_file_path);
cmd.arg("-U");
cmd.arg(&buffer_file_to_sign_path);
cmd.stderr(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stdin(Stdio::null());
let child = cmd.spawn()?;
output = child.wait_with_output()?;
} else {
cmd.arg(signing_key);
cmd.arg(&buffer_file_to_sign_path);
cmd.stderr(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stdin(Stdio::null());
let child = cmd.spawn()?;
output = child.wait_with_output()?;
}
if output.status.success() {
// read signed_storage path plus .sig
let signature_path = buffer_file_to_sign_path.with_extension("sig");
let sig_data = std::fs::read(signature_path)?;
let signature = BString::new(sig_data);
return Ok(signature);
} else {
let stderr = BString::new(output.stderr);
let stdout = BString::new(output.stdout);
let std_both = format!("{} {}", stdout, stderr);
bail!("Failed to sign SSH: {}", std_both);
}
} else {
// is gpg
let gpg_program = self.config()?.get_string("gpg.program");
let mut gpg_program = gpg_program.unwrap_or("gpg".to_string());
// if cmd is "", use gpg
if gpg_program.is_empty() {
gpg_program = "gpg".to_string();
}
let mut cmd = std::process::Command::new(gpg_program);
cmd.args(["--status-fd=2", "-bsau", &signing_key])
//.arg(&signed_storage)
.arg("-")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::piped());
#[cfg(windows)]
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
let mut child = cmd
.spawn()
.context(anyhow::format_err!("failed to spawn {:?}", cmd))?;
child
.stdin
.take()
.expect("configured")
.write_all(&buffer.as_bstring())?;
let output = child.wait_with_output()?;
if output.status.success() {
// read stdout
let signature = BString::new(output.stdout);
return Ok(signature);
} else {
let stderr = BString::new(output.stderr);
let stdout = BString::new(output.stdout);
let std_both = format!("{} {}", stdout, stderr);
bail!("Failed to sign GPG: {}", std_both);
}
}
}
Err(anyhow::anyhow!("No signing key found"))
}
}
/// Signs the buffer with the configured gpg key, returning the signature.
pub fn sign_buffer(repo: &git2::Repository, buffer: &String) -> Result<String> {
// check git config for gpg.signingkey
// TODO: support gpg.ssh.defaultKeyCommand to get the signing key if this value doesn't exist
let signing_key = repo.config()?.get_string("user.signingkey");
if let Ok(signing_key) = signing_key {
let sign_format = repo.config()?.get_string("gpg.format");
let is_ssh = if let Ok(sign_format) = sign_format {
sign_format == "ssh"
} else {
false
};
if is_ssh {
// write commit data to a temp file so we can sign it
let mut signature_storage = tempfile::NamedTempFile::new()?;
signature_storage.write_all(buffer.as_ref())?;
let buffer_file_to_sign_path = signature_storage.into_temp_path();
let gpg_program = repo.config()?.get_string("gpg.ssh.program");
let mut gpg_program = gpg_program.unwrap_or("ssh-keygen".to_string());
// if cmd is "", use gpg
if gpg_program.is_empty() {
gpg_program = "ssh-keygen".to_string();
}
let mut cmd = std::process::Command::new(gpg_program);
cmd.args(["-Y", "sign", "-n", "git", "-f"]);
#[cfg(windows)]
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
let output;
// support literal ssh key
if let (true, signing_key) = is_literal_ssh_key(&signing_key) {
// write the key to a temp file
let mut key_storage = tempfile::NamedTempFile::new()?;
key_storage.write_all(signing_key.as_bytes())?;
// if on unix
#[cfg(unix)]
{
// make sure the tempfile permissions are acceptable for a private ssh key
let mut permissions = key_storage.as_file().metadata()?.permissions();
permissions.set_mode(0o600);
key_storage.as_file().set_permissions(permissions)?;
}
let key_file_path = key_storage.into_temp_path();
cmd.arg(&key_file_path);
cmd.arg("-U");
cmd.arg(&buffer_file_to_sign_path);
cmd.stderr(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stdin(Stdio::null());
let child = cmd.spawn()?;
output = child.wait_with_output()?;
} else {
cmd.arg(signing_key);
cmd.arg(&buffer_file_to_sign_path);
cmd.stderr(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stdin(Stdio::null());
let child = cmd.spawn()?;
output = child.wait_with_output()?;
}
if output.status.success() {
// read signed_storage path plus .sig
let signature_path = buffer_file_to_sign_path.with_extension("sig");
let sig_data = std::fs::read(signature_path)?;
let signature = String::from_utf8_lossy(&sig_data).into_owned();
return Ok(signature);
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let std_both = format!("{} {}", stdout, stderr);
bail!("Failed to sign SSH: {}", std_both);
}
} else {
// is gpg
let gpg_program = repo.config()?.get_string("gpg.program");
let mut gpg_program = gpg_program.unwrap_or("gpg".to_string());
// if cmd is "", use gpg
if gpg_program.is_empty() {
gpg_program = "gpg".to_string();
}
let mut cmd = std::process::Command::new(gpg_program);
cmd.args(["--status-fd=2", "-bsau", &signing_key])
//.arg(&signed_storage)
.arg("-")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::piped());
#[cfg(windows)]
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
let mut child = cmd
.spawn()
.context(anyhow::format_err!("failed to spawn {:?}", cmd))?;
child
.stdin
.take()
.expect("configured")
.write_all(buffer.to_string().as_ref())?;
let output = child.wait_with_output()?;
if output.status.success() {
// read stdout
let signature = String::from_utf8_lossy(&output.stdout).into_owned();
return Ok(signature);
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let std_both = format!("{} {}", stdout, stderr);
bail!("Failed to sign GPG: {}", std_both);
}
}
}
Err(anyhow::anyhow!("No signing key found"))
}
fn is_literal_ssh_key(string: &str) -> (bool, &str) {
pub fn is_literal_ssh_key(string: &str) -> (bool, &str) {
if let Some(key) = string.strip_prefix("key::") {
return (true, key);
}
@ -349,34 +367,6 @@ fn is_literal_ssh_key(string: &str) -> (bool, &str) {
(false, string)
}
// in commit_buffer, inject a line right before the first `\n\n` that we see:
// `change-id: <id>`
fn inject_change_id(commit_buffer: &[u8], change_id: Option<&str>) -> Result<String> {
// if no change id, generate one
let change_id = change_id
.map(|id| id.to_string())
.unwrap_or_else(|| format!("{}", uuid::Uuid::new_v4()));
let commit_ends_in_newline = commit_buffer.ends_with(b"\n");
let commit_buffer = str::from_utf8(commit_buffer)?;
let lines = commit_buffer.lines();
let mut new_buffer = String::new();
let mut found = false;
for line in lines {
if line.is_empty() && !found {
new_buffer.push_str(&format!("change-id {}\n", change_id));
found = true;
}
new_buffer.push_str(line);
new_buffer.push('\n');
}
if !commit_ends_in_newline {
// strip last \n
new_buffer.pop();
}
Ok(new_buffer)
}
pub struct CheckoutTreeBuidler<'a> {
repo: &'a git2::Repository,
tree: &'a git2::Tree<'a>,

View File

@ -11,23 +11,12 @@ use super::entry::Trailer;
/// Snapshot functionality
impl Project {
pub(crate) fn snapshot_branch_applied(
&self,
snapshot_tree: git2::Oid,
result: Result<&String, &anyhow::Error>,
) -> anyhow::Result<()> {
let result = result.map(|o| Some(o.clone()));
let details = SnapshotDetails::new(OperationKind::ApplyBranch)
.with_trailers(result_trailer(result, "name".to_string()));
self.commit_snapshot(snapshot_tree, details)?;
Ok(())
}
pub(crate) fn snapshot_branch_unapplied(
&self,
snapshot_tree: git2::Oid,
result: Result<&Option<Branch>, &anyhow::Error>,
result: Result<&git2::Branch, &anyhow::Error>,
) -> anyhow::Result<()> {
let result = result.map(|o| o.clone().map(|b| b.name));
let result = result.map(|o| o.name().ok().flatten().map(|s| s.to_string()));
let details = SnapshotDetails::new(OperationKind::UnapplyBranch)
.with_trailers(result_trailer(result, "name".to_string()));
self.commit_snapshot(snapshot_tree, details)?;

View File

@ -7,7 +7,6 @@ use std::{
use anyhow::{anyhow, Context, Result};
use super::conflicts;
use crate::error::Code;
use crate::{
askpass,
git::{self, Url},
@ -15,6 +14,7 @@ use crate::{
ssh, users,
virtual_branches::{Branch, BranchId},
};
use crate::{error::Code, git::CommitHeadersV2};
use crate::{error::Marker, git::RepositoryExt};
pub struct Repository {
@ -339,12 +339,20 @@ impl Repository {
message: &str,
tree: &git2::Tree,
parents: &[&git2::Commit],
change_id: Option<&str>,
commit_headers: Option<CommitHeadersV2>,
) -> Result<git2::Oid> {
let (author, committer) =
super::signatures::signatures(self, user).context("failed to get signatures")?;
self.repo()
.commit_with_signature(None, &author, &committer, message, tree, parents, change_id)
.commit_with_signature(
None,
&author,
&committer,
message,
tree,
parents,
commit_headers,
)
.context("failed to commit")
}

View File

@ -5,6 +5,7 @@ use std::{
use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use bstr::BString;
use super::{storage, storage::UpdateRequest, Project, ProjectId};
use crate::git::RepositoryExt;
@ -231,7 +232,7 @@ impl Controller {
let project = self.projects_storage.get(id)?;
let repo = project_repository::Repository::open(&project)?;
let signed = repo.repo().sign_buffer("test");
let signed = repo.repo().sign_buffer(&BString::new("test".into()).into());
match signed {
Ok(_) => Ok(true),
Err(e) => Err(e),

View File

@ -1,3 +1,4 @@
use crate::git::HasCommitHeaders;
use crate::{error::Marker, git::CommitExt, git::RepositoryExt, project_repository};
use anyhow::{anyhow, Context, Result};
use bstr::ByteSlice;
@ -72,7 +73,7 @@ pub fn cherry_rebase_group(
.find_tree(merge_tree_oid)
.context("failed to find merge tree")?;
let change_id = to_rebase.change_id();
let commit_headers = to_rebase.gitbutler_headers();
let commit_oid = project_repository
.repo()
@ -83,7 +84,7 @@ pub fn cherry_rebase_group(
&to_rebase.message_bstr().to_str_lossy(),
&merge_tree,
&[&head],
change_id.as_deref(),
commit_headers,
)
.context("failed to create commit")?;

View File

@ -8,3 +8,6 @@ pub mod default_true;
pub struct Sensitive<T>(pub T);
mod sensitive;
mod tagged_string;
pub use tagged_string::*;

View File

@ -0,0 +1,60 @@
use std::{fmt, marker::PhantomData, ops::Deref};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// Tagged string is designed to clarify the purpose of strings when used as a return type
pub struct TaggedString<T>(String, PhantomData<T>);
impl<T> From<String> for TaggedString<T> {
fn from(value: String) -> Self {
TaggedString(value, PhantomData)
}
}
impl<T> From<&str> for TaggedString<T> {
fn from(value: &str) -> Self {
TaggedString(value.to_string(), PhantomData)
}
}
impl<T> Deref for TaggedString<T> {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'de, T> Deserialize<'de> for TaggedString<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).map(Into::into)
}
}
impl<T> Serialize for TaggedString<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
impl<T> fmt::Display for TaggedString<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl<T> fmt::Debug for TaggedString<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
pub struct _ReferenceName;
/// The name of a reference ie. `refs/heads/master`
pub type ReferenceName = TaggedString<_ReferenceName>;

View File

@ -5,7 +5,7 @@ use git2::Index;
use serde::Serialize;
use super::{
branch,
branch, convert_to_real_branch,
integration::{
get_workspace_head, update_gitbutler_integration, GITBUTLER_INTEGRATION_REFERENCE,
},
@ -327,10 +327,10 @@ fn _print_tree(repo: &git2::Repository, tree: &git2::Tree) -> Result<()> {
// determine if what the target branch is now pointing to is mergeable with our current working directory
// merge the target branch into our current working directory
// update the target sha
pub fn update_base_branch(
project_repository: &project_repository::Repository,
pub fn update_base_branch<'repo>(
project_repository: &'repo project_repository::Repository,
user: Option<&users::User>,
) -> anyhow::Result<Vec<branch::Branch>> {
) -> anyhow::Result<Vec<git2::Branch<'repo>>> {
project_repository.assure_resolved()?;
// look up the target and see if there is a new oid
@ -346,10 +346,10 @@ pub fn update_base_branch(
.peel_to_commit()
.context(format!("failed to peel branch {} to commit", target.branch))?;
let mut unapplied_branches: Vec<branch::Branch> = Vec::new();
let mut unapplied_branch_names: Vec<git2::Branch> = Vec::new();
if new_target_commit.id() == target.sha {
return Ok(unapplied_branches);
return Ok(unapplied_branch_names);
}
let new_target_tree = new_target_commit
@ -423,12 +423,15 @@ 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.
if branch.applied {
unapplied_branches.push(branch.clone());
}
branch.applied = false;
vb_state.set_branch(branch.clone())?;
return Ok(Some(branch));
let unapplied_real_branch = convert_to_real_branch(
project_repository,
branch.id,
Default::default(),
)?;
unapplied_branch_names.push(unapplied_real_branch);
return Ok(None);
}
let branch_merge_index_tree_oid =
@ -456,12 +459,14 @@ 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.
if branch.applied {
unapplied_branches.push(branch.clone());
}
branch.applied = false;
vb_state.set_branch(branch.clone())?;
return Ok(Some(branch));
let unapplied_real_branch = convert_to_real_branch(
project_repository,
branch.id,
Default::default(),
)?;
unapplied_branch_names.push(unapplied_real_branch);
return Ok(None);
}
// branch commits do not conflict with new target, so lets merge them
@ -567,7 +572,7 @@ pub fn update_base_branch(
// Rewriting the integration commit is necessary after changing target sha.
super::integration::update_gitbutler_integration(&vb_state, project_repository)?;
Ok(unapplied_branches)
Ok(unapplied_branch_names)
}
pub fn target_to_base_branch(

View File

@ -1,4 +1,8 @@
use crate::ops::entry::{OperationKind, SnapshotDetails};
use crate::{
git::BranchExt,
ops::entry::{OperationKind, SnapshotDetails},
types::ReferenceName,
};
use anyhow::Result;
use std::{collections::HashMap, path::Path, sync::Arc};
@ -7,7 +11,8 @@ use tokio::{sync::Semaphore, task::JoinHandle};
use super::{
branch::{BranchId, BranchOwnershipClaims},
target, target_to_base_branch, BaseBranch, Branch, RemoteBranchFile, VirtualBranchesHandle,
target, target_to_base_branch, BaseBranch, NameConflitResolution, RemoteBranchFile,
VirtualBranchesHandle,
};
use crate::{
git, project_repository,
@ -72,16 +77,6 @@ impl Controller {
.can_apply_remote_branch(project_id, branch_name)
}
pub async fn can_apply_virtual_branch(
&self,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<bool> {
self.inner(project_id)
.await
.can_apply_virtual_branch(project_id, branch_id)
}
pub async fn list_virtual_branches(
&self,
project_id: ProjectId,
@ -161,7 +156,7 @@ impl Controller {
.await
}
pub async fn update_base_branch(&self, project_id: ProjectId) -> Result<Vec<Branch>> {
pub async fn update_base_branch(&self, project_id: ProjectId) -> Result<Vec<ReferenceName>> {
self.inner(project_id)
.await
.update_base_branch(project_id)
@ -189,17 +184,6 @@ impl Controller {
.await
}
pub async fn apply_virtual_branch(
&self,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<()> {
self.inner(project_id)
.await
.apply_virtual_branch(project_id, branch_id)
.await
}
pub async fn unapply_ownership(
&self,
project_id: ProjectId,
@ -301,14 +285,15 @@ impl Controller {
.await
}
pub async fn unapply_virtual_branch(
pub async fn convert_to_real_branch(
&self,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<()> {
name_conflict_resolution: NameConflitResolution,
) -> Result<ReferenceName> {
self.inner(project_id)
.await
.unapply_virtual_branch(project_id, branch_id)
.convert_to_real_branch(project_id, branch_id, name_conflict_resolution)
.await
}
@ -325,18 +310,6 @@ impl Controller {
.await
}
pub async fn cherry_pick(
&self,
project_id: ProjectId,
branch_id: BranchId,
commit_oid: git2::Oid,
) -> Result<Option<git2::Oid>> {
self.inner(project_id)
.await
.cherry_pick(project_id, branch_id, commit_oid)
.await
}
pub async fn list_remote_branches(
&self,
project_id: ProjectId,
@ -471,16 +444,6 @@ impl ControllerInner {
super::is_remote_branch_mergeable(&project_repository, branch_name).map_err(Into::into)
}
pub fn can_apply_virtual_branch(
&self,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<bool> {
let project = self.projects.get(project_id)?;
let project_repository = project_repository::Repository::open(&project)?;
super::is_virtual_branch_mergeable(&project_repository, branch_id).map_err(Into::into)
}
pub async fn list_virtual_branches(
&self,
project_id: ProjectId,
@ -569,14 +532,21 @@ impl ControllerInner {
})
}
pub async fn update_base_branch(&self, project_id: ProjectId) -> Result<Vec<Branch>> {
pub async fn update_base_branch(&self, project_id: ProjectId) -> Result<Vec<ReferenceName>> {
let _permit = self.semaphore.acquire().await;
self.with_verify_branch(project_id, |project_repository, user| {
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::UpdateWorkspaceBase));
super::update_base_branch(project_repository, user).map_err(Into::into)
super::update_base_branch(project_repository, user)
.map(|unapplied_branches| {
unapplied_branches
.iter()
.filter_map(|unapplied_branch| unapplied_branch.reference_name().ok())
.collect()
})
.map_err(Into::into)
})
}
@ -620,27 +590,6 @@ impl ControllerInner {
})
}
pub async fn apply_virtual_branch(
&self,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<()> {
let _permit = self.semaphore.acquire().await;
self.with_verify_branch(project_id, |project_repository, user| {
let snapshot_tree = project_repository.project().prepare_snapshot();
let result =
super::apply_branch(project_repository, branch_id, user).map_err(Into::into);
let _ = snapshot_tree.and_then(|snapshot_tree| {
project_repository
.project()
.snapshot_branch_applied(snapshot_tree, result.as_ref())
});
result.map(|_| ())
})
}
pub async fn unapply_ownership(
&self,
project_id: ProjectId,
@ -785,22 +734,28 @@ impl ControllerInner {
})
}
pub async fn unapply_virtual_branch(
pub async fn convert_to_real_branch(
&self,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<()> {
name_conflict_resolution: NameConflitResolution,
) -> Result<ReferenceName> {
let _permit = self.semaphore.acquire().await;
self.with_verify_branch(project_id, |project_repository, _| {
let snapshot_tree = project_repository.project().prepare_snapshot();
let result = super::unapply_branch(project_repository, branch_id).map_err(Into::into);
let result = super::convert_to_real_branch(
project_repository,
branch_id,
name_conflict_resolution,
)
.map_err(Into::into);
let _ = snapshot_tree.and_then(|snapshot_tree| {
project_repository
.project()
.snapshot_branch_unapplied(snapshot_tree, result.as_ref())
});
result.map(|_| ())
result.and_then(|b| b.reference_name())
})
}
@ -819,22 +774,6 @@ impl ControllerInner {
.await?
}
pub async fn cherry_pick(
&self,
project_id: ProjectId,
branch_id: BranchId,
commit_oid: git2::Oid,
) -> Result<Option<git2::Oid>> {
let _permit = self.semaphore.acquire().await;
self.with_verify_branch(project_id, |project_repository, _| {
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::CherryPick));
super::cherry_pick(project_repository, branch_id, commit_oid).map_err(Into::into)
})
}
pub fn list_remote_branches(&self, project_id: ProjectId) -> Result<Vec<super::RemoteBranch>> {
let project = self.projects.get(project_id)?;
let project_repository = project_repository::Repository::open(&project)?;

View File

@ -22,7 +22,7 @@ const WORKSPACE_HEAD: &str = "Workspace Head";
pub const GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME: &str = "GitButler";
pub const GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL: &str = "gitbutler@gitbutler.com";
fn get_committer<'a>() -> Result<git2::Signature<'a>> {
pub fn get_integration_commiter<'a>() -> Result<git2::Signature<'a>> {
Ok(git2::Signature::now(
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL,
@ -85,7 +85,7 @@ pub fn get_workspace_head(
}
// TODO(mg): Can we make this a constant?
let committer = get_committer()?;
let committer = get_integration_commiter()?;
let mut heads: Vec<git2::Commit<'_>> = applied_branches
.iter()
@ -228,7 +228,7 @@ pub fn update_gitbutler_integration(
message.push_str("For more information about what we're doing here, check out our docs:\n");
message.push_str("https://docs.gitbutler.com/features/virtual-branches/integration-branch\n");
let committer = get_committer()?;
let committer = get_integration_commiter()?;
// It would be nice if we could pass an `update_ref` parameter to this function, but that
// requires committing to the tip of the branch, and we're mostly replacing the tip.

View File

@ -5,6 +5,7 @@ use std::{
use crate::{error::Code, fs::read_toml_file_or_default};
use anyhow::{anyhow, Result};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use super::{target::Target, Branch};
@ -126,6 +127,38 @@ impl VirtualBranchesHandle {
fn write_file(&self, virtual_branches: &VirtualBranches) -> Result<()> {
write(self.file_path.as_path(), virtual_branches)
}
pub fn update_ordering(&self) -> Result<()> {
let succeeded = self
.list_branches()?
.iter()
.sorted_by_key(|branch| branch.order)
.enumerate()
.all(|(index, branch)| {
let mut branch = branch.clone();
branch.order = index;
self.set_branch(branch).is_ok()
});
if succeeded {
Ok(())
} else {
Err(anyhow!("Failed to update virtual branches ordering"))
}
}
pub fn next_order_index(&self) -> Result<usize> {
self.update_ordering()?;
let order = self
.list_branches()?
.iter()
.sorted_by_key(|branch| branch.order)
.collect::<Vec<&Branch>>()
.last()
.map_or(0, |b| b.order + 1);
Ok(order)
}
}
fn write<P: AsRef<Path>>(file_path: P, virtual_branches: &VirtualBranches) -> Result<()> {

File diff suppressed because it is too large Load Diff

View File

@ -1,62 +1,5 @@
use super::*;
#[tokio::test]
async fn deltect_conflict() {
let Test {
repository,
project_id,
controller,
..
} = &Test::default();
controller
.set_base_branch(*project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch1_id = {
let branch1_id = controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "branch one").unwrap();
branch1_id
};
// unapply first vbranch
controller
.unapply_virtual_branch(*project_id, branch1_id)
.await
.unwrap();
{
// create another vbranch that conflicts with the first one
controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "branch two").unwrap();
}
{
// it should not be possible to apply the first branch
assert!(!controller
.can_apply_virtual_branch(*project_id, branch1_id)
.await
.unwrap());
assert!(matches!(
controller
.apply_virtual_branch(*project_id, branch1_id)
.await
.unwrap_err()
.downcast_ref(),
Some(Marker::ProjectConflict)
));
}
}
#[tokio::test]
async fn rebase_commit() {
let Test {
@ -82,7 +25,7 @@ async fn rebase_commit() {
.await
.unwrap();
let branch1_id = {
let mut branch1_id = {
// create a branch with some commited work
let branch1_id = controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
@ -105,10 +48,10 @@ async fn rebase_commit() {
branch1_id
};
{
let unapplied_branch = {
// unapply first vbranch
controller
.unapply_virtual_branch(*project_id, branch1_id)
let unapplied_branch = controller
.convert_to_real_branch(*project_id, branch1_id, Default::default())
.await
.unwrap();
@ -122,12 +65,10 @@ async fn rebase_commit() {
);
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].commits.len(), 1);
assert!(!branches[0].active);
}
assert_eq!(branches.len(), 0);
git::Refname::from_str(&unapplied_branch).unwrap()
};
{
// fetch remote
@ -135,12 +76,7 @@ async fn rebase_commit() {
// branch is stil unapplied
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].commits.len(), 1);
assert!(!branches[0].active);
assert!(!branches[0].conflicted);
assert_eq!(branches.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("another_file.txt")).unwrap(),
@ -154,8 +90,8 @@ async fn rebase_commit() {
{
// apply first vbranch again
controller
.apply_virtual_branch(*project_id, branch1_id)
branch1_id = controller
.create_virtual_branch_from_branch(*project_id, &unapplied_branch)
.await
.unwrap();
@ -203,7 +139,7 @@ async fn rebase_work() {
.await
.unwrap();
let branch1_id = {
let mut branch1_id = {
// make a branch with some work
let branch1_id = controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
@ -221,23 +157,21 @@ async fn rebase_work() {
branch1_id
};
{
let unapplied_branch = {
// unapply first vbranch
controller
.unapply_virtual_branch(*project_id, branch1_id)
let unapplied_branch = controller
.convert_to_real_branch(*project_id, branch1_id, Default::default())
.await
.unwrap();
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert!(!branches[0].active);
assert_eq!(branches.len(), 0);
assert!(!repository.path().join("another_file.txt").exists());
assert!(!repository.path().join("file.txt").exists());
}
git::Refname::from_str(&unapplied_branch).unwrap()
};
{
// fetch remote
@ -245,12 +179,7 @@ async fn rebase_work() {
// first branch is stil unapplied
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
assert!(!branches[0].active);
assert!(!branches[0].conflicted);
assert_eq!(branches.len(), 0);
assert!(!repository.path().join("another_file.txt").exists());
assert!(repository.path().join("file.txt").exists());
@ -258,8 +187,8 @@ async fn rebase_work() {
{
// apply first vbranch again
controller
.apply_virtual_branch(*project_id, branch1_id)
branch1_id = controller
.create_virtual_branch_from_branch(*project_id, &unapplied_branch)
.await
.unwrap();

View File

@ -1,386 +0,0 @@
use super::*;
mod cleanly {
use super::*;
#[tokio::test]
async fn applied() {
let Test {
repository,
project_id,
controller,
..
} = &Test::default();
controller
.set_base_branch(*project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one = {
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(*project_id, branch_id, "commit", None, false)
.await
.unwrap()
};
let commit_two = {
fs::write(repository.path().join("file.txt"), "content two").unwrap();
controller
.create_commit(*project_id, branch_id, "commit", None, false)
.await
.unwrap()
};
controller
.push_virtual_branch(*project_id, branch_id, false, None)
.await
.unwrap();
controller
.reset_virtual_branch(*project_id, branch_id, commit_one)
.await
.unwrap();
repository.reset_hard(None);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
let cherry_picked_commit_oid = controller
.cherry_pick(*project_id, branch_id, commit_two)
.await
.unwrap();
assert!(cherry_picked_commit_oid.is_some());
assert!(repository.path().join("file.txt").exists());
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content two"
);
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert_eq!(branches[0].commits.len(), 2);
assert_eq!(branches[0].commits[0].id, cherry_picked_commit_oid.unwrap());
assert_eq!(branches[0].commits[1].id, commit_one);
}
#[tokio::test]
async fn to_different_branch() {
let Test {
repository,
project_id,
controller,
..
} = &Test::default();
controller
.set_base_branch(*project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one = {
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(*project_id, branch_id, "commit", None, false)
.await
.unwrap()
};
let commit_two = {
fs::write(repository.path().join("file_two.txt"), "content two").unwrap();
controller
.create_commit(*project_id, branch_id, "commit", None, false)
.await
.unwrap()
};
controller
.push_virtual_branch(*project_id, branch_id, false, None)
.await
.unwrap();
controller
.reset_virtual_branch(*project_id, branch_id, commit_one)
.await
.unwrap();
repository.reset_hard(None);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
assert!(!repository.path().join("file_two.txt").exists());
let branch_two_id = controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let cherry_picked_commit_oid = controller
.cherry_pick(*project_id, branch_two_id, commit_two)
.await
.unwrap();
assert!(cherry_picked_commit_oid.is_some());
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert!(repository.path().join("file_two.txt").exists());
assert_eq!(
fs::read_to_string(repository.path().join("file_two.txt")).unwrap(),
"content two"
);
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].id, branch_id);
assert!(!branches[0].active);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, commit_one);
assert_eq!(branches[1].id, branch_two_id);
assert!(branches[1].active);
assert_eq!(branches[1].commits.len(), 1);
assert_eq!(branches[1].commits[0].id, cherry_picked_commit_oid.unwrap());
}
#[tokio::test]
async fn non_applied() {
let Test {
repository,
project_id,
controller,
..
} = &Test::default();
controller
.set_base_branch(*project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one_oid = {
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(*project_id, branch_id, "commit", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file_two.txt"), "content two").unwrap();
controller
.create_commit(*project_id, branch_id, "commit", None, false)
.await
.unwrap()
};
let commit_three_oid = {
fs::write(repository.path().join("file_three.txt"), "content three").unwrap();
controller
.create_commit(*project_id, branch_id, "commit", None, false)
.await
.unwrap()
};
controller
.reset_virtual_branch(*project_id, branch_id, commit_one_oid)
.await
.unwrap();
controller
.unapply_virtual_branch(*project_id, branch_id)
.await
.unwrap();
assert_eq!(
controller
.cherry_pick(*project_id, branch_id, commit_three_oid)
.await
.unwrap_err()
.to_string(),
"can not cherry pick a branch that is not applied"
);
}
}
mod with_conflicts {
use super::*;
#[tokio::test]
async fn applied() {
let Test {
repository,
project_id,
controller,
..
} = &Test::default();
controller
.set_base_branch(*project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let commit_one = {
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(*project_id, branch_id, "commit one", None, false)
.await
.unwrap()
};
{
fs::write(repository.path().join("file_two.txt"), "content two").unwrap();
controller
.create_commit(*project_id, branch_id, "commit two", None, false)
.await
.unwrap()
};
let commit_three = {
fs::write(repository.path().join("file_three.txt"), "content three").unwrap();
controller
.create_commit(*project_id, branch_id, "commit three", None, false)
.await
.unwrap()
};
controller
.push_virtual_branch(*project_id, branch_id, false, None)
.await
.unwrap();
controller
.reset_virtual_branch(*project_id, branch_id, commit_one)
.await
.unwrap();
repository.reset_hard(None);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
assert!(!repository.path().join("file_two.txt").exists());
assert!(!repository.path().join("file_three.txt").exists());
// introduce conflict with the remote commit
fs::write(repository.path().join("file_three.txt"), "conflict").unwrap();
{
// cherry picking leads to conflict
let cherry_picked_commit_oid = controller
.cherry_pick(*project_id, branch_id, commit_three)
.await
.unwrap();
assert!(cherry_picked_commit_oid.is_none());
assert_eq!(
fs::read_to_string(repository.path().join("file_three.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\ncontent three\n>>>>>>> theirs\n"
);
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert_eq!(branches[0].files.len(), 1);
assert!(branches[0].files[0].conflicted);
assert_eq!(branches[0].commits.len(), 1);
}
{
// conflict can be resolved
fs::write(repository.path().join("file_three.txt"), "resolved").unwrap();
let commited_oid = controller
.create_commit(*project_id, branch_id, "resolution", None, false)
.await
.unwrap();
let commit = repository.find_commit(commited_oid).unwrap();
assert_eq!(commit.parent_count(), 2);
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert!(branches[0].active);
assert!(branches[0].requires_force);
assert!(!branches[0].conflicted);
assert_eq!(branches[0].commits.len(), 2);
// resolution commit is there
assert_eq!(branches[0].commits[0].id, commited_oid);
assert_eq!(branches[0].commits[1].id, commit_one);
}
}
#[tokio::test]
async fn non_applied() {
let Test {
repository,
project_id,
controller,
..
} = &Test::default();
let commit_oid = {
let first = repository.commit_all("commit");
fs::write(repository.path().join("file.txt"), "content").unwrap();
let second = repository.commit_all("commit");
repository.push();
repository.reset_hard(Some(first));
second
};
controller
.set_base_branch(*project_id, &"refs/remotes/origin/master".parse().unwrap())
.await
.unwrap();
let branch_id = controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
// introduce conflict with the remote commit
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
controller
.unapply_virtual_branch(*project_id, branch_id)
.await
.unwrap();
assert_eq!(
controller
.cherry_pick(*project_id, branch_id, commit_oid)
.await
.unwrap_err()
.to_string(),
"can not cherry pick a branch that is not applied"
);
}
}

View File

@ -20,15 +20,14 @@ async fn unapply_with_data() {
assert_eq!(branches.len(), 1);
controller
.unapply_virtual_branch(*project_id, branches[0].id)
.convert_to_real_branch(*project_id, branches[0].id, Default::default())
.await
.unwrap();
assert!(!repository.path().join("file.txt").exists());
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert!(!branches[0].active);
assert_eq!(branches.len(), 0);
}
#[tokio::test]
@ -55,7 +54,7 @@ async fn conflicting() {
.await
.unwrap();
let branch_id = {
let unapplied_branch = {
// make a conflicting branch, and stash it
std::fs::write(repository.path().join("file.txt"), "conflict").unwrap();
@ -69,12 +68,12 @@ async fn conflicting() {
"@@ -1 +1 @@\n-first\n\\ No newline at end of file\n+conflict\n\\ No newline at end of file\n"
);
controller
.unapply_virtual_branch(*project_id, branches[0].id)
let unapplied_branch = controller
.convert_to_real_branch(*project_id, branches[0].id, Default::default())
.await
.unwrap();
branches[0].id
git::Refname::from_str(&unapplied_branch).unwrap()
};
{
@ -85,23 +84,12 @@ async fn conflicting() {
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"second"
);
let branch = controller
.list_virtual_branches(*project_id)
.await
.unwrap()
.0
.into_iter()
.find(|branch| branch.id == branch_id)
.unwrap();
assert!(!branch.base_current);
assert!(!branch.active);
}
{
let branch_id = {
// apply branch, it should conflict
controller
.apply_virtual_branch(*project_id, branch_id)
let branch_id = controller
.create_virtual_branch_from_branch(*project_id, &unapplied_branch)
.await
.unwrap();
@ -110,22 +98,24 @@ async fn conflicting() {
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
);
let branch = controller
.list_virtual_branches(*project_id)
.await
.unwrap()
.0
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert!(branch.base_current);
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
let branch = &branches[0];
// assert!(!branch.base_current);
assert!(branch.conflicted);
assert_eq!(branch.files[0].hunks[0].diff, "@@ -1 +1,5 @@\n-first\n\\ No newline at end of file\n+<<<<<<< ours\n+conflict\n+=======\n+second\n+>>>>>>> theirs\n");
}
assert_eq!(
branch.files[0].hunks[0].diff,
"@@ -1 +1,5 @@\n-first\n\\ No newline at end of file\n+<<<<<<< ours\n+conflict\n+=======\n+second\n+>>>>>>> theirs\n"
);
branch_id
};
{
// Converting the branch to a real branch should put us back in an unconflicted state
controller
.unapply_virtual_branch(*project_id, branch_id)
.convert_to_real_branch(*project_id, branch_id, Default::default())
.await
.unwrap();
@ -133,22 +123,6 @@ async fn conflicting() {
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"second"
);
let branch = controller
.list_virtual_branches(*project_id)
.await
.unwrap()
.0
.into_iter()
.find(|b| b.id == branch_id)
.unwrap();
assert!(!branch.active);
assert!(!branch.base_current);
assert!(!branch.conflicted);
assert_eq!(
branch.files[0].hunks[0].diff,
"@@ -1 +1 @@\n-first\n\\ No newline at end of file\n+conflict\n\\ No newline at end of file\n"
);
}
}
@ -174,7 +148,7 @@ async fn delete_if_empty() {
assert_eq!(branches.len(), 1);
controller
.unapply_virtual_branch(*project_id, branches[0].id)
.convert_to_real_branch(*project_id, branches[0].id, Default::default())
.await
.unwrap();

View File

@ -64,7 +64,7 @@ impl Test {
mod amend;
mod apply_virtual_branch;
mod cherry_pick;
mod convert_to_real_branch;
mod create_commit;
mod create_virtual_branch_from_branch;
mod delete_virtual_branch;
@ -80,7 +80,6 @@ mod reset_virtual_branch;
mod selected_for_changes;
mod set_base_branch;
mod squash;
mod unapply;
mod unapply_ownership;
mod undo_commit;
mod update_base_branch;
@ -114,7 +113,7 @@ async fn resolve_conflict_flow() {
.await
.unwrap();
let branch1_id = {
{
// make a branch that conflicts with the remote branch, but doesn't know about it yet
let branch1_id = controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
@ -126,31 +125,29 @@ async fn resolve_conflict_flow() {
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert!(branches[0].active);
branch1_id
};
{
// fetch remote
controller.update_base_branch(*project_id).await.unwrap();
let unapplied_branch = {
// fetch remote. There is now a conflict, so the branch will be unapplied
let unapplied_branches = controller.update_base_branch(*project_id).await.unwrap();
assert_eq!(unapplied_branches.len(), 1);
// there is a conflict now, so the branch should be inactive
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert!(!branches[0].active);
}
assert_eq!(branches.len(), 0);
{
git::Refname::from_str(&unapplied_branches[0]).unwrap()
};
let branch1_id = {
// when we apply conflicted branch, it has conflict
controller
.apply_virtual_branch(*project_id, branch1_id)
let branch1_id = controller
.create_virtual_branch_from_branch(*project_id, &unapplied_branch)
.await
.unwrap();
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert!(branches[0].active);
assert!(branches[0].conflicted);
assert_eq!(branches[0].files.len(), 2); // third.txt should be present during conflict
@ -160,7 +157,9 @@ async fn resolve_conflict_flow() {
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
);
}
branch1_id
};
{
// can't commit conflicts
@ -177,6 +176,7 @@ async fn resolve_conflict_flow() {
{
// fixing the conflict removes conflicted mark
fs::write(repository.path().join("file.txt"), "resolved").unwrap();
controller.list_virtual_branches(*project_id).await.unwrap();
let commit_oid = controller
.create_commit(*project_id, branch1_id, "resolution", None, false)
.await

View File

@ -194,7 +194,6 @@ async fn move_file_up_overlapping_hunks() {
.find(|b| b.id == branch_id)
.unwrap();
dbg!(&branch.commits);
assert_eq!(branch.commits.len(), 4);
//
}

View File

@ -38,19 +38,16 @@ async fn unapplying_selected_branch_selects_anther() {
assert!(!b2.selected_for_changes);
controller
.unapply_virtual_branch(*project_id, b_id)
.convert_to_real_branch(*project_id, b_id, Default::default())
.await
.unwrap();
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].id, b.id);
assert!(!branches[0].selected_for_changes);
assert!(!branches[0].active);
assert_eq!(branches[1].id, b2.id);
assert!(branches[1].selected_for_changes);
assert!(branches[1].active);
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, b2.id);
assert!(branches[0].selected_for_changes);
assert!(branches[0].active);
}
#[tokio::test]
@ -289,20 +286,33 @@ async fn unapply_virtual_branch_should_reset_selected_for_changes() {
.unwrap();
assert!(b1.selected_for_changes);
controller
.unapply_virtual_branch(*project_id, b1_id)
let b2_id = controller
.create_virtual_branch(*project_id, &branch::BranchCreateRequest::default())
.await
.unwrap();
let b1 = controller
let b2 = controller
.list_virtual_branches(*project_id)
.await
.unwrap()
.0
.into_iter()
.find(|b| b.id == b1_id)
.find(|b| b.id == b2_id)
.unwrap();
assert!(!b1.selected_for_changes);
assert!(!b2.selected_for_changes);
controller
.convert_to_real_branch(*project_id, b1_id, Default::default())
.await
.unwrap();
assert!(controller
.list_virtual_branches(*project_id)
.await
.unwrap()
.0
.into_iter()
.any(|b| b.selected_for_changes && b.id != b1_id))
}
#[tokio::test]
@ -359,12 +369,13 @@ async fn applying_first_branch() {
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
assert_eq!(branches.len(), 1);
controller
.unapply_virtual_branch(*project_id, branches[0].id)
let unapplied_branch = controller
.convert_to_real_branch(*project_id, branches[0].id, Default::default())
.await
.unwrap();
let unapplied_branch = git::Refname::from_str(&unapplied_branch).unwrap();
controller
.apply_virtual_branch(*project_id, branches[0].id)
.create_virtual_branch_from_branch(*project_id, &unapplied_branch)
.await
.unwrap();
@ -424,7 +435,6 @@ async fn new_locked_hunk_without_modifying_existing() {
repository.write_file("file.txt", &lines);
let (branches, _) = controller.list_virtual_branches(*project_id).await.unwrap();
dbg!(&branches);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[1].files.len(), 1);

View File

@ -15,14 +15,15 @@ use std::{
use anyhow::{Context, Result};
use git2::TreeEntry;
use gitbutler_core::{
git::{self, CommitExt, RepositoryExt},
git::{self, CommitExt, CommitHeadersV2, RepositoryExt},
virtual_branches::{
self, apply_branch,
self,
branch::{BranchCreateRequest, BranchOwnershipClaims, BranchUpdateRequest},
commit, create_virtual_branch, integrate_upstream_commits,
commit, create_virtual_branch, create_virtual_branch_from_branch,
integrate_upstream_commits,
integration::verify_branch,
is_remote_branch_mergeable, is_virtual_branch_mergeable, list_remote_branches,
unapply_ownership, update_branch,
is_remote_branch_mergeable, list_remote_branches, list_virtual_branches, unapply_ownership,
update_branch,
},
};
use pretty_assertions::assert_eq;
@ -726,7 +727,12 @@ fn commit_id_can_be_generated_or_specified() -> Result<()> {
.expect("failed to get head"),
)
.expect("failed to find commit")],
Some("my-change-id"),
// The change ID should always be generated by calling CommitHeadersV2::new
Some(CommitHeadersV2 {
change_id: "my-change-id".to_string(),
is_unapplied_header_commit: false,
vbranch_name: None,
}),
)
.expect("failed to commit");
@ -1077,7 +1083,11 @@ fn unapply_branch() -> Result<()> {
assert_eq!(branch.files.len(), 1);
assert!(branch.active);
virtual_branches::unapply_branch(project_repository, branch1_id)?;
let real_branch = virtual_branches::convert_to_real_branch(
project_repository,
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)?);
@ -1085,11 +1095,13 @@ fn unapply_branch() -> Result<()> {
assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
let (branches, _) = virtual_branches::list_virtual_branches(project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1);
assert!(!branch.active);
assert!(!branches.iter().any(|b| b.id == branch1_id));
apply_branch(project_repository, branch1_id, None)?;
let branch1_id = virtual_branches::create_virtual_branch_from_branch(
project_repository,
&git::Refname::try_from(&real_branch)?,
None,
)?;
let contents = std::fs::read(Path::new(&project.path).join(file_path))?;
assert_eq!(
"line1\nline2\nline3\nline4\nbranch1\n",
@ -1153,20 +1165,43 @@ fn apply_unapply_added_deleted_files() -> Result<()> {
},
)?;
virtual_branches::unapply_branch(project_repository, branch2_id)?;
list_virtual_branches(project_repository).unwrap();
let real_branch_2 = virtual_branches::convert_to_real_branch(
project_repository,
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)?);
virtual_branches::unapply_branch(project_repository, branch3_id)?;
let real_branch_3 = virtual_branches::convert_to_real_branch(
project_repository,
branch3_id,
Default::default(),
)?;
// check that file3 is gone
assert!(!Path::new(&project.path).join(file_path3).exists());
apply_branch(project_repository, branch2_id, None)?;
create_virtual_branch_from_branch(
project_repository,
&git::Refname::try_from(&real_branch_2).unwrap(),
None,
)
.unwrap();
// check that file2 is gone
assert!(!Path::new(&project.path).join(file_path2).exists());
apply_branch(project_repository, branch3_id, None)?;
create_virtual_branch_from_branch(
project_repository,
&git::Refname::try_from(&real_branch_3).unwrap(),
None,
)
.unwrap();
// check that file3 is back
let contents = std::fs::read(Path::new(&project.path).join(file_path3))?;
assert_eq!("file3\n", String::from_utf8(contents)?);
@ -1174,6 +1209,7 @@ fn apply_unapply_added_deleted_files() -> Result<()> {
Ok(())
}
// Verifies that we are able to detect when a remote branch is conflicting with the current applied branches.
#[test]
fn detect_mergeable_branch() -> Result<()> {
let suite = Suite::default();
@ -1218,8 +1254,8 @@ fn detect_mergeable_branch() -> Result<()> {
.expect("failed to update branch");
// unapply both branches and create some conflicting ones
virtual_branches::unapply_branch(project_repository, branch1_id)?;
virtual_branches::unapply_branch(project_repository, branch2_id)?;
virtual_branches::convert_to_real_branch(project_repository, branch1_id, Default::default())?;
virtual_branches::convert_to_real_branch(project_repository, branch2_id, Default::default())?;
project_repository.repo().set_head("refs/heads/master")?;
project_repository
@ -1293,17 +1329,6 @@ fn detect_mergeable_branch() -> Result<()> {
};
vb_state.set_branch(branch4.clone())?;
let (branches, _) = virtual_branches::list_virtual_branches(project_repository)?;
assert_eq!(branches.len(), 4);
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert!(!branch1.active);
assert!(!is_virtual_branch_mergeable(project_repository, branch1.id).unwrap());
let branch2 = &branches.iter().find(|b| b.id == branch2_id).unwrap();
assert!(!branch2.active);
assert!(is_virtual_branch_mergeable(project_repository, branch2.id).unwrap());
let remotes = list_remote_branches(project_repository).expect("failed to list remotes");
let _remote1 = &remotes
.iter()

View File

@ -185,17 +185,14 @@ fn main() {
virtual_branches::commands::integrate_upstream_commits,
virtual_branches::commands::update_virtual_branch,
virtual_branches::commands::delete_virtual_branch,
virtual_branches::commands::apply_branch,
virtual_branches::commands::unapply_branch,
virtual_branches::commands::convert_to_real_branch,
virtual_branches::commands::unapply_ownership,
virtual_branches::commands::reset_files,
virtual_branches::commands::push_virtual_branch,
virtual_branches::commands::create_virtual_branch_from_branch,
virtual_branches::commands::can_apply_virtual_branch,
virtual_branches::commands::can_apply_remote_branch,
virtual_branches::commands::list_remote_commit_files,
virtual_branches::commands::reset_virtual_branch,
virtual_branches::commands::cherry_pick_onto_virtual_branch,
virtual_branches::commands::amend_virtual_branch,
virtual_branches::commands::move_commit_file,
virtual_branches::commands::undo_commit,

View File

@ -4,12 +4,14 @@ pub mod commands {
use gitbutler_core::{
assets,
error::Code,
git, projects,
projects::ProjectId,
git,
projects::{self, ProjectId},
types::ReferenceName,
virtual_branches::{
branch::{self, BranchId, BranchOwnershipClaims},
controller::Controller,
BaseBranch, RemoteBranch, RemoteBranchData, RemoteBranchFile, VirtualBranches,
BaseBranch, NameConflitResolution, RemoteBranch, RemoteBranchData, RemoteBranchFile,
VirtualBranches,
},
};
use tauri::{AppHandle, Manager};
@ -153,7 +155,7 @@ pub mod commands {
pub async fn update_base_branch(
handle: AppHandle,
project_id: ProjectId,
) -> Result<Vec<branch::Branch>, Error> {
) -> Result<Vec<ReferenceName>, Error> {
let unapplied_branches = handle
.state::<Controller>()
.update_base_branch(project_id)
@ -195,29 +197,15 @@ pub mod commands {
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn apply_branch(
pub async fn convert_to_real_branch(
handle: AppHandle,
project_id: ProjectId,
branch: BranchId,
name_conflict_resolution: NameConflitResolution,
) -> Result<(), Error> {
handle
.state::<Controller>()
.apply_virtual_branch(project_id, branch)
.await?;
emit_vbranches(&handle, project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn unapply_branch(
handle: AppHandle,
project_id: ProjectId,
branch: BranchId,
) -> Result<(), Error> {
handle
.state::<Controller>()
.unapply_virtual_branch(project_id, branch)
.convert_to_real_branch(project_id, branch, name_conflict_resolution)
.await?;
emit_vbranches(&handle, project_id).await;
Ok(())
@ -275,20 +263,6 @@ pub mod commands {
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn can_apply_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<bool, Error> {
handle
.state::<Controller>()
.can_apply_virtual_branch(project_id, branch_id)
.await
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn can_apply_remote_branch(
@ -334,23 +308,6 @@ pub mod commands {
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn cherry_pick_onto_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
target_commit_oid: String,
) -> Result<Option<String>, Error> {
let target_commit_oid = git2::Oid::from_str(&target_commit_oid).map_err(|e| anyhow!(e))?;
let oid = handle
.state::<Controller>()
.cherry_pick(project_id, branch_id, target_commit_oid)
.await?;
emit_vbranches(&handle, project_id).await;
Ok(oid.map(|o| o.to_string()))
}
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub async fn amend_virtual_branch(