mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-09-11 12:25:42 +03:00
Merge branch 'master' into ndom91/create-gitbutler-ui-package
This commit is contained in:
commit
4fab3ec200
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -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",
|
||||
|
@ -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()]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 });
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
}),
|
||||
|
@ -5,6 +5,7 @@ edition = "2021"
|
||||
authors = ["GitButler <gitbutler@gitbutler.com>"]
|
||||
publish = false
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
once_cell = "1.19"
|
||||
pretty_assertions = "1.4"
|
||||
|
15
crates/gitbutler-core/src/git/branch_ext.rs
Normal file
15
crates/gitbutler-core/src/git/branch_ext.rs
Normal 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())
|
||||
}
|
||||
}
|
91
crates/gitbutler-core/src/git/commit_buffer.rs
Normal file
91
crates/gitbutler-core/src/git/commit_buffer.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
132
crates/gitbutler-core/src/git/commit_headers.rs
Normal file
132
crates/gitbutler-core/src/git/commit_headers.rs
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
@ -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::*;
|
||||
|
@ -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>,
|
||||
|
@ -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)?;
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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")?;
|
||||
|
||||
|
@ -8,3 +8,6 @@ pub mod default_true;
|
||||
pub struct Sensitive<T>(pub T);
|
||||
|
||||
mod sensitive;
|
||||
|
||||
mod tagged_string;
|
||||
pub use tagged_string::*;
|
||||
|
60
crates/gitbutler-core/src/types/tagged_string.rs
Normal file
60
crates/gitbutler-core/src/types/tagged_string.rs
Normal 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>;
|
@ -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(
|
||||
|
@ -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)?;
|
||||
|
@ -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.
|
||||
|
@ -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
@ -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();
|
||||
|
||||
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
//
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user