Skip to content

Commit 8c6b180

Browse files
committed
Name resolution
1 parent 0609bff commit 8c6b180

File tree

16 files changed

+320
-97
lines changed

16 files changed

+320
-97
lines changed

app/src/lib/components/BranchLanePopupMenu.svelte

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import { Project } from '$lib/backend/projects';
44
import Button from '$lib/components/Button.svelte';
55
import Modal from '$lib/components/Modal.svelte';
6+
import Select from '$lib/components/Select.svelte';
7+
import SelectItem from '$lib/components/SelectItem.svelte';
68
import TextBox from '$lib/components/TextBox.svelte';
79
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
810
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
@@ -12,7 +14,7 @@
1214
import { normalizeBranchName } from '$lib/utils/branch';
1315
import { getContext, getContextStore } from '$lib/utils/context';
1416
import { BranchController } from '$lib/vbranches/branchController';
15-
import { Branch } from '$lib/vbranches/types';
17+
import { Branch, type NameConflictResolution } from '$lib/vbranches/types';
1618
import { createEventDispatcher } from 'svelte';
1719
1820
export let visible: boolean;
@@ -45,8 +47,96 @@
4547
function close() {
4648
visible = false;
4749
}
50+
51+
let unapplyBranchModal: Modal;
52+
53+
type ResolutionVariants = NameConflictResolution['type'];
54+
55+
const resolutions: { value: ResolutionVariants; label: string }[] = [
56+
{
57+
value: 'overwrite',
58+
label: 'Overwrite the existing branch'
59+
},
60+
{
61+
value: 'suffix',
62+
label: 'Suffix the branch name'
63+
},
64+
{
65+
value: 'rename',
66+
label: 'Use a new name'
67+
}
68+
];
69+
70+
let selectedResolution: ResolutionVariants = resolutions[0].value;
71+
let newBranchName = '';
72+
73+
function unapplyBranchWithSelectedResolution() {
74+
let resolution: NameConflictResolution | undefined;
75+
if (selectedResolution === 'rename') {
76+
resolution = {
77+
type: selectedResolution,
78+
value: newBranchName
79+
};
80+
} else {
81+
resolution = {
82+
type: selectedResolution,
83+
value: undefined
84+
};
85+
}
86+
87+
branchController.convertToRealBranch(branch.id, resolution);
88+
89+
unapplyBranchModal.close();
90+
}
91+
92+
const remoteBranches = branchController.remoteBranchService.branches$;
93+
94+
function tryUnapplyBranch() {
95+
if ($remoteBranches.find((b) => b.name.endsWith(normalizeBranchName(branch.name)))) {
96+
unapplyBranchModal.show();
97+
} else {
98+
// No resolution required
99+
branchController.convertToRealBranch(branch.id);
100+
}
101+
}
48102
</script>
49103

104+
<Modal bind:this={unapplyBranchModal}>
105+
<div class="flow">
106+
<div class="modal-copy">
107+
<p class="text-base-15">There is already branch with the name</p>
108+
<Button size="tag" clickable={false}>{normalizeBranchName(branch.name)}</Button>
109+
<p class="text-base-15">.</p>
110+
<p class="text-base-15">Please choose how you want to resolve this:</p>
111+
</div>
112+
113+
<Select
114+
items={resolutions}
115+
itemId={'value'}
116+
labelId={'label'}
117+
bind:selectedItemId={selectedResolution}
118+
>
119+
<SelectItem slot="template" let:item let:selected {selected}>
120+
{item.label}
121+
</SelectItem>
122+
</Select>
123+
{#if selectedResolution === 'rename'}
124+
<TextBox
125+
label="New branch name"
126+
id="newBranchName"
127+
bind:value={newBranchName}
128+
placeholder="Enter new branch name"
129+
/>
130+
{/if}
131+
</div>
132+
<svelte:fragment slot="controls">
133+
<Button style="ghost" outline on:click={() => unapplyBranchModal.close()}>Cancel</Button>
134+
<Button style="pop" kind="solid" grow on:click={unapplyBranchWithSelectedResolution}
135+
>Submit</Button
136+
>
137+
</svelte:fragment>
138+
</Modal>
139+
50140
{#if visible}
51141
<ContextMenu>
52142
<ContextMenuSection>
@@ -65,7 +155,7 @@
65155
<ContextMenuItem
66156
label="Unapply"
67157
on:click={() => {
68-
if (branch.id) branchController.unapplyBranch(branch.id);
158+
tryUnapplyBranch();
69159
close();
70160
}}
71161
/>
@@ -166,3 +256,16 @@
166256
</Button>
167257
</svelte:fragment>
168258
</Modal>
259+
260+
<style lang="postcss">
261+
.flow {
262+
display: flex;
263+
flex-direction: column;
264+
gap: 16px;
265+
}
266+
.modal-copy {
267+
& > * {
268+
display: inline;
269+
}
270+
}
271+
</style>

app/src/lib/components/Modal.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
background-color: var(--clr-bg-1);
7474
border: 1px solid var(--clr-border-2);
7575
box-shadow: var(--fx-shadow-l);
76+
overflow: visible;
7677
}
7778
7879
/* modifiers */
@@ -97,7 +98,7 @@
9798
}
9899
99100
.modal__body {
100-
overflow: auto;
101+
overflow: visible;
101102
padding: 16px;
102103
}
103104
@@ -109,5 +110,6 @@
109110
padding: 16px;
110111
border-top: 1px solid var(--clr-border-2);
111112
background-color: var(--clr-bg-1);
113+
border-radius: 0 0 var(--radius-l) var(--radius-l);
112114
}
113115
</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 {
@@ -169,10 +169,16 @@ export class BranchController {
169169
}
170170
}
171171

172-
async unapplyBranch(branchId: string) {
172+
async convertToRealBranch(
173+
branchId: string,
174+
nameConflictResolution: NameConflictResolution = { type: 'suffix', value: undefined }
175+
) {
173176
try {
174-
// TODO: make this optimistic again.
175-
await invoke<void>('unapply_branch', { projectId: this.projectId, branch: branchId });
177+
await invoke<void>('convert_to_real_branch', {
178+
projectId: this.projectId,
179+
branch: branchId,
180+
nameConflictResolution
181+
});
176182
this.remoteBranchService.reload();
177183
} catch (err) {
178184
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
@@ -464,3 +464,17 @@ export class BaseBranch {
464464
return this.repoBaseUrl.includes('gitlab.com');
465465
}
466466
}
467+
468+
export type NameConflictResolution =
469+
| {
470+
type: 'suffix';
471+
value: undefined;
472+
}
473+
| {
474+
type: 'overwrite';
475+
value: undefined;
476+
}
477+
| {
478+
type: 'rename';
479+
value: string;
480+
};
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
@@ -148,13 +148,13 @@ impl RepositoryExt for Repository {
148148
.commit_create_buffer(author, committer, message, tree, parents)?
149149
.try_into()?;
150150

151-
commit_buffer.inject_change_id(change_id);
151+
commit_buffer.set_change_id(change_id);
152152

153153
let oid = if self.gb_config()?.sign_commits.unwrap_or(false) {
154154
let signature = self.sign_buffer(&commit_buffer);
155155
match signature {
156156
Ok(signature) => self
157-
.commit_signed(&commit_buffer, &signature, None)
157+
.commit_signed(commit_buffer.as_string().as_ref(), &signature, None)
158158
.map_err(Into::into),
159159
Err(e) => {
160160
// If signing fails, set the "gitbutler.signCommits" config to false before erroring out
@@ -176,9 +176,10 @@ impl RepositoryExt for Repository {
176176
}
177177

178178
fn commit_buffer(&self, commit_buffer: &CommitBuffer) -> Result<git2::Oid> {
179-
let oid = self
180-
.odb()?
181-
.write(git2::ObjectType::Commit, commit_buffer.as_bytes())?;
179+
let oid = self.odb()?.write(
180+
git2::ObjectType::Commit,
181+
commit_buffer.as_string().as_bytes(),
182+
)?;
182183

183184
Ok(oid)
184185
}
@@ -215,7 +216,7 @@ impl RepositoryExt for Repository {
215216
if is_ssh {
216217
// write commit data to a temp file so we can sign it
217218
let mut signature_storage = tempfile::NamedTempFile::new()?;
218-
signature_storage.write_all(buffer.as_ref())?;
219+
signature_storage.write_all(buffer.as_string().as_ref())?;
219220
let buffer_file_to_sign_path = signature_storage.into_temp_path();
220221

221222
let gpg_program = self.config()?.get_string("gpg.ssh.program");
@@ -309,7 +310,7 @@ impl RepositoryExt for Repository {
309310
.stdin
310311
.take()
311312
.expect("configured")
312-
.write_all(buffer.to_string().as_ref())?;
313+
.write_all(buffer.as_string().as_ref())?;
313314

314315
let output = child.wait_with_output()?;
315316
if output.status.success() {

0 commit comments

Comments
 (0)