Skip to content

Commit 82c3987

Browse files
Unapply to real branches (#4025)
* Don't return optional * Rename get_integration_commiter * Add a header to wip commit * Stuff * Unapply all branches * Reorder code * Fix one test * Name resolution * Fix two tests * Fix another! * wip * Fix so many tests * Fix unapply.rs tests * Fix selected for changes tests * Move unapplying logic to delete_branch method * Remove unused and kinda borked cherry_commit code * Fix the tests!!!!! * Make apply_branch private * Change handling of headers * Improve order integrity * Updated types and comments to convey more meaning
1 parent 9aac60b commit 82c3987

32 files changed

+1350
-2512
lines changed

app/src/lib/branch/BranchLanePopupMenu.svelte

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
import { projectAiGenEnabled } from '$lib/config/config';
88
import Button from '$lib/shared/Button.svelte';
99
import Modal from '$lib/shared/Modal.svelte';
10+
import Select from '$lib/shared/Select.svelte';
11+
import SelectItem from '$lib/shared/SelectItem.svelte';
1012
import TextBox from '$lib/shared/TextBox.svelte';
1113
import Toggle from '$lib/shared/Toggle.svelte';
1214
import { User } from '$lib/stores/user';
1315
import { normalizeBranchName } from '$lib/utils/branch';
1416
import { getContext, getContextStore } from '$lib/utils/context';
1517
import { BranchController } from '$lib/vbranches/branchController';
16-
import { Branch } from '$lib/vbranches/types';
18+
import { Branch, type NameConflictResolution } from '$lib/vbranches/types';
1719
import { createEventDispatcher } from 'svelte';
1820
1921
export let visible: boolean;
@@ -51,8 +53,96 @@
5153
function close() {
5254
visible = false;
5355
}
56+
57+
let unapplyBranchModal: Modal;
58+
59+
type ResolutionVariants = NameConflictResolution['type'];
60+
61+
const resolutions: { value: ResolutionVariants; label: string }[] = [
62+
{
63+
value: 'overwrite',
64+
label: 'Overwrite the existing branch'
65+
},
66+
{
67+
value: 'suffix',
68+
label: 'Suffix the branch name'
69+
},
70+
{
71+
value: 'rename',
72+
label: 'Use a new name'
73+
}
74+
];
75+
76+
let selectedResolution: ResolutionVariants = resolutions[0].value;
77+
let newBranchName = '';
78+
79+
function unapplyBranchWithSelectedResolution() {
80+
let resolution: NameConflictResolution | undefined;
81+
if (selectedResolution === 'rename') {
82+
resolution = {
83+
type: selectedResolution,
84+
value: newBranchName
85+
};
86+
} else {
87+
resolution = {
88+
type: selectedResolution,
89+
value: undefined
90+
};
91+
}
92+
93+
branchController.convertToRealBranch(branch.id, resolution);
94+
95+
unapplyBranchModal.close();
96+
}
97+
98+
const remoteBranches = branchController.remoteBranchService.branches$;
99+
100+
function tryUnapplyBranch() {
101+
if ($remoteBranches.find((b) => b.name.endsWith(normalizeBranchName(branch.name)))) {
102+
unapplyBranchModal.show();
103+
} else {
104+
// No resolution required
105+
branchController.convertToRealBranch(branch.id);
106+
}
107+
}
54108
</script>
55109

110+
<Modal bind:this={unapplyBranchModal}>
111+
<div class="flow">
112+
<div class="modal-copy">
113+
<p class="text-base-15">There is already branch with the name</p>
114+
<Button size="tag" clickable={false}>{normalizeBranchName(branch.name)}</Button>
115+
<p class="text-base-15">.</p>
116+
<p class="text-base-15">Please choose how you want to resolve this:</p>
117+
</div>
118+
119+
<Select
120+
items={resolutions}
121+
itemId={'value'}
122+
labelId={'label'}
123+
bind:selectedItemId={selectedResolution}
124+
>
125+
<SelectItem slot="template" let:item let:selected {selected}>
126+
{item.label}
127+
</SelectItem>
128+
</Select>
129+
{#if selectedResolution === 'rename'}
130+
<TextBox
131+
label="New branch name"
132+
id="newBranchName"
133+
bind:value={newBranchName}
134+
placeholder="Enter new branch name"
135+
/>
136+
{/if}
137+
</div>
138+
{#snippet controls()}
139+
<Button style="ghost" outline on:click={() => unapplyBranchModal.close()}>Cancel</Button>
140+
<Button style="pop" kind="solid" grow on:click={unapplyBranchWithSelectedResolution}
141+
>Submit</Button
142+
>
143+
{/snippet}
144+
</Modal>
145+
56146
{#if visible}
57147
<ContextMenu>
58148
<ContextMenuSection>
@@ -71,7 +161,7 @@
71161
<ContextMenuItem
72162
label="Unapply"
73163
on:click={() => {
74-
if (branch.id) branchController.unapplyBranch(branch.id);
164+
tryUnapplyBranch();
75165
close();
76166
}}
77167
/>
@@ -186,3 +276,16 @@
186276
</Button>
187277
{/snippet}
188278
</Modal>
279+
280+
<style lang="postcss">
281+
.flow {
282+
display: flex;
283+
flex-direction: column;
284+
gap: 16px;
285+
}
286+
.modal-copy {
287+
& > * {
288+
display: inline;
289+
}
290+
}
291+
</style>

app/src/lib/vbranches/branchController.ts

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as toasts from '$lib/utils/toasts';
44
import posthog from 'posthog-js';
55
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
66
import type { BaseBranchService } from './baseBranch';
7-
import type { Branch, Hunk, LocalFile } from './types';
7+
import type { Branch, Hunk, LocalFile, NameConflictResolution } from './types';
88
import type { VirtualBranchService } from './virtualBranch';
99

1010
export class BranchController {
@@ -180,10 +180,17 @@ export class BranchController {
180180
}
181181
}
182182

183-
async unapplyBranch(branchId: string) {
183+
async convertToRealBranch(
184+
branchId: string,
185+
nameConflictResolution: NameConflictResolution = { type: 'suffix', value: undefined }
186+
) {
184187
try {
185-
// TODO: make this optimistic again.
186-
await invoke<void>('unapply_branch', { projectId: this.projectId, branch: branchId });
188+
await invoke<void>('convert_to_real_branch', {
189+
projectId: this.projectId,
190+
branch: branchId,
191+
nameConflictResolution
192+
});
193+
this.remoteBranchService.reload();
187194
} catch (err) {
188195
showError('Failed to unapply branch', err);
189196
}
@@ -284,25 +291,6 @@ You can find them in the 'Branches' sidebar in order to resolve conflicts.`;
284291
}
285292
}
286293

287-
async cherryPick(branchId: string, targetCommitOid: string) {
288-
try {
289-
await invoke<void>('cherry_pick_onto_virtual_branch', {
290-
projectId: this.projectId,
291-
branchId,
292-
targetCommitOid
293-
});
294-
} catch (err: any) {
295-
// TODO: Probably we wanna have error code checking in a more generic way
296-
if (err.code === 'errors.commit.signing_failed') {
297-
showSignError(err);
298-
} else {
299-
showError('Failed to cherry-pick commit', err);
300-
}
301-
} finally {
302-
this.targetBranchService.reload();
303-
}
304-
}
305-
306294
async markResolved(path: string) {
307295
try {
308296
await invoke<void>('mark_resolved', { projectId: this.projectId, path });

app/src/lib/vbranches/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,17 @@ export class BaseBranch {
461461
return this.repoBaseUrl.includes('gitlab.com');
462462
}
463463
}
464+
465+
export type NameConflictResolution =
466+
| {
467+
type: 'suffix';
468+
value: undefined;
469+
}
470+
| {
471+
type: 'overwrite';
472+
value: undefined;
473+
}
474+
| {
475+
type: 'rename';
476+
value: string;
477+
};

app/src/lib/vbranches/virtualBranch.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,8 @@ export class VirtualBranchService {
5858
tap((branches) => {
5959
branches.forEach((branch) => {
6060
branch.files.sort((a) => (a.conflicted ? -1 : 0));
61-
branch.isMergeable = invoke<boolean>('can_apply_virtual_branch', {
62-
projectId: projectId,
63-
branchId: branch.id
64-
});
61+
// This is always true now
62+
branch.isMergeable = Promise.resolve(true);
6563
});
6664
this.fresh$.next(); // Notification for fresh reload
6765
}),

crates/gitbutler-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition = "2021"
55
authors = ["GitButler <gitbutler@gitbutler.com>"]
66
publish = false
77

8+
89
[dev-dependencies]
910
once_cell = "1.19"
1011
pretty_assertions = "1.4"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use anyhow::{Context, Result};
2+
3+
use crate::types::ReferenceName;
4+
5+
pub trait BranchExt {
6+
fn reference_name(&self) -> Result<ReferenceName>;
7+
}
8+
9+
impl<'repo> BranchExt for git2::Branch<'repo> {
10+
fn reference_name(&self) -> Result<ReferenceName> {
11+
let name = self.get().name().context("Failed to get branch name")?;
12+
13+
Ok(name.into())
14+
}
15+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use bstr::{BStr, BString, ByteSlice, ByteVec};
2+
use core::str;
3+
4+
use super::CommitHeadersV2;
5+
6+
pub struct CommitBuffer {
7+
heading: Vec<(BString, BString)>,
8+
message: BString,
9+
}
10+
11+
impl CommitBuffer {
12+
pub fn new(buffer: &[u8]) -> Self {
13+
let buffer = BStr::new(buffer);
14+
if let Some((heading, message)) = buffer.split_once_str("\n\n") {
15+
let heading = heading
16+
.lines()
17+
.filter_map(|line| line.split_once_str(" "))
18+
.map(|(key, value)| (key.into(), value.into()))
19+
.collect();
20+
21+
Self {
22+
heading,
23+
message: message.into(),
24+
}
25+
} else {
26+
Self {
27+
heading: vec![],
28+
message: buffer.into(),
29+
}
30+
}
31+
}
32+
33+
pub fn set_header(&mut self, key: &str, value: &str) {
34+
let mut set_heading = false;
35+
self.heading.iter_mut().for_each(|(k, v)| {
36+
if k == key {
37+
*v = value.into();
38+
set_heading = true;
39+
}
40+
});
41+
42+
if !set_heading {
43+
self.heading.push((key.into(), value.into()));
44+
}
45+
}
46+
47+
/// Defers to the CommitHeadersV2 struct about which headers should be injected.
48+
/// If `commit_headers: None` is provided, a default set of headers, including a generated change-id will be used
49+
pub fn set_gitbutler_headers(&mut self, commit_headers: Option<CommitHeadersV2>) {
50+
if let Some(commit_headers) = commit_headers {
51+
commit_headers.inject_into(self)
52+
} else {
53+
CommitHeadersV2::inject_default(self)
54+
}
55+
}
56+
57+
pub fn as_bstring(&self) -> BString {
58+
let mut output = BString::new(vec![]);
59+
60+
for (key, value) in &self.heading {
61+
output.push_str(key);
62+
output.push_str(" ");
63+
output.push_str(value);
64+
output.push_str("\n");
65+
}
66+
67+
output.push_str("\n");
68+
69+
output.push_str(&self.message);
70+
71+
output
72+
}
73+
}
74+
75+
impl From<git2::Buf> for CommitBuffer {
76+
fn from(git2_buffer: git2::Buf) -> Self {
77+
Self::new(&git2_buffer)
78+
}
79+
}
80+
81+
impl From<BString> for CommitBuffer {
82+
fn from(s: BString) -> Self {
83+
Self::new(s.as_bytes())
84+
}
85+
}
86+
87+
impl From<CommitBuffer> for BString {
88+
fn from(buffer: CommitBuffer) -> BString {
89+
buffer.as_bstring()
90+
}
91+
}

crates/gitbutler-core/src/git/commit_ext.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
// use anyhow::Result;
21
use bstr::BStr;
32

3+
use super::HasCommitHeaders;
4+
45
/// Extension trait for `git2::Commit`.
56
///
67
/// For now, it collects useful methods from `gitbutler-core::git::Commit`
@@ -15,13 +16,9 @@ impl<'repo> CommitExt for git2::Commit<'repo> {
1516
fn message_bstr(&self) -> &BStr {
1617
self.message_bytes().as_ref()
1718
}
19+
1820
fn change_id(&self) -> Option<String> {
19-
let cid = self.header_field_bytes("change-id").ok()?;
20-
if cid.is_empty() {
21-
None
22-
} else {
23-
String::from_utf8(cid.to_owned()).ok()
24-
}
21+
self.gitbutler_headers().map(|headers| headers.change_id)
2522
}
2623
fn is_signed(&self) -> bool {
2724
self.header_field_bytes("gpgsig").is_ok()

0 commit comments

Comments
 (0)