Skip to content

Commit ebbb372

Browse files
committed
Name resolution
1 parent 6b08749 commit ebbb372

File tree

15 files changed

+317
-96
lines changed

15 files changed

+317
-96
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
/>
@@ -182,3 +272,16 @@
182272
</Button>
183273
{/snippet}
184274
</Modal>
275+
276+
<style lang="postcss">
277+
.flow {
278+
display: flex;
279+
flex-direction: column;
280+
gap: 16px;
281+
}
282+
.modal-copy {
283+
& > * {
284+
display: inline;
285+
}
286+
}
287+
</style>

app/src/lib/vbranches/branchController.ts

Lines changed: 10 additions & 4 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,16 @@ 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+
});
187193
this.remoteBranchService.reload();
188194
} catch (err) {
189195
showError('Failed to unapply branch', err);

app/src/lib/vbranches/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,3 +466,17 @@ export class BaseBranch {
466466
return this.repoBaseUrl.includes('gitlab.com');
467467
}
468468
}
469+
470+
export type NameConflictResolution =
471+
| {
472+
type: 'suffix';
473+
value: undefined;
474+
}
475+
| {
476+
type: 'overwrite';
477+
value: undefined;
478+
}
479+
| {
480+
type: 'rename';
481+
value: string;
482+
};
Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,70 @@
11
use anyhow::Result;
22
use core::str;
3-
use std::ops::Deref;
43

5-
pub struct CommitBuffer(String);
4+
pub struct CommitBuffer {
5+
heading: Vec<(String, String)>,
6+
message: String,
7+
}
68

79
impl CommitBuffer {
8-
pub fn new(git2_buffer: &git2::Buf) -> Result<Self> {
9-
let commit_ends_in_newline = git2_buffer.ends_with(b"\n");
10-
let git2_buffer = str::from_utf8(git2_buffer)?;
11-
let lines = git2_buffer.lines();
12-
13-
let mut new_buffer = String::new();
14-
15-
for line in lines {
16-
new_buffer.push_str(line);
17-
new_buffer.push('\n');
18-
}
19-
20-
if !commit_ends_in_newline {
21-
// strip last \n
22-
new_buffer.pop();
10+
pub fn new(buffer: &[u8]) -> Result<Self> {
11+
let buffer = str::from_utf8(buffer)?;
12+
13+
dbg!(&buffer);
14+
15+
if let Some((heading, message)) = buffer.split_once("\n\n") {
16+
let heading = heading
17+
.lines()
18+
.filter_map(|line| line.split_once(' '))
19+
.map(|(key, value)| (key.to_string(), value.to_string()))
20+
.collect();
21+
22+
Ok(Self {
23+
heading,
24+
message: message.to_string(),
25+
})
26+
} else {
27+
Ok(Self {
28+
heading: vec![],
29+
message: buffer.to_string(),
30+
})
2331
}
24-
25-
Ok(Self(new_buffer))
2632
}
2733

28-
pub fn inject_header(&mut self, key: &str, value: &str) {
29-
if let Some((heading, body)) = self.split_once("\n\n") {
30-
let mut output = String::new();
31-
32-
output.push_str(heading);
33-
output.push_str(format!("\n{} {}", key, value).as_str());
34-
output.push_str("\n\n");
35-
output.push_str(body);
36-
37-
self.0 = output;
34+
pub fn set_header(&mut self, key: &str, value: &str) {
35+
let mut set_heading = false;
36+
self.heading.iter_mut().for_each(|(k, v)| {
37+
if k == key {
38+
*v = value.to_string();
39+
set_heading = true;
40+
}
41+
});
42+
43+
if !set_heading {
44+
self.heading.push((key.to_string(), value.to_string()));
3845
}
3946
}
4047

41-
pub fn inject_change_id(&mut self, change_id: Option<&str>) {
48+
pub fn set_change_id(&mut self, change_id: Option<&str>) {
4249
let change_id = change_id
4350
.map(|id| id.to_string())
4451
.unwrap_or_else(|| format!("{}", uuid::Uuid::new_v4()));
4552

46-
self.inject_header("change-id", change_id.as_str());
53+
self.set_header("change-id", change_id.as_str());
54+
}
55+
56+
pub fn as_string(&self) -> String {
57+
let mut output = String::new();
58+
59+
for (key, value) in &self.heading {
60+
output.push_str(&format!("{} {}\n", key, value));
61+
}
62+
63+
output.push('\n');
64+
65+
output.push_str(&self.message);
66+
67+
output
4768
}
4869
}
4970

@@ -55,16 +76,16 @@ impl TryFrom<git2::Buf> for CommitBuffer {
5576
}
5677
}
5778

58-
impl From<String> for CommitBuffer {
59-
fn from(s: String) -> Self {
60-
Self(s)
79+
impl TryFrom<String> for CommitBuffer {
80+
type Error = anyhow::Error;
81+
82+
fn try_from(s: String) -> Result<Self> {
83+
Self::new(s.as_bytes())
6184
}
6285
}
6386

64-
impl Deref for CommitBuffer {
65-
type Target = str;
66-
67-
fn deref(&self) -> &Self::Target {
68-
&self.0
87+
impl From<CommitBuffer> for String {
88+
fn from(buffer: CommitBuffer) -> String {
89+
buffer.as_string()
6990
}
7091
}

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,13 @@ impl RepositoryExt for Repository {
167167
.commit_create_buffer(author, committer, message, tree, parents)?
168168
.try_into()?;
169169

170-
commit_buffer.inject_change_id(change_id);
170+
commit_buffer.set_change_id(change_id);
171171

172172
let oid = if self.gb_config()?.sign_commits.unwrap_or(false) {
173173
let signature = self.sign_buffer(&commit_buffer);
174174
match signature {
175175
Ok(signature) => self
176-
.commit_signed(&commit_buffer, &signature, None)
176+
.commit_signed(commit_buffer.as_string().as_ref(), &signature, None)
177177
.map_err(Into::into),
178178
Err(e) => {
179179
// If signing fails, set the "gitbutler.signCommits" config to false before erroring out
@@ -195,9 +195,10 @@ impl RepositoryExt for Repository {
195195
}
196196

197197
fn commit_buffer(&self, commit_buffer: &CommitBuffer) -> Result<git2::Oid> {
198-
let oid = self
199-
.odb()?
200-
.write(git2::ObjectType::Commit, commit_buffer.as_bytes())?;
198+
let oid = self.odb()?.write(
199+
git2::ObjectType::Commit,
200+
commit_buffer.as_string().as_bytes(),
201+
)?;
201202

202203
Ok(oid)
203204
}
@@ -234,7 +235,7 @@ impl RepositoryExt for Repository {
234235
if is_ssh {
235236
// write commit data to a temp file so we can sign it
236237
let mut signature_storage = tempfile::NamedTempFile::new()?;
237-
signature_storage.write_all(buffer.as_ref())?;
238+
signature_storage.write_all(buffer.as_string().as_ref())?;
238239
let buffer_file_to_sign_path = signature_storage.into_temp_path();
239240

240241
let gpg_program = self.config()?.get_string("gpg.ssh.program");
@@ -328,7 +329,7 @@ impl RepositoryExt for Repository {
328329
.stdin
329330
.take()
330331
.expect("configured")
331-
.write_all(buffer.to_string().as_ref())?;
332+
.write_all(buffer.as_string().as_ref())?;
332333

333334
let output = child.wait_with_output()?;
334335
if output.status.success() {

crates/gitbutler-core/src/projects/controller.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use anyhow::{bail, Context, Result};
77
use async_trait::async_trait;
88

99
use super::{storage, storage::UpdateRequest, Project, ProjectId};
10-
use crate::git::{RepositoryExt};
10+
use crate::git::RepositoryExt;
1111
use crate::projects::AuthKey;
1212
use crate::{error, project_repository};
1313

@@ -231,7 +231,9 @@ impl Controller {
231231
let project = self.projects_storage.get(id)?;
232232

233233
let repo = project_repository::Repository::open(&project)?;
234-
let signed = repo.repo().sign_buffer(&"test".to_string().into());
234+
let signed = repo
235+
.repo()
236+
.sign_buffer(&"test".to_string().try_into().unwrap());
235237
match signed {
236238
Ok(_) => Ok(true),
237239
Err(e) => Err(e),

0 commit comments

Comments
 (0)