Mergy merge

This commit is contained in:
Caleb Owens 2024-07-31 13:11:02 +02:00
commit 09de80f6f0
37 changed files with 385 additions and 547 deletions

View File

@ -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

View File

@ -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;

View File

@ -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) => {

View File

@ -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();
}}

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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>

View File

@ -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();
}}

View File

@ -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
) {}
}

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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
}}
>

View File

@ -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">

View File

@ -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>

View File

@ -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 {

View File

@ -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];
}
);

View 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();
}}

View File

@ -13,6 +13,7 @@ body {
color: var(--clr-text-1);
background-color: var(--clr-bg-2);
user-select: none;
}
/**

View File

@ -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;
}
}

View File

@ -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(

View File

@ -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())

View File

@ -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)

View File

@ -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

View File

@ -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()?;

View File

@ -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

View File

@ -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];

View File

@ -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 {

View File

@ -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,

View File

@ -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)
}

View File

@ -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(())

View File

@ -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 {

View File

@ -44,6 +44,7 @@
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
a {

View File

@ -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;
}
}
}