mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-28 04:47:42 +03:00
Mergy merge
This commit is contained in:
commit
09de80f6f0
4
.github/workflows/publish.yaml
vendored
4
.github/workflows/publish.yaml
vendored
@ -46,11 +46,9 @@ jobs:
|
||||
shell: bash
|
||||
if: ${{ !github.event.workflow_run }}
|
||||
run: |
|
||||
VITEMODE=development
|
||||
VITEMODE=nightly
|
||||
if [[ "${{ github.event.inputs.channel }}" == "release" ]]; then
|
||||
VITEMODE=production
|
||||
elif [[ "${{ github.event.inputs.channel }}" == "nightly" ]]; then
|
||||
VITEMODE=nightly
|
||||
fi
|
||||
|
||||
echo "vitemode=$VITEMODE" >> $GITHUB_ENV
|
||||
|
@ -20,7 +20,7 @@ export class Project {
|
||||
title!: string;
|
||||
description?: string;
|
||||
path!: string;
|
||||
api?: CloudProject & { sync: boolean };
|
||||
api?: CloudProject & { sync: boolean; sync_code: boolean | undefined };
|
||||
preferred_key!: Key;
|
||||
ok_with_force_push!: boolean;
|
||||
omit_certificate_check: boolean | undefined;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { showToast } from '$lib/notifications/toasts';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { relaunch } from '@tauri-apps/api/process';
|
||||
import {
|
||||
checkUpdate,
|
||||
@ -10,7 +11,7 @@ import {
|
||||
import posthog from 'posthog-js';
|
||||
import { derived, writable, type Readable } from 'svelte/store';
|
||||
|
||||
// TOOD: Investigate why 'DOWNLOADED' is not in the type provided by Tauri.
|
||||
// TODO: Investigate why 'DOWNLOADED' is not in the type provided by Tauri.
|
||||
export type Update =
|
||||
| { version?: string; status?: UpdateStatus | 'DOWNLOADED'; body?: string }
|
||||
| undefined;
|
||||
@ -33,6 +34,7 @@ export class UpdaterService {
|
||||
undefined
|
||||
);
|
||||
|
||||
currentVersion = writable<string | undefined>(undefined);
|
||||
readonly version = derived(this.update, (update) => update?.version);
|
||||
|
||||
prev: Update | undefined;
|
||||
@ -42,6 +44,8 @@ export class UpdaterService {
|
||||
constructor() {}
|
||||
|
||||
private async start() {
|
||||
const currentVersion = await getVersion();
|
||||
this.currentVersion.set(currentVersion);
|
||||
await this.checkForUpdate();
|
||||
setInterval(async () => await this.checkForUpdate(), 3600000); // hourly
|
||||
this.unlistenFn = await onUpdaterEvent((status) => {
|
||||
|
@ -118,7 +118,7 @@
|
||||
<div class="wrapper" data-tauri-drag-region>
|
||||
<BranchCard {commitBoxOpen} {isLaneCollapsed} />
|
||||
|
||||
{#await $selectedFile then selected}
|
||||
{#await $selectedFile then [commitId, selected]}
|
||||
{#if selected}
|
||||
<div
|
||||
class="file-preview"
|
||||
@ -132,6 +132,7 @@
|
||||
file={selected}
|
||||
readonly={selected instanceof RemoteFile}
|
||||
selectable={$commitBoxOpen}
|
||||
{commitId}
|
||||
on:close={() => {
|
||||
fileIdSelection.clear();
|
||||
}}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { DraggableCommit, DraggableHunk, DraggableFile } from '$lib/dragging/draggables';
|
||||
import { filesToOwnership } from '$lib/vbranches/ownership';
|
||||
import { LocalFile, type VirtualBranch } from '$lib/vbranches/types';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { VirtualBranch } from '$lib/vbranches/types';
|
||||
|
||||
class BranchDragActions {
|
||||
constructor(
|
||||
@ -18,9 +18,13 @@ class BranchDragActions {
|
||||
}
|
||||
|
||||
acceptBranchDrop(data: any) {
|
||||
if (data instanceof DraggableHunk && data.branchId !== this.branch.id) {
|
||||
if (data instanceof DraggableHunk && !data.commitId && data.branchId !== this.branch.id) {
|
||||
return !data.hunk.locked;
|
||||
} else if (data instanceof DraggableFile && data.branchId && data.branchId !== this.branch.id) {
|
||||
} else if (
|
||||
data instanceof DraggableFile &&
|
||||
data.file instanceof LocalFile &&
|
||||
this.branch.id !== data.branchId
|
||||
) {
|
||||
return !data.files.some((f) => f.locked);
|
||||
} else {
|
||||
return false;
|
||||
|
@ -31,18 +31,17 @@ export class CommitDragActions {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data instanceof DraggableHunk && data.branchId === this.branch.id) {
|
||||
if (data.lockedTo.length > 0) {
|
||||
return !!data.lockedTo.find((lock) => lock.commitId === this.commit.id);
|
||||
}
|
||||
if (
|
||||
data instanceof DraggableHunk &&
|
||||
data.branchId === this.branch.id &&
|
||||
data.commitId !== this.commit.id
|
||||
) {
|
||||
return true;
|
||||
} else if (data instanceof DraggableFile && data.branchId === this.branch.id) {
|
||||
const someLock = data.files.some((file) => file.lockedIds.length > 0);
|
||||
if (someLock) {
|
||||
return data.files.every((file) =>
|
||||
file.lockedIds.find((lock) => lock.commitId === this.commit.id)
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
data instanceof DraggableFile &&
|
||||
data.branchId === this.branch.id &&
|
||||
data.commit?.id !== this.commit.id
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -8,10 +8,13 @@
|
||||
const updaterService = getContext(UpdaterService);
|
||||
const update = updaterService.update;
|
||||
const version = updaterService.version;
|
||||
const currentVersion = updaterService.currentVersion;
|
||||
|
||||
let dismissed = $state(false);
|
||||
$effect(() => {
|
||||
if (version && dismissed) dismissed = false;
|
||||
if ($version !== $currentVersion && dismissed) {
|
||||
dismissed = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -120,9 +123,8 @@
|
||||
Release notes
|
||||
</Button>
|
||||
<div class="status-section">
|
||||
<div class="sliding-gradient"></div>
|
||||
|
||||
{#if !$update.status}
|
||||
<div class="sliding-gradient"></div>
|
||||
<div class="cta-btn" transition:fade={{ duration: 100 }}>
|
||||
<Button
|
||||
wide
|
||||
@ -136,6 +138,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
{:else if $update.status === 'DONE'}
|
||||
<div class="sliding-gradient"></div>
|
||||
<div class="cta-btn" transition:fade={{ duration: 100 }}>
|
||||
<Button style="pop" kind="solid" wide on:click={() => updaterService.relaunchApp()}
|
||||
>Restart</Button
|
||||
@ -190,7 +193,6 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
height: var(--size-button);
|
||||
width: 100%;
|
||||
border-radius: var(--radius-m);
|
||||
|
||||
|
@ -358,7 +358,7 @@
|
||||
.empty-board__suggestions__links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
gap: 2px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
@ -368,7 +368,7 @@
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 4px 6px 4px 4px;
|
||||
padding: 6px;
|
||||
border-radius: var(--radius-s);
|
||||
gap: 10px;
|
||||
transition: background-color var(--transition-fast);
|
||||
@ -387,6 +387,7 @@
|
||||
}
|
||||
|
||||
.empty-board__suggestions__link__icon {
|
||||
display: flex;
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
}
|
||||
</style>
|
||||
|
@ -176,13 +176,14 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="base__right">
|
||||
{#await $selectedFile then selected}
|
||||
{#await $selectedFile then [commitId, selected]}
|
||||
{#if selected}
|
||||
<FileCard
|
||||
conflicted={selected.conflicted}
|
||||
file={selected}
|
||||
isUnapplied={false}
|
||||
readonly={true}
|
||||
{commitId}
|
||||
on:close={() => {
|
||||
fileIdSelection.clear();
|
||||
}}
|
||||
|
@ -18,7 +18,8 @@ export class DraggableHunk {
|
||||
constructor(
|
||||
public readonly branchId: string,
|
||||
public readonly hunk: Hunk,
|
||||
public readonly lockedTo: HunkLock[]
|
||||
public readonly lockedTo: HunkLock[],
|
||||
public readonly commitId: string | undefined
|
||||
) {}
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,7 @@
|
||||
target: '.dropzone-target'
|
||||
}}
|
||||
class:fill-height={fillHeight}
|
||||
class="dropzone-container"
|
||||
>
|
||||
{@render overlay({ hovered, activated })}
|
||||
|
||||
@ -67,4 +68,8 @@
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dropzone-container {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
@ -24,13 +24,12 @@
|
||||
--dropzone-overlap: calc(var(--dropzone-height) / 2);
|
||||
--dropzone-height: 16px;
|
||||
|
||||
position: absolute;
|
||||
top: var(--y-offset);
|
||||
|
||||
height: var(--dropzone-height);
|
||||
margin-top: calc(var(--dropzone-overlap) * -1);
|
||||
margin-bottom: calc(var(--dropzone-overlap) * -1);
|
||||
/* background-color: rgba(0, 0, 0, 0.1); */
|
||||
width: 100%;
|
||||
position: relative;
|
||||
top: var(--y-offset);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -14,12 +14,14 @@
|
||||
selectable?: boolean;
|
||||
readonly?: boolean;
|
||||
isCard?: boolean;
|
||||
commitId?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
file,
|
||||
conflicted,
|
||||
isUnapplied,
|
||||
commitId,
|
||||
selectable = false,
|
||||
readonly = false,
|
||||
isCard = true
|
||||
@ -67,6 +69,7 @@
|
||||
{isFileLocked}
|
||||
{isUnapplied}
|
||||
{selectable}
|
||||
{commitId}
|
||||
/>
|
||||
</ScrollableContainer>
|
||||
</div>
|
||||
|
@ -17,6 +17,7 @@
|
||||
selectable: boolean;
|
||||
isFileLocked: boolean;
|
||||
readonly: boolean;
|
||||
commitId?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
@ -25,6 +26,7 @@
|
||||
isLarge,
|
||||
sections,
|
||||
isUnapplied,
|
||||
commitId,
|
||||
selectable = false,
|
||||
isFileLocked = false,
|
||||
readonly = false
|
||||
@ -67,9 +69,12 @@
|
||||
{@const { added, removed } = computeAddedRemovedByHunk(section)}
|
||||
{#if 'hunk' in section}
|
||||
<div class="hunk-wrapper">
|
||||
<div class="indicators text-base-11">
|
||||
<span class="added">+{added}</span>
|
||||
<span class="removed">-{removed}</span>
|
||||
<div class="indicators text-base-11 text-semibold">
|
||||
<div class="text-base-10 semibold added-removed">
|
||||
<span class="added">+{added}</span>
|
||||
<span class="removed">-{removed}</span>
|
||||
</div>
|
||||
|
||||
{#if section.hunk.lockedTo && section.hunk.lockedTo.length > 0 && commits}
|
||||
<div
|
||||
use:tooltip={{
|
||||
@ -92,6 +97,7 @@
|
||||
{isFileLocked}
|
||||
{minWidth}
|
||||
{readonly}
|
||||
{commitId}
|
||||
linesModified={added + removed}
|
||||
/>
|
||||
</div>
|
||||
@ -120,10 +126,25 @@
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.added {
|
||||
color: #45b156;
|
||||
|
||||
.added-removed {
|
||||
display: flex;
|
||||
border-radius: var(--radius-s);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.removed,
|
||||
.added {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.added {
|
||||
color: var(--clr-scale-succ-30);
|
||||
background-color: var(--clr-theme-succ-bg);
|
||||
}
|
||||
|
||||
.removed {
|
||||
color: #ff3e00;
|
||||
color: var(--clr-scale-err-30);
|
||||
background-color: var(--clr-theme-err-bg);
|
||||
}
|
||||
</style>
|
||||
|
@ -52,7 +52,7 @@
|
||||
let contents = $state<HTMLDivElement>();
|
||||
|
||||
const WHITESPACE_REGEX = /\s/;
|
||||
const NUMBER_COLUMN_WIDTH_PX = minWidth * 16;
|
||||
const NUMBER_COLUMN_WIDTH_PX = minWidth * 20;
|
||||
|
||||
const selectedOwnership: Writable<Ownership> | undefined = maybeGetContextStore(Ownership);
|
||||
|
||||
@ -210,6 +210,22 @@
|
||||
const renderRows = $derived(generateRows(subsections));
|
||||
</script>
|
||||
|
||||
{#snippet countColumn(count: number | undefined, lineType: SectionType)}
|
||||
<td
|
||||
class="table__numberColumn"
|
||||
class:diff-line-deletion={lineType === SectionType.RemovedLines}
|
||||
class:diff-line-addition={lineType === SectionType.AddedLines}
|
||||
style="--number-col-width: {NUMBER_COLUMN_WIDTH_PX}px;"
|
||||
align="center"
|
||||
class:selected={isSelected}
|
||||
onclick={() => {
|
||||
selectable && handleSelected(hunk, !isSelected);
|
||||
}}
|
||||
>
|
||||
{count}
|
||||
</td>
|
||||
{/snippet}
|
||||
|
||||
<div
|
||||
class="table__wrapper hide-native-scrollbar"
|
||||
bind:this={viewport}
|
||||
@ -224,28 +240,8 @@
|
||||
<tbody>
|
||||
{#each renderRows as line}
|
||||
<tr data-no-drag>
|
||||
<td
|
||||
class="table__numberColumn"
|
||||
style="--number-col-width: {NUMBER_COLUMN_WIDTH_PX}px;"
|
||||
align="center"
|
||||
class:selected={isSelected}
|
||||
onclick={() => {
|
||||
selectable && handleSelected(hunk, !isSelected);
|
||||
}}
|
||||
>
|
||||
{line.beforeLineNumber}
|
||||
</td>
|
||||
<td
|
||||
class="table__numberColumn"
|
||||
style="--number-col-width: {NUMBER_COLUMN_WIDTH_PX}px;"
|
||||
align="center"
|
||||
class:selected={isSelected}
|
||||
onclick={() => {
|
||||
selectable && handleSelected(hunk, !isSelected);
|
||||
}}
|
||||
>
|
||||
{line.afterLineNumber}
|
||||
</td>
|
||||
{@render countColumn(line.beforeLineNumber, line.type)}
|
||||
{@render countColumn(line.afterLineNumber, line.type)}
|
||||
<td
|
||||
{onclick}
|
||||
class="table__textContent"
|
||||
@ -274,6 +270,7 @@
|
||||
.table__wrapper {
|
||||
border: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--clr-bg-1);
|
||||
overflow-x: auto;
|
||||
|
||||
&:hover .table__drag-handle {
|
||||
@ -311,53 +308,48 @@
|
||||
}
|
||||
|
||||
.table__numberColumn {
|
||||
color: var(--clr-text-3);
|
||||
color: color-mix(in srgb, var(--clr-text-1), transparent 60%);
|
||||
border-color: var(--clr-border-2);
|
||||
background-color: var(--clr-bg-1-muted);
|
||||
font-size: 11px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
text-align: center;
|
||||
padding: 0 4px;
|
||||
text-align: right;
|
||||
cursor: var(--cursor);
|
||||
user-select: none;
|
||||
|
||||
position: sticky;
|
||||
left: calc(var(--number-col-width));
|
||||
width: var(--number-col-width);
|
||||
min-width: var(--number-col-width);
|
||||
max-width: var(--number-col-width);
|
||||
left: calc(var(--number-col-width) + 1px);
|
||||
box-shadow: 1px 0px 0px 0px var(--clr-border-2);
|
||||
|
||||
box-shadow: inset -1px 0 0 0 var(--clr-border-2);
|
||||
|
||||
&.diff-line-addition {
|
||||
background-color: var(--override-addition-counter-background);
|
||||
color: var(--override-addition-counter-text);
|
||||
box-shadow: inset -1px 0 0 0 var(--override-addition-counter-border);
|
||||
}
|
||||
|
||||
&.diff-line-deletion {
|
||||
background-color: var(--override-deletion-counter-background);
|
||||
color: var(--override-deletion-counter-text);
|
||||
box-shadow: inset -1px 0 0 0 var(--override-deletion-counter-border);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--hunk-line-selected-bg);
|
||||
border-color: var(--hunk-line-selected-border);
|
||||
color: white;
|
||||
box-shadow: inset -1px 0 0 0 var(--hunk-line-selected-border);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.table__numberColumn:first-of-type {
|
||||
width: var(--number-col-width);
|
||||
min-width: var(--number-col-width);
|
||||
max-width: var(--number-col-width);
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
tr:first-of-type .table__numberColumn:first-child {
|
||||
border-radius: var(--radius-s) 0 0 0;
|
||||
}
|
||||
|
||||
tr:last-of-type .table__numberColumn:first-child {
|
||||
border-radius: 0 0 0 var(--radius-s);
|
||||
}
|
||||
|
||||
.diff-line-deletion {
|
||||
background-color: #cf8d8e20;
|
||||
}
|
||||
|
||||
.diff-line-addition {
|
||||
background-color: #94cf8d20;
|
||||
}
|
||||
|
||||
.table__textContent {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
|
@ -21,6 +21,7 @@
|
||||
readonly: boolean;
|
||||
minWidth: number;
|
||||
linesModified: number;
|
||||
commitId?: string | undefined;
|
||||
}
|
||||
|
||||
let {
|
||||
@ -31,6 +32,7 @@
|
||||
isUnapplied,
|
||||
isFileLocked,
|
||||
minWidth,
|
||||
commitId,
|
||||
readonly = false
|
||||
}: Props = $props();
|
||||
|
||||
@ -42,7 +44,7 @@
|
||||
let alwaysShow = $state(false);
|
||||
let viewport = $state<HTMLDivElement>();
|
||||
let contextMenu = $state<HunkContextMenu>();
|
||||
const draggingDisabled = $derived(readonly || isUnapplied);
|
||||
const draggingDisabled = $derived(isUnapplied);
|
||||
|
||||
function onHunkSelected(hunk: Hunk, isSelected: boolean) {
|
||||
if (!selectedOwnership) return;
|
||||
@ -71,7 +73,7 @@
|
||||
class:opacity-60={section.hunk.locked && !isFileLocked}
|
||||
oncontextmenu={(e) => e.preventDefault()}
|
||||
use:draggableElement={{
|
||||
data: new DraggableHunk($branch?.id || '', section.hunk, section.hunk.lockedTo),
|
||||
data: new DraggableHunk($branch?.id || '', section.hunk, section.hunk.lockedTo, commitId),
|
||||
disabled: draggingDisabled
|
||||
}}
|
||||
>
|
||||
|
@ -31,7 +31,7 @@
|
||||
project.api.repository_id
|
||||
);
|
||||
if (cloudProject === project.api) return;
|
||||
project.api = { ...cloudProject, sync: project.api.sync };
|
||||
project.api = { ...cloudProject, sync: project.api.sync, sync_code: project.api.sync_code };
|
||||
projectService.updateProject(project);
|
||||
});
|
||||
|
||||
@ -45,7 +45,25 @@
|
||||
description: project.description,
|
||||
uid: project.id
|
||||
}));
|
||||
project.api = { ...cloudProject, sync };
|
||||
project.api = { ...cloudProject, sync, sync_code: project.api?.sync_code };
|
||||
projectService.updateProject(project);
|
||||
} catch (error) {
|
||||
console.error(`Failed to update project sync status: ${error}`);
|
||||
toasts.error('Failed to update project sync status');
|
||||
}
|
||||
}
|
||||
// These functions are disgusting
|
||||
async function onSyncCodeChange(sync_code: boolean) {
|
||||
if (!$user) return;
|
||||
try {
|
||||
const cloudProject =
|
||||
project.api ??
|
||||
(await projectService.createCloudProject($user.access_token, {
|
||||
name: project.title,
|
||||
description: project.description,
|
||||
uid: project.id
|
||||
}));
|
||||
project.api = { ...cloudProject, sync: project.api?.sync || false, sync_code: sync_code };
|
||||
projectService.updateProject(project);
|
||||
} catch (error) {
|
||||
console.error(`Failed to update project sync status: ${error}`);
|
||||
@ -110,7 +128,8 @@
|
||||
|
||||
<SectionCard labelFor="historySync" orientation="row">
|
||||
<svelte:fragment slot="caption">
|
||||
Sync my history, repository and branch data for backup, sharing and team features.
|
||||
Sync this project's operations log with GitButler Web services. The operations log includes
|
||||
snapshots of the repository state, including non-committed code changes.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle
|
||||
@ -120,6 +139,18 @@
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
<SectionCard labelFor="historySync" orientation="row">
|
||||
<svelte:fragment slot="caption">
|
||||
Sync this repository's branches with the GitButler Remote.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle
|
||||
id="historySync"
|
||||
checked={project.api?.sync_code || false}
|
||||
on:click={async (e) => await onSyncCodeChange(!!e.detail)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
|
||||
{#if project.api}
|
||||
<div class="api-link">
|
||||
|
@ -22,7 +22,7 @@
|
||||
description: project.description
|
||||
})
|
||||
: undefined;
|
||||
project.api = api ? { ...api, sync: true } : undefined;
|
||||
project.api = api ? { ...api, sync: false, sync_code: undefined } : undefined;
|
||||
projectService.updateProject(project);
|
||||
}
|
||||
</script>
|
||||
|
@ -23,9 +23,10 @@
|
||||
.large-diff-message {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
flex-direction: column;
|
||||
background-color: var(--clr-bg-1);
|
||||
border: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
|
||||
.frame-box {
|
||||
|
@ -95,14 +95,15 @@ export class FileIdSelection {
|
||||
return fileKey;
|
||||
}
|
||||
|
||||
#selectedFile: Readable<Promise<AnyFile | undefined>> | undefined;
|
||||
#selectedFile: Readable<Promise<[string | undefined, AnyFile | undefined]>> | undefined;
|
||||
get selectedFile() {
|
||||
this.#selectedFile ||= derived(
|
||||
[this as Readable<string[]>, this.localFiles],
|
||||
async ([selection, localFiles]): Promise<AnyFile | undefined> => {
|
||||
if (selection.length !== 1) return;
|
||||
async ([selection, localFiles]): Promise<[string | undefined, AnyFile | undefined]> => {
|
||||
if (selection.length !== 1) return [undefined, undefined];
|
||||
const fileKey = parseFileKey(selection[0]);
|
||||
return await findFileByKey(localFiles, this.projectId, fileKey);
|
||||
const file = await findFileByKey(localFiles, this.projectId, fileKey);
|
||||
return [fileKey.commitId, file];
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -63,13 +63,14 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="base__right">
|
||||
{#await $selectedFile then selected}
|
||||
{#await $selectedFile then [commitId, selected]}
|
||||
{#if selected}
|
||||
<FileCard
|
||||
conflicted={selected.conflicted}
|
||||
file={selected}
|
||||
isUnapplied={false}
|
||||
readonly={true}
|
||||
{commitId}
|
||||
on:close={() => {
|
||||
fileIdSelection.clear();
|
||||
}}
|
||||
|
@ -13,6 +13,7 @@ body {
|
||||
|
||||
color: var(--clr-text-1);
|
||||
background-color: var(--clr-bg-2);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,199 +0,0 @@
|
||||
.token-variable {
|
||||
color: #8953800;
|
||||
}
|
||||
|
||||
.token-property {
|
||||
color: #0550ae;
|
||||
}
|
||||
|
||||
.token-type {
|
||||
color: #116329;
|
||||
}
|
||||
|
||||
.token-variable-special {
|
||||
color: #953800;
|
||||
}
|
||||
|
||||
.token-definition {
|
||||
color: #953800;
|
||||
}
|
||||
|
||||
/* .token-builtin {
|
||||
color: #d3869b;
|
||||
} */
|
||||
|
||||
.token-number {
|
||||
color: #0550ae;
|
||||
}
|
||||
|
||||
.token-string {
|
||||
color: #0550ae;
|
||||
}
|
||||
|
||||
.token-string-special {
|
||||
color: #0a3069;
|
||||
}
|
||||
|
||||
/* .token-atom {
|
||||
color: #0a3069;
|
||||
} */
|
||||
|
||||
.token-keyword {
|
||||
color: #cf222e;
|
||||
}
|
||||
|
||||
.token-comment {
|
||||
color: #6e7781;
|
||||
}
|
||||
|
||||
.token-meta {
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
.token-invalid {
|
||||
color: #82071e;
|
||||
}
|
||||
|
||||
.token-tag {
|
||||
color: #116329;
|
||||
}
|
||||
|
||||
.token-attribute {
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
.token-attribute-value {
|
||||
color: var(--color-token-attribute-value);
|
||||
}
|
||||
|
||||
.token-inserted {
|
||||
color: #116329;
|
||||
background-color: #11632960;
|
||||
}
|
||||
|
||||
.token-deleted {
|
||||
color: #82071e;
|
||||
background-color: #82071e40;
|
||||
}
|
||||
|
||||
.token-heading {
|
||||
color: var(--color-token-variable-special);
|
||||
}
|
||||
|
||||
.token-link {
|
||||
color: var(--color-token-variable-special);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.token-strikethrough {
|
||||
text-decoration: strike-through;
|
||||
}
|
||||
|
||||
.token-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.token-variable {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.token-property {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.token-type {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.token-variable-special {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.token-definition {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
/* .token-builtin {
|
||||
color: #d3869b;
|
||||
} */
|
||||
|
||||
.token-number {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.token-string {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.token-string-special {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
/* .token-atom {
|
||||
color: #0a3069;
|
||||
} */
|
||||
|
||||
.token-keyword {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.token-comment {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.token-meta {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
.token-invalid {
|
||||
color: #ffa198;
|
||||
}
|
||||
|
||||
.token-tag {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.token-attribute {
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.token-attribute-value {
|
||||
color: var(--color-token-attribute-value);
|
||||
}
|
||||
|
||||
.token-inserted {
|
||||
color: #7ee787;
|
||||
background-color: #7ee78740;
|
||||
}
|
||||
|
||||
.token-deleted {
|
||||
color: #ffa198;
|
||||
background-color: #ffa19840;
|
||||
}
|
||||
|
||||
.token-heading {
|
||||
color: var(--color-token-variable-special);
|
||||
}
|
||||
|
||||
.token-link {
|
||||
color: var(--color-token-variable-special);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.token-strikethrough {
|
||||
text-decoration: strike-through;
|
||||
}
|
||||
|
||||
.token-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
@ -206,7 +206,9 @@ impl VirtualBranchActions {
|
||||
.context("Deleting a branch order requires open workspace mode")?;
|
||||
let branch_manager = ctx.branch_manager();
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
branch_manager.delete_branch(branch_id, guard.write_permission())
|
||||
let default_target = ctx.project().virtual_branches().get_default_target()?;
|
||||
let target_commit = ctx.repository().find_commit(default_target.sha)?;
|
||||
branch_manager.delete_branch(branch_id, guard.write_permission(), &target_commit)
|
||||
}
|
||||
|
||||
pub fn unapply_ownership(
|
||||
|
@ -106,7 +106,8 @@ fn combine_branches(
|
||||
group_branches.push(GroupBranch::Virtual(branch));
|
||||
}
|
||||
let remotes = repo.remotes()?;
|
||||
let target_branch = vb_handle.get_default_target().ok();
|
||||
let target_branch = vb_handle.get_default_target()?;
|
||||
|
||||
// Group branches by identity
|
||||
let mut groups: HashMap<Option<String>, Vec<&GroupBranch>> = HashMap::new();
|
||||
for branch in group_branches.iter() {
|
||||
@ -132,6 +133,7 @@ fn combine_branches(
|
||||
group_branches.clone(),
|
||||
repo,
|
||||
&local_author,
|
||||
target_branch.sha,
|
||||
);
|
||||
if branch_entry.is_err() {
|
||||
tracing::warn!(
|
||||
@ -152,6 +154,7 @@ fn branch_group_to_branch(
|
||||
group_branches: Vec<&GroupBranch>,
|
||||
repo: &git2::Repository,
|
||||
local_author: &git2::Signature,
|
||||
target_sha: git2::Oid,
|
||||
) -> Result<BranchListing> {
|
||||
let virtual_branch = group_branches
|
||||
.iter()
|
||||
@ -219,9 +222,8 @@ fn branch_group_to_branch(
|
||||
virtual_branch.map_or(0, |x| x.updated_timestamp_ms),
|
||||
);
|
||||
let last_commiter = head_commit.author().into();
|
||||
let repo_head = repo.head()?.peel_to_commit()?;
|
||||
// If no merge base can be found, return with zero stats
|
||||
let branch = if let Ok(base) = repo.merge_base(repo_head.id(), head) {
|
||||
let branch = if let Ok(base) = repo.merge_base(target_sha, head) {
|
||||
let mut revwalk = repo.revwalk()?;
|
||||
revwalk.push(head)?;
|
||||
revwalk.hide(base)?;
|
||||
@ -297,12 +299,10 @@ impl GroupBranch<'_> {
|
||||
|
||||
/// Determines if a branch should be listed in the UI.
|
||||
/// This excludes the target branch as well as gitbutler specific branches.
|
||||
fn should_list_git_branch(identity: &Option<String>, target: &Option<Target>) -> bool {
|
||||
fn should_list_git_branch(identity: &Option<String>, target: &Target) -> bool {
|
||||
// Exclude the target branch
|
||||
if let Some(target) = target {
|
||||
if identity == &Some(target.branch.branch().to_owned()) {
|
||||
return false;
|
||||
}
|
||||
if identity == &Some(target.branch.branch().to_owned()) {
|
||||
return false;
|
||||
}
|
||||
// Exclude gitbutler technical branches (not useful for the user)
|
||||
if identity == &Some("gitbutler/integration".to_string())
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use git2::Commit;
|
||||
use gitbutler_branch::{Branch, BranchExt, BranchId};
|
||||
use gitbutler_commit::commit_headers::CommitHeadersV2;
|
||||
use gitbutler_oplog::SnapshotExt;
|
||||
@ -25,13 +26,17 @@ impl BranchManager<'_> {
|
||||
perm: &mut WorktreeWritePermission,
|
||||
) -> Result<ReferenceName> {
|
||||
let vb_state = self.ctx.project().virtual_branches();
|
||||
let target_commit = self
|
||||
.ctx
|
||||
.repository()
|
||||
.find_commit(vb_state.get_default_target()?.sha)?;
|
||||
|
||||
let mut target_branch = vb_state.get_branch(branch_id)?;
|
||||
|
||||
// Convert the vbranch to a real branch
|
||||
let real_branch = self.build_real_branch(&mut target_branch)?;
|
||||
|
||||
self.delete_branch(branch_id, perm)?;
|
||||
self.delete_branch(branch_id, perm, &target_commit)?;
|
||||
|
||||
// If we were conflicting, it means that it was the only branch applied. Since we've now unapplied it we can clear all conflicts
|
||||
if conflicts::is_conflicting(self.ctx, None)? {
|
||||
@ -52,6 +57,7 @@ impl BranchManager<'_> {
|
||||
&self,
|
||||
branch_id: BranchId,
|
||||
perm: &mut WorktreeWritePermission,
|
||||
target_commit: &Commit,
|
||||
) -> Result<()> {
|
||||
let vb_state = self.ctx.project().virtual_branches();
|
||||
let Some(branch) = vb_state.try_branch(branch_id)? else {
|
||||
@ -70,7 +76,6 @@ impl BranchManager<'_> {
|
||||
|
||||
let repo = self.ctx.repository();
|
||||
|
||||
let target_commit = repo.target_commit()?;
|
||||
let base_tree = target_commit.tree().context("failed to get target tree")?;
|
||||
|
||||
let applied_statuses = get_applied_status(self.ctx, None)
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{path::PathBuf, vec};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bstr::ByteSlice;
|
||||
use gitbutler_branch::{
|
||||
self, Branch, BranchCreateRequest, VirtualBranchesHandle,
|
||||
@ -16,6 +16,7 @@ use gitbutler_repo::{LogUntil, RepoActionsExt, RepositoryExt};
|
||||
use crate::{branch_manager::BranchManagerExt, conflicts, VirtualBranchesExt};
|
||||
|
||||
const WORKSPACE_HEAD: &str = "Workspace Head";
|
||||
const GITBUTLER_INTEGRATION_COMMIT_TITLE: &str = "GitButler Integration Commit";
|
||||
|
||||
pub(crate) fn get_integration_commiter<'a>() -> Result<git2::Signature<'a>> {
|
||||
Ok(git2::Signature::now(
|
||||
@ -37,20 +38,9 @@ pub(crate) fn get_workspace_head(ctx: &CommandContext) -> Result<git2::Oid> {
|
||||
|
||||
let mut virtual_branches: Vec<Branch> = vb_state.list_branches_in_workspace()?;
|
||||
|
||||
let branch_heads = virtual_branches
|
||||
.iter()
|
||||
.map(|b| repo.find_commit(b.head))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let branch_head_refs = branch_heads.iter().collect::<Vec<_>>();
|
||||
|
||||
let target_commit = repo.find_commit(target.sha)?;
|
||||
let mut workspace_tree = target_commit.tree()?;
|
||||
|
||||
// If no branches are applied then the workspace head is the target.
|
||||
if branch_head_refs.is_empty() {
|
||||
return Ok(target_commit.id());
|
||||
}
|
||||
|
||||
if conflicts::is_conflicting(ctx, None)? {
|
||||
let merge_parent = conflicts::merge_parent(ctx)?.ok_or(anyhow!("No merge parent"))?;
|
||||
let first_branch = virtual_branches.first().ok_or(anyhow!("No branches"))?;
|
||||
@ -142,9 +132,6 @@ pub fn update_gitbutler_integration(
|
||||
|
||||
let repo: &git2::Repository = ctx.repository();
|
||||
|
||||
// get commit object from target.sha
|
||||
let target_commit = repo.find_commit(target.sha)?;
|
||||
|
||||
// get current repo head for reference
|
||||
let head_ref = repo.head()?;
|
||||
let integration_filepath = repo.path().join("integration");
|
||||
@ -168,11 +155,10 @@ pub fn update_gitbutler_integration(
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to list virtual branches")?;
|
||||
|
||||
let integration_commit = repo.find_commit(get_workspace_head(ctx)?)?;
|
||||
let integration_tree = integration_commit.tree()?;
|
||||
let workspace_head = repo.find_commit(get_workspace_head(ctx)?)?;
|
||||
|
||||
// message that says how to get back to where they were
|
||||
let mut message = "GitButler Integration Commit".to_string();
|
||||
let mut message = GITBUTLER_INTEGRATION_COMMIT_TITLE.to_string();
|
||||
message.push_str("\n\n");
|
||||
message.push_str(
|
||||
"This is an integration commit for the virtual branches that GitButler is tracking.\n\n",
|
||||
@ -217,13 +203,17 @@ pub fn update_gitbutler_integration(
|
||||
|
||||
// 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.
|
||||
|
||||
let parents = workspace_head.parents().collect::<Vec<_>>();
|
||||
let workspace_tree = workspace_head.tree()?;
|
||||
|
||||
let final_commit = repo.commit(
|
||||
None,
|
||||
&committer,
|
||||
&committer,
|
||||
&message,
|
||||
&integration_commit.tree()?,
|
||||
&[&target_commit],
|
||||
&workspace_tree,
|
||||
parents.iter().collect::<Vec<_>>().as_slice(),
|
||||
)?;
|
||||
|
||||
// Create or replace the integration branch reference, then set as HEAD.
|
||||
@ -236,7 +226,7 @@ pub fn update_gitbutler_integration(
|
||||
repo.set_head(&GITBUTLER_INTEGRATION_REFERENCE.clone().to_string())?;
|
||||
|
||||
let mut index = repo.index()?;
|
||||
index.read_tree(&integration_tree)?;
|
||||
index.read_tree(&workspace_tree)?;
|
||||
index.write()?;
|
||||
|
||||
// finally, update the refs/gitbutler/ heads to the states of the current virtual branches
|
||||
@ -330,16 +320,21 @@ fn verify_head_is_clean(ctx: &CommandContext, perm: &mut WorktreeWritePermission
|
||||
.get_default_target()
|
||||
.context("failed to get default target")?;
|
||||
|
||||
let mut extra_commits = ctx
|
||||
let commits = ctx
|
||||
.log(head_commit.id(), LogUntil::Commit(default_target.sha))
|
||||
.context("failed to get log")?;
|
||||
|
||||
let integration_commit = extra_commits.pop();
|
||||
|
||||
if integration_commit.is_none() {
|
||||
// no integration commit found
|
||||
bail!("gibButler's integration commit not found on head");
|
||||
}
|
||||
let integration_index = commits
|
||||
.iter()
|
||||
.position(|commit| {
|
||||
commit
|
||||
.message()
|
||||
.is_some_and(|message| message.starts_with(GITBUTLER_INTEGRATION_COMMIT_TITLE))
|
||||
})
|
||||
.context("GitButler integration commit not found")?;
|
||||
let integration_commit = &commits[integration_index];
|
||||
let mut extra_commits = commits[..integration_index].to_vec();
|
||||
extra_commits.reverse();
|
||||
|
||||
if extra_commits.is_empty() {
|
||||
// no extra commits found, so we're good
|
||||
@ -347,11 +342,7 @@ fn verify_head_is_clean(ctx: &CommandContext, perm: &mut WorktreeWritePermission
|
||||
}
|
||||
|
||||
ctx.repository()
|
||||
.reset(
|
||||
integration_commit.as_ref().unwrap().as_object(),
|
||||
git2::ResetType::Soft,
|
||||
None,
|
||||
)
|
||||
.reset(integration_commit.as_object(), git2::ResetType::Soft, None)
|
||||
.context("failed to reset to integration commit")?;
|
||||
|
||||
let branch_manager = ctx.branch_manager();
|
||||
@ -369,7 +360,7 @@ fn verify_head_is_clean(ctx: &CommandContext, perm: &mut WorktreeWritePermission
|
||||
|
||||
// rebasing the extra commits onto the new branch
|
||||
let vb_state = ctx.project().virtual_branches();
|
||||
extra_commits.reverse();
|
||||
// let mut head = new_branch.head;
|
||||
let mut head = new_branch.head;
|
||||
for commit in extra_commits {
|
||||
let new_branch_head = ctx
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::{collections::HashMap, path::PathBuf, vec};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use git2::Tree;
|
||||
use gitbutler_branch::{
|
||||
Branch, BranchCreateRequest, BranchId, BranchOwnershipClaims, OwnershipClaim,
|
||||
};
|
||||
@ -8,7 +9,6 @@ use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_diff::{diff_files_into_hunks, GitHunk, Hunk, HunkHash};
|
||||
use gitbutler_operating_modes::assure_open_workspace_mode;
|
||||
use gitbutler_project::access::WorktreeWritePermission;
|
||||
use gitbutler_repo::RepositoryExt;
|
||||
|
||||
use crate::{
|
||||
conflicts::RepoConflictsExt,
|
||||
@ -72,7 +72,12 @@ pub fn get_applied_status(
|
||||
.map(|branch| (branch.id, HashMap::new()))
|
||||
.collect();
|
||||
|
||||
let locks = compute_locks(ctx.repository(), &base_diffs, &virtual_branches)?;
|
||||
let vb_state = ctx.project().virtual_branches();
|
||||
let base_tree = ctx
|
||||
.repository()
|
||||
.find_commit(vb_state.get_default_target()?.sha)?
|
||||
.tree()?;
|
||||
let locks = compute_locks(ctx.repository(), &base_diffs, &virtual_branches, base_tree)?;
|
||||
|
||||
for branch in &mut virtual_branches {
|
||||
let old_claims = branch.ownership.claims.clone();
|
||||
@ -190,7 +195,6 @@ pub fn get_applied_status(
|
||||
|
||||
// write updated state if not resolving
|
||||
if !ctx.is_resolving() {
|
||||
let vb_state = ctx.project().virtual_branches();
|
||||
for (vbranch, files) in &mut hunks_by_branch {
|
||||
vbranch.tree = gitbutler_diff::write::hunks_onto_oid(ctx, &vbranch.head, files)?;
|
||||
vb_state
|
||||
@ -224,10 +228,8 @@ fn compute_locks(
|
||||
repository: &git2::Repository,
|
||||
unstaged_hunks_by_path: &HashMap<PathBuf, Vec<gitbutler_diff::GitHunk>>,
|
||||
virtual_branches: &[Branch],
|
||||
base_tree: Tree,
|
||||
) -> Result<HashMap<HunkHash, Vec<HunkLock>>> {
|
||||
// If we cant find the integration commit and subsequently the target commit, we can't find any locks
|
||||
let target_tree = repository.target_commit()?.tree()?;
|
||||
|
||||
let mut diff_opts = git2::DiffOptions::new();
|
||||
let opts = diff_opts
|
||||
.show_binary(true)
|
||||
@ -240,7 +242,7 @@ fn compute_locks(
|
||||
let commit = repository.find_commit(branch.head).ok()?;
|
||||
let tree = commit.tree().ok()?;
|
||||
let diff = repository
|
||||
.diff_tree_to_tree(Some(&target_tree), Some(&tree), Some(opts))
|
||||
.diff_tree_to_tree(Some(&base_tree), Some(&tree), Some(opts))
|
||||
.ok()?;
|
||||
let hunks_by_filepath =
|
||||
gitbutler_diff::hunks_by_filepath(Some(repository), &diff).ok()?;
|
||||
|
@ -8,19 +8,6 @@ git init remote
|
||||
git add . && git commit -m "init"
|
||||
)
|
||||
|
||||
git clone remote single-branch-no-vbranch
|
||||
|
||||
git clone remote single-branch-no-vbranch-one-commit
|
||||
(cd single-branch-no-vbranch-one-commit
|
||||
echo change >> file && git add . && git commit -m "local change"
|
||||
)
|
||||
|
||||
git clone remote single-branch-no-vbranch-multi-remote
|
||||
(cd single-branch-no-vbranch-multi-remote
|
||||
git remote add other-origin ../remote
|
||||
git fetch other-origin
|
||||
)
|
||||
|
||||
export GITBUTLER_CLI_DATA_DIR=./git/gitbutler/app-data
|
||||
git clone remote one-vbranch-on-integration
|
||||
(cd one-vbranch-on-integration
|
||||
|
@ -2,65 +2,6 @@ use anyhow::Result;
|
||||
use gitbutler_branch_actions::{list_branches, Author};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
|
||||
#[test]
|
||||
fn on_main_single_branch_no_vbranch() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("single-branch-no-vbranch")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "main", "short names are used");
|
||||
assert_eq!(branch.remotes, ["origin"]);
|
||||
assert_eq!(branch.virtual_branch, None);
|
||||
assert_eq!(branch.number_of_commits, 0);
|
||||
assert_eq!(
|
||||
branch.authors,
|
||||
[],
|
||||
"there is no local commit, so no authors are known"
|
||||
);
|
||||
assert!(branch.own_branch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_main_single_branch_no_vbranch_multiple_remotes() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("single-branch-no-vbranch-multi-remote")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "main");
|
||||
assert_eq!(branch.remotes, ["other-origin", "origin"]);
|
||||
assert_eq!(branch.virtual_branch, None);
|
||||
assert_eq!(branch.number_of_commits, 0);
|
||||
assert_eq!(branch.authors, []);
|
||||
assert!(branch.own_branch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_main_single_branch_no_vbranch_one_commit() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("single-branch-no-vbranch-one-commit")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "main");
|
||||
assert_eq!(branch.remotes, ["origin"]);
|
||||
assert_eq!(branch.virtual_branch, None);
|
||||
assert_eq!(
|
||||
branch.number_of_commits, 0,
|
||||
"local-only commits aren't detected"
|
||||
);
|
||||
assert_eq!(
|
||||
branch.authors,
|
||||
[],
|
||||
"and thus there is no ownership information"
|
||||
);
|
||||
assert!(branch.own_branch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_vbranch_on_integration() -> Result<()> {
|
||||
init_env();
|
||||
@ -86,7 +27,8 @@ fn one_vbranch_on_integration() -> Result<()> {
|
||||
#[test]
|
||||
fn one_vbranch_on_integration_one_commit() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("one-vbranch-on-integration-one-commit")?, None)?;
|
||||
let ctx = project_ctx("one-vbranch-on-integration-one-commit")?;
|
||||
let list = list_branches(&ctx, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
|
@ -26,11 +26,17 @@ pub struct ApiProject {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub repository_id: String,
|
||||
/// The "gitbuler data, i.e. oplog" URL
|
||||
pub git_url: String,
|
||||
/// The "project" git URL
|
||||
pub code_git_url: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
/// Determines if the project Operations log will be synched with the GitButHub
|
||||
pub sync: bool,
|
||||
/// Determines if the project code will be synched with the GitButHub
|
||||
#[serde(default)]
|
||||
pub sync_code: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
@ -93,8 +99,23 @@ pub struct Project {
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn is_sync_enabled(&self) -> bool {
|
||||
self.api.as_ref().map(|api| api.sync).unwrap_or_default()
|
||||
/// Determines if the project Operations log will be synched with the GitButHub
|
||||
pub fn oplog_sync_enabled(&self) -> bool {
|
||||
let has_url = self.api.as_ref().map(|api| api.git_url.clone()).is_some();
|
||||
self.api.as_ref().map(|api| api.sync).unwrap_or_default() && has_url
|
||||
}
|
||||
/// Determines if the project code will be synched with the GitButHub
|
||||
pub fn code_sync_enabled(&self) -> bool {
|
||||
let has_code_url = self
|
||||
.api
|
||||
.as_ref()
|
||||
.and_then(|api| api.code_git_url.clone())
|
||||
.is_some();
|
||||
self.api
|
||||
.as_ref()
|
||||
.map(|api| api.sync_code)
|
||||
.unwrap_or_default()
|
||||
&& has_code_url
|
||||
}
|
||||
|
||||
pub fn has_code_url(&self) -> bool {
|
||||
|
@ -32,8 +32,6 @@ pub trait RepositoryExt {
|
||||
fn in_memory_repo(&self) -> Result<git2::Repository>;
|
||||
/// 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>;
|
||||
|
||||
@ -154,10 +152,6 @@ impl RepositoryExt for git2::Repository {
|
||||
Ok(integration_ref.peel_to_commit()?)
|
||||
}
|
||||
|
||||
fn target_commit(&self) -> Result<git2::Commit<'_>> {
|
||||
Ok(self.integration_commit()?.parent(0)?)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn commit_with_signature(
|
||||
&self,
|
||||
|
@ -16,7 +16,8 @@ use gitbutler_url::Url;
|
||||
use gitbutler_user as users;
|
||||
use itertools::Itertools;
|
||||
|
||||
pub fn sync_with_gitbutler(
|
||||
/// Pushes the repository to the GitButler remote
|
||||
pub fn push_repo(
|
||||
ctx: &CommandContext,
|
||||
user: &users::User,
|
||||
projects: &projects::Controller,
|
||||
@ -43,18 +44,25 @@ pub fn sync_with_gitbutler(
|
||||
|
||||
// Push all refs
|
||||
push_all_refs(ctx, user, project.id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pushes the Oplog head to GitButler server
|
||||
pub fn push_oplog(ctx: &CommandContext, user: &users::User) -> Result<()> {
|
||||
// Push Oplog head
|
||||
let oplog_refspec = ctx
|
||||
.project()
|
||||
.oplog_head()?
|
||||
.map(|sha| format!("+{}:refs/gitbutler/oplog/oplog", sha));
|
||||
.map(|sha| format!("+{}:refs/gitbutler/oplog", sha));
|
||||
|
||||
if let Some(oplog_refspec) = oplog_refspec {
|
||||
let x = push_to_gitbutler_server(ctx, Some(user), &[&oplog_refspec]);
|
||||
println!("\n\n\nHERE: {:?}", x?);
|
||||
push_to_gitbutler_server(
|
||||
ctx,
|
||||
Some(user),
|
||||
&[&oplog_refspec],
|
||||
remote(ctx, RemoteKind::Oplog)?,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -80,11 +88,12 @@ fn push_target(
|
||||
"batches left to push",
|
||||
);
|
||||
|
||||
let remote = remote(ctx, RemoteKind::Code)?;
|
||||
let id_count = ids.len();
|
||||
for (idx, id) in ids.iter().enumerate().rev() {
|
||||
let refspec = format!("+{}:refs/push-tmp/{}", id, project_id);
|
||||
|
||||
push_to_gitbutler_server(ctx, Some(user), &[&refspec])?;
|
||||
push_to_gitbutler_server(ctx, Some(user), &[&refspec], remote.clone())?;
|
||||
update_project(projects, project_id, *id)?;
|
||||
|
||||
tracing::info!(
|
||||
@ -99,6 +108,7 @@ fn push_target(
|
||||
ctx,
|
||||
Some(user),
|
||||
&[&format!("+{}:refs/{}", default_target.sha, project_id)],
|
||||
remote.clone(),
|
||||
)?;
|
||||
|
||||
//TODO: remove push-tmp ref
|
||||
@ -168,7 +178,8 @@ fn push_all_refs(
|
||||
|
||||
let all_refs: Vec<_> = all_refs.iter().map(String::as_str).collect();
|
||||
|
||||
let anything_pushed = push_to_gitbutler_server(ctx, Some(user), &all_refs)?;
|
||||
let anything_pushed =
|
||||
push_to_gitbutler_server(ctx, Some(user), &all_refs, remote(ctx, RemoteKind::Code)?)?;
|
||||
if anything_pushed {
|
||||
tracing::info!(
|
||||
%project_id,
|
||||
@ -199,23 +210,9 @@ fn push_to_gitbutler_server(
|
||||
ctx: &CommandContext,
|
||||
user: Option<&users::User>,
|
||||
ref_specs: &[&str],
|
||||
mut remote: git2::Remote,
|
||||
) -> Result<bool> {
|
||||
let project = ctx.project();
|
||||
let url = project
|
||||
.api
|
||||
.as_ref()
|
||||
.context("api not set")?
|
||||
.code_git_url
|
||||
.as_ref()
|
||||
.context("code_git_url not set")?
|
||||
.as_str()
|
||||
.parse::<Url>()?;
|
||||
|
||||
tracing::debug!(
|
||||
project_id = %project.id,
|
||||
%url,
|
||||
"pushing code to gb repo",
|
||||
);
|
||||
|
||||
let user = user
|
||||
.context("need user to push to gitbutler")
|
||||
@ -243,8 +240,6 @@ fn push_to_gitbutler_server(
|
||||
let headers = &[auth_header.as_str()];
|
||||
push_options.custom_headers(headers);
|
||||
|
||||
let mut remote = ctx.repository().remote_anonymous(&url.to_string())?;
|
||||
|
||||
remote
|
||||
.push(ref_specs, Some(&mut push_options))
|
||||
.map_err(|err| match err.class() {
|
||||
@ -270,3 +265,24 @@ fn push_to_gitbutler_server(
|
||||
|
||||
Ok(total_objects_pushed > 0)
|
||||
}
|
||||
|
||||
enum RemoteKind {
|
||||
Code,
|
||||
Oplog,
|
||||
}
|
||||
fn remote(ctx: &CommandContext, kind: RemoteKind) -> Result<git2::Remote> {
|
||||
let api_project = ctx.project().api.as_ref().context("api not set")?;
|
||||
let url = match kind {
|
||||
RemoteKind::Code => {
|
||||
let url = api_project
|
||||
.code_git_url
|
||||
.as_ref()
|
||||
.context("code_git_url not set")?;
|
||||
url.as_str().parse::<Url>()
|
||||
}
|
||||
RemoteKind::Oplog => api_project.git_url.as_str().parse::<Url>(),
|
||||
}?;
|
||||
ctx.repository()
|
||||
.remote_anonymous(&url.to_string())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ use gitbutler_oplog::{
|
||||
use gitbutler_project as projects;
|
||||
use gitbutler_project::ProjectId;
|
||||
use gitbutler_reference::{LocalRefname, Refname};
|
||||
use gitbutler_sync::cloud::sync_with_gitbutler;
|
||||
use gitbutler_sync::cloud::{push_oplog, push_repo};
|
||||
use gitbutler_user as users;
|
||||
use tracing::instrument;
|
||||
|
||||
@ -204,11 +204,13 @@ impl Handler {
|
||||
.get(project_id)
|
||||
.context("failed to get project")?;
|
||||
|
||||
if project.is_sync_enabled() && project.has_code_url() {
|
||||
if let Some(user) = self.users.get_user()? {
|
||||
let repository = CommandContext::open(&project)
|
||||
.context("failed to open project repository for project")?;
|
||||
return sync_with_gitbutler(&repository, &user, &self.projects);
|
||||
if let Some(user) = self.users.get_user()? {
|
||||
let ctx = CommandContext::open(&project)?;
|
||||
if project.oplog_sync_enabled() {
|
||||
push_oplog(&ctx, &user)?;
|
||||
}
|
||||
if project.code_sync_enabled() {
|
||||
push_repo(&ctx, &user, &self.projects)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -1,11 +1,35 @@
|
||||
:root {
|
||||
--hunk-line-selected-bg: #60a5fa;
|
||||
--hunk-line-selected-border: #2563eb;
|
||||
|
||||
--override-addition-background: #e0fbf0;
|
||||
--override-addition-inner-diff-background: #b6edd6;
|
||||
--override-addition-counter-background: #c8f3e1;
|
||||
--override-addition-counter-text: #87a89a;
|
||||
--override-addition-counter-border: #aecbb7;
|
||||
|
||||
--override-deletion-background: #fff0f2;
|
||||
--override-deletion-inner-diff-background: #fdd2da;
|
||||
--override-deletion-counter-background: #fcdfe4;
|
||||
--override-deletion-counter-text: #b69292;
|
||||
--override-deletion-counter-border: #e3c1c1;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
--hunk-line-selected-bg: #044289;
|
||||
--hunk-line-selected-border: #005cc5;
|
||||
|
||||
--override-addition-background: #0e2f25;
|
||||
--override-addition-inner-diff-background: #075445;
|
||||
--override-addition-counter-background: #0c4538;
|
||||
--override-addition-counter-text: #689e88;
|
||||
--override-addition-counter-border: #2b6e53;
|
||||
|
||||
--override-deletion-background: #3c131b;
|
||||
--override-deletion-inner-diff-background: #78061c;
|
||||
--override-deletion-counter-background: #53131e;
|
||||
--override-deletion-counter-text: #b36773;
|
||||
--override-deletion-counter-border: #8e3c3c;
|
||||
}
|
||||
|
||||
.inner-diff {
|
||||
@ -14,28 +38,20 @@
|
||||
|
||||
.diff-line-marker-addition,
|
||||
.diff-line-addition {
|
||||
--override-addition-background-color: hsl(144deg 55% 49% / 20%);
|
||||
|
||||
background-color: var(--override-addition-background-color);
|
||||
background-color: var(--override-addition-background);
|
||||
}
|
||||
|
||||
.diff-line-marker-deletion,
|
||||
.diff-line-deletion {
|
||||
--override-deletion-background-color: rgba(220, 38, 38, 0.2);
|
||||
|
||||
background-color: var(--override-deletion-background-color);
|
||||
background-color: var(--override-deletion-background);
|
||||
}
|
||||
|
||||
.diff-line-addition .inner-diff {
|
||||
--override-addition-inner-diff-background-color: hsl(144deg 55% 49% / 60%);
|
||||
|
||||
background-color: var(--override-addition-inner-diff-background-color);
|
||||
background-color: var(--override-addition-inner-diff-background);
|
||||
}
|
||||
|
||||
.diff-line-deletion .inner-diff {
|
||||
--override-deletion-inner-diff-background-color: rgba(220, 38, 38, 0.3);
|
||||
|
||||
background-color: var(--override-deletion-inner-diff-background-color);
|
||||
background-color: var(--override-deletion-inner-diff-background);
|
||||
}
|
||||
|
||||
.diff-line-spacer {
|
||||
|
@ -44,6 +44,7 @@
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -60,13 +60,11 @@
|
||||
}
|
||||
|
||||
.token-inserted {
|
||||
color: #116329;
|
||||
background-color: #11632960;
|
||||
background-color: var(--override-addition-inner-diff-background);
|
||||
}
|
||||
|
||||
.token-deleted {
|
||||
color: #82071e;
|
||||
background-color: #82071e40;
|
||||
background-color: var(--override-deletion-inner-diff-background);
|
||||
}
|
||||
|
||||
.token-heading {
|
||||
@ -92,94 +90,86 @@
|
||||
|
||||
/* DARK */
|
||||
|
||||
.dark .token-variable {
|
||||
color: #79c0ff;
|
||||
}
|
||||
.dark {
|
||||
.token-variable {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.dark .token-property {
|
||||
color: #79c0ff;
|
||||
}
|
||||
.token-property {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.dark .token-type {
|
||||
color: #7ee787;
|
||||
}
|
||||
.token-type {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.dark .token-variable-special {
|
||||
color: #79c0ff;
|
||||
}
|
||||
.token-variable-special {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.dark .token-definition {
|
||||
color: #ffa657;
|
||||
}
|
||||
.token-definition {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
.dark .token-number {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
.token-number {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.dark .token-string {
|
||||
color: #79c0ff;
|
||||
}
|
||||
.token-string {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.dark .token-string-special {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
.token-string-special {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.dark .token-keyword {
|
||||
color: #ff7b72;
|
||||
}
|
||||
.token-keyword {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.dark .token-comment {
|
||||
color: #8b949e;
|
||||
}
|
||||
.token-comment {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.dark .token-meta {
|
||||
color: #ffa657;
|
||||
}
|
||||
.token-meta {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
.dark .token-invalid {
|
||||
color: #ffa198;
|
||||
}
|
||||
.token-invalid {
|
||||
color: #ffa198;
|
||||
}
|
||||
|
||||
.dark .token-tag {
|
||||
color: #7ee787;
|
||||
}
|
||||
.token-tag {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.dark .token-attribute {
|
||||
color: #e6edf3;
|
||||
}
|
||||
.token-attribute {
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.dark .token-attribute-value {
|
||||
color: var(--color-token-attribute-value);
|
||||
}
|
||||
.token-attribute-value {
|
||||
color: var(--color-token-attribute-value);
|
||||
}
|
||||
|
||||
.dark .token-inserted {
|
||||
color: #7ee787;
|
||||
background-color: #7ee78740;
|
||||
}
|
||||
.token-heading {
|
||||
color: var(--color-token-variable-special);
|
||||
}
|
||||
|
||||
.dark .token-deleted {
|
||||
color: #ffa198;
|
||||
background-color: #ffa19840;
|
||||
}
|
||||
.token-link {
|
||||
color: var(--color-token-variable-special);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dark .token-heading {
|
||||
color: var(--color-token-variable-special);
|
||||
}
|
||||
.token-strikethrough {
|
||||
text-decoration: strike-through;
|
||||
}
|
||||
|
||||
.dark .token-link {
|
||||
color: var(--color-token-variable-special);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.token-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dark .token-strikethrough {
|
||||
text-decoration: strike-through;
|
||||
}
|
||||
|
||||
.dark .token-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dark .token-emphasis {
|
||||
font-style: italic;
|
||||
.token-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user