Skip to content

Commit 75805d3

Browse files
authored
Merge pull request #4718 from gitbutlerapp/branch-stacking-first-stab
branch stacking first stab
2 parents a9b12f4 + 06f50b4 commit 75805d3

File tree

23 files changed

+809
-35
lines changed

23 files changed

+809
-35
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src/lib/commit/CommitCard.svelte

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import { BaseBranch } from '$lib/baseBranch/baseBranch';
55
import CommitMessageInput from '$lib/commit/CommitMessageInput.svelte';
66
import { persistedCommitMessage } from '$lib/config/config';
7+
import { featureBranchStacking } from '$lib/config/uiFeatureFlags';
78
import { draggableCommit } from '$lib/dragging/draggable';
89
import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables';
910
import BranchFilesList from '$lib/file/BranchFilesList.svelte';
1011
import { ModeService } from '$lib/modes/service';
12+
import TextBox from '$lib/shared/TextBox.svelte';
1113
import { copyToClipboard } from '$lib/utils/clipboard';
1214
import { getContext, getContextStore, maybeGetContext } from '$lib/utils/context';
1315
import { openExternalUrl } from '$lib/utils/url';
@@ -48,6 +50,8 @@
4850
4951
const currentCommitMessage = persistedCommitMessage(project.id, branch?.id || '');
5052
53+
const branchStacking = featureBranchStacking();
54+
5155
let draggableCommitElement: HTMLElement | null = null;
5256
let files: RemoteFile[] = [];
5357
let showDetails = false;
@@ -82,6 +86,20 @@
8286
let commitMessageValid = false;
8387
let description = '';
8488
89+
let createRefModal: Modal;
90+
let createRefName = $baseBranch.remoteName + '/';
91+
92+
function openCreateRefModal(e: Event, commit: DetailedCommit | Commit) {
93+
e.stopPropagation();
94+
createRefModal.show(commit);
95+
}
96+
97+
function pushCommitRef(commit: DetailedCommit) {
98+
if (branch && commit.remoteRef) {
99+
branchController.pushChangeReference(branch.id, commit.remoteRef);
100+
}
101+
}
102+
85103
function openCommitMessageModal(e: Event) {
86104
e.stopPropagation();
87105
@@ -156,6 +174,29 @@
156174
{/snippet}
157175
</Modal>
158176

177+
<Modal bind:this={createRefModal} width="small">
178+
{#snippet children(commit)}
179+
<TextBox label="Remote branch name" id="newRemoteName" bind:value={createRefName} focus />
180+
<Button
181+
style="pop"
182+
kind="solid"
183+
onclick={() => {
184+
branchController.createChangeReference(
185+
branch?.id || '',
186+
'refs/remotes/' + createRefName,
187+
commit.changeId
188+
);
189+
createRefModal.close();
190+
}}
191+
>
192+
Ok
193+
</Button>
194+
{/snippet}
195+
{#snippet controls(close)}
196+
<Button style="ghost" outline type="reset" onclick={close}>Cancel</Button>
197+
{/snippet}
198+
</Modal>
199+
159200
<div
160201
class="commit-row"
161202
class:is-commit-open={showDetails}
@@ -315,6 +356,14 @@
315356
<span class="commit__subtitle-divider">•</span>
316357

317358
<span>{getTimeAndAuthor()}</span>
359+
360+
{#if $branchStacking && commit instanceof DetailedCommit}
361+
<div
362+
style="background-color:var(--clr-core-pop-80); border-radius: 3px; padding: 2px;"
363+
>
364+
{commit?.remoteRef}
365+
</div>
366+
{/if}
318367
</div>
319368
{/if}
320369
</div>
@@ -352,6 +401,26 @@
352401
icon="edit-small"
353402
onclick={openCommitMessageModal}>Edit message</Button
354403
>
404+
{#if $branchStacking && commit instanceof DetailedCommit && !commit.remoteRef}
405+
<Button
406+
size="tag"
407+
style="ghost"
408+
outline
409+
icon="branch"
410+
onclick={(e: Event) => {openCreateRefModal(e, commit)}}>Create ref</Button
411+
>
412+
{/if}
413+
{#if $branchStacking && commit instanceof DetailedCommit && commit.remoteRef}
414+
<Button
415+
size="tag"
416+
style="ghost"
417+
outline
418+
icon="remote"
419+
onclick={() => {
420+
pushCommitRef(commit);
421+
}}>Push ref</Button
422+
>
423+
{/if}
355424
{/if}
356425
{#if canEdit() && project.succeedingRebases}
357426
<Button size="tag" style="ghost" outline onclick={editPatch}>

apps/desktop/src/lib/config/uiFeatureFlags.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@ export function featureInlineUnifiedDiffs(): Persisted<boolean> {
1515
const key = 'inlineUnifiedDiffs';
1616
return persisted(false, key);
1717
}
18+
19+
export function featureBranchStacking(): Persisted<boolean> {
20+
const key = 'branchStacking';
21+
return persisted(false, key);
22+
}

apps/desktop/src/lib/vbranches/branchController.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,44 @@ export class BranchController {
9696
}
9797
}
9898

99+
/*
100+
* Creates a new GitButler change reference associated with a branch.
101+
* @param branchId
102+
* @param reference in the format refs/remotes/origin/my-branch (must be remote)
103+
* @param changeId The change id to point the reference to
104+
*/
105+
async createChangeReference(branchId: string, referenceName: string, changeId: string) {
106+
try {
107+
await invoke<void>('create_change_reference', {
108+
projectId: this.projectId,
109+
branchId: branchId,
110+
name: referenceName,
111+
changeId: changeId
112+
});
113+
} catch (err) {
114+
showError('Failed to create branch reference', err);
115+
}
116+
}
117+
118+
/*
119+
* Pushes a change reference to (converted to a git reference to a commit) to the remote
120+
* @param branchId
121+
* @param reference in the format refs/remotes/origin/my-branch (must be remote)
122+
* @param changeId The change id that is being pushed
123+
*/
124+
async pushChangeReference(branchId: string, referenceName: string, withForce: boolean = false) {
125+
try {
126+
await invoke<void>('push_change_reference', {
127+
projectId: this.projectId,
128+
branchId: branchId,
129+
name: referenceName,
130+
withForce: withForce
131+
});
132+
} catch (err) {
133+
showError('Failed to push change reference', err);
134+
}
135+
}
136+
99137
async updateBranchRemoteName(branchId: string, upstream: string) {
100138
try {
101139
await invoke<void>('update_virtual_branch', {

apps/desktop/src/lib/vbranches/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ export class DetailedCommit {
177177
isSigned!: boolean;
178178
relatedTo?: Commit;
179179
conflicted!: boolean;
180+
// Set if a GitButler branch reference pointing to this commit exists. In the format of "refs/remotes/origin/my-branch"
181+
remoteRef?: string | undefined;
180182

181183
prev?: DetailedCommit;
182184
next?: DetailedCommit;

apps/desktop/src/routes/settings/experimental/+page.svelte

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
import SectionCard from '$lib/components/SectionCard.svelte';
33
import {
44
featureBaseBranchSwitching,
5-
featureInlineUnifiedDiffs
5+
featureInlineUnifiedDiffs,
6+
featureBranchStacking
67
} from '$lib/config/uiFeatureFlags';
78
import ContentWrapper from '$lib/settings/ContentWrapper.svelte';
89
import Toggle from '$lib/shared/Toggle.svelte';
910
1011
const baseBranchSwitching = featureBaseBranchSwitching();
1112
const inlineUnifiedDiffs = featureInlineUnifiedDiffs();
13+
const branchStacking = featureBranchStacking();
1214
</script>
1315

1416
<ContentWrapper title="Experimental features">
@@ -45,6 +47,19 @@
4547
/>
4648
</svelte:fragment>
4749
</SectionCard>
50+
<SectionCard labelFor="branchStacking" orientation="row">
51+
<svelte:fragment slot="title">Branch stacking</svelte:fragment>
52+
<svelte:fragment slot="caption">
53+
Allows for branch / pull request stacking. The user interface for this is still very crude.
54+
</svelte:fragment>
55+
<svelte:fragment slot="actions">
56+
<Toggle
57+
id="branchStacking"
58+
checked={$branchStacking}
59+
on:click={() => ($branchStacking = !$branchStacking)}
60+
/>
61+
</svelte:fragment>
62+
</SectionCard>
4863
</ContentWrapper>
4964

5065
<style>

crates/gitbutler-branch-actions/src/actions.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use anyhow::{Context, Result};
2-
use gitbutler_branch::{BranchCreateRequest, BranchId, BranchOwnershipClaims, BranchUpdateRequest};
2+
use gitbutler_branch::{
3+
BranchCreateRequest, BranchId, BranchOwnershipClaims, BranchUpdateRequest, ChangeReference,
4+
};
35
use gitbutler_command_context::CommandContext;
46
use gitbutler_operating_modes::assure_open_workspace_mode;
57
use gitbutler_oplog::{
@@ -356,6 +358,30 @@ impl VirtualBranchActions {
356358
branch::insert_blank_commit(&ctx, branch_id, commit_oid, offset).map_err(Into::into)
357359
}
358360

361+
pub fn create_change_reference(
362+
&self,
363+
project: &Project,
364+
branch_id: BranchId,
365+
name: ReferenceName,
366+
change_id: String,
367+
) -> Result<ChangeReference> {
368+
let ctx = open_with_verify(project)?;
369+
assure_open_workspace_mode(&ctx).context("Requires an open workspace mode")?;
370+
gitbutler_repo::create_change_reference(&ctx, branch_id, name, change_id)
371+
}
372+
373+
pub fn push_change_reference(
374+
&self,
375+
project: &Project,
376+
branch_id: BranchId,
377+
name: ReferenceName,
378+
with_force: bool,
379+
) -> Result<()> {
380+
let helper = Helper::default();
381+
let ctx = open_with_verify(project)?;
382+
gitbutler_repo::push_change_reference(&ctx, branch_id, name, with_force, &helper)
383+
}
384+
359385
pub fn reorder_commit(
360386
&self,
361387
project: &Project,

crates/gitbutler-branch-actions/src/base.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ pub(crate) fn set_base_branch(
251251
applied: true,
252252
in_workspace: true,
253253
not_in_workspace_wip_change_id: None,
254+
references: vec![],
254255
};
255256

256257
vb_state.set_branch(branch)?;

crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ impl BranchManager<'_> {
112112
in_workspace: true,
113113
not_in_workspace_wip_change_id: None,
114114
source_refname: None,
115+
references: vec![],
115116
};
116117

117118
if let Some(ownership) = &create.ownership {
@@ -256,6 +257,7 @@ impl BranchManager<'_> {
256257
applied: true,
257258
in_workspace: true,
258259
not_in_workspace_wip_change_id: None,
260+
references: vec![],
259261
}
260262
};
261263

crates/gitbutler-branch-actions/src/commit.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use anyhow::{Context, Result};
66
use gitbutler_branch::{Branch, BranchId};
77
use gitbutler_command_context::CommandContext;
88
use gitbutler_commit::commit_ext::CommitExt;
9+
use gitbutler_reference::ReferenceName;
10+
use gitbutler_repo::list_branch_references;
911
use gitbutler_serde::BStringForFrontend;
1012
use serde::Serialize;
1113

@@ -34,10 +36,11 @@ pub struct VirtualBranchCommit {
3436
pub change_id: Option<String>,
3537
pub is_signed: bool,
3638
pub conflicted: bool,
39+
pub remote_ref: Option<ReferenceName>,
3740
}
3841

3942
pub(crate) fn commit_to_vbranch_commit(
40-
repository: &CommandContext,
43+
ctx: &CommandContext,
4144
branch: &Branch,
4245
commit: &git2::Commit,
4346
is_integrated: bool,
@@ -46,8 +49,7 @@ pub(crate) fn commit_to_vbranch_commit(
4649
let timestamp = u128::try_from(commit.time().seconds())?;
4750
let message = commit.message_bstr().to_owned();
4851

49-
let files =
50-
list_virtual_commit_files(repository, commit).context("failed to list commit files")?;
52+
let files = list_virtual_commit_files(ctx, commit).context("failed to list commit files")?;
5153

5254
let parent_ids: Vec<git2::Oid> = commit
5355
.parents()
@@ -56,6 +58,15 @@ pub(crate) fn commit_to_vbranch_commit(
5658
c
5759
})
5860
.collect::<Vec<_>>();
61+
let remote_ref = list_branch_references(ctx, branch.id)
62+
.map(|references| {
63+
references
64+
.into_iter()
65+
.find(|r| Some(r.change_id.clone()) == commit.change_id())
66+
})
67+
.ok()
68+
.flatten()
69+
.map(|r| r.name);
5970

6071
let commit = VirtualBranchCommit {
6172
id: commit.id(),
@@ -70,6 +81,7 @@ pub(crate) fn commit_to_vbranch_commit(
7081
change_id: commit.change_id(),
7182
is_signed: commit.is_signed(),
7283
conflicted: commit.is_conflicted(),
84+
remote_ref,
7385
};
7486

7587
Ok(commit)

crates/gitbutler-branch/src/branch.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname, Virtual
55
use serde::{Deserialize, Serialize, Serializer};
66
use std::ops::Deref;
77

8-
use crate::ownership::BranchOwnershipClaims;
8+
use crate::{ownership::BranchOwnershipClaims, reference::ChangeReference};
99

1010
pub type BranchId = Id<Branch>;
1111

@@ -67,6 +67,8 @@ pub struct Branch {
6767
pub in_workspace: bool,
6868
#[serde(default)]
6969
pub not_in_workspace_wip_change_id: Option<String>,
70+
#[serde(default)]
71+
pub references: Vec<ChangeReference>,
7072
}
7173

7274
fn default_true() -> bool {

crates/gitbutler-branch/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub use ownership::{reconcile_claims, BranchOwnershipClaims, ClaimOutcome};
1616
pub mod serde;
1717
mod target;
1818
pub use target::Target;
19+
mod reference;
20+
pub use reference::ChangeReference;
1921

2022
mod state;
2123
use lazy_static::lazy_static;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use gitbutler_reference::ReferenceName;
2+
use serde::{Deserialize, Serialize};
3+
4+
use crate::BranchId;
5+
6+
/// GitButler reference associated with a change (commit) on a virtual branch.
7+
/// These are not the same as regular Git references, but rather app-managed refs.
8+
/// Represent a deployable / reviewable part of a virtual branch that can be pushed to a remote
9+
/// and have a "Pull Request" created for it.
10+
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
11+
pub struct ChangeReference {
12+
/// Branch id of the virtual branch this reference belongs to
13+
/// Multiple references may belong to the same virtual branch, representing separate deployable / reviewable parts of the vbranch.
14+
pub branch_id: BranchId,
15+
/// Fully qualified reference name.
16+
/// The reference must be a remote reference.
17+
pub name: ReferenceName,
18+
/// The change id this reference points to.
19+
pub change_id: String,
20+
}

0 commit comments

Comments
 (0)