mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-18 14:31:30 +03:00
Make it possible to view remote commits using existing components
- we need a union type rather than using File | RemoteFile everywhere
This commit is contained in:
parent
a69b6d9279
commit
f5428dcec7
@ -4,8 +4,9 @@
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { projectMergeUpstreamWarningDismissed } from '$lib/config/config';
|
||||
import { tooltip } from '$lib/utils/tooltip';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { BaseBranch } from '$lib/vbranches/types';
|
||||
import type { BaseBranch, File, RemoteFile } from '$lib/vbranches/types';
|
||||
|
||||
export let base: BaseBranch;
|
||||
export let projectId: string;
|
||||
@ -15,6 +16,7 @@
|
||||
const mergeUpstreamWarningDismissed = projectMergeUpstreamWarningDismissed(
|
||||
branchController.projectId
|
||||
);
|
||||
const selectedFiles = writable<(File | RemoteFile)[]>([]);
|
||||
|
||||
let updateTargetModal: Modal;
|
||||
let mergeUpstreamWarningDismissedCheckbox = false;
|
||||
@ -52,6 +54,7 @@
|
||||
<CommitCard
|
||||
{commit}
|
||||
{projectId}
|
||||
{selectedFiles}
|
||||
commitUrl={base.commitUrl(commit.id)}
|
||||
{projectPath}
|
||||
{branchController}
|
||||
@ -76,6 +79,7 @@
|
||||
<CommitCard
|
||||
{commit}
|
||||
{projectId}
|
||||
{selectedFiles}
|
||||
commitUrl={base.commitUrl(commit.id)}
|
||||
{projectPath}
|
||||
{branchController}
|
||||
|
@ -28,7 +28,7 @@
|
||||
import type { BranchService } from '$lib/branches/service';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { BaseBranch, Branch, File } from '$lib/vbranches/types';
|
||||
import type { BaseBranch, Branch, File, RemoteFile } from '$lib/vbranches/types';
|
||||
|
||||
export let branch: Branch;
|
||||
export let isUnapplied = false;
|
||||
@ -245,6 +245,7 @@
|
||||
{branchService}
|
||||
{branchCount}
|
||||
{isUnapplied}
|
||||
{selectedFiles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,8 @@
|
||||
import type { BranchService } from '$lib/branches/service';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { BaseBranch, Branch } from '$lib/vbranches/types';
|
||||
import type { BaseBranch, Branch, File, RemoteFile } from '$lib/vbranches/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let project: Project;
|
||||
export let branch: Branch;
|
||||
@ -12,6 +13,7 @@
|
||||
export let githubService: GitHubService;
|
||||
export let branchController: BranchController;
|
||||
export let branchService: BranchService;
|
||||
export let selectedFiles: Writable<(File | RemoteFile)[]>;
|
||||
export let isUnapplied: boolean;
|
||||
export let branchCount: number;
|
||||
</script>
|
||||
@ -26,6 +28,7 @@
|
||||
{branchCount}
|
||||
{githubService}
|
||||
{isUnapplied}
|
||||
{selectedFiles}
|
||||
type="upstream"
|
||||
/>
|
||||
<CommitList
|
||||
@ -36,6 +39,7 @@
|
||||
{branchService}
|
||||
{githubService}
|
||||
{isUnapplied}
|
||||
{selectedFiles}
|
||||
type="local"
|
||||
/>
|
||||
<CommitList
|
||||
@ -46,6 +50,7 @@
|
||||
{branchService}
|
||||
{githubService}
|
||||
{isUnapplied}
|
||||
{selectedFiles}
|
||||
type="remote"
|
||||
/>
|
||||
<CommitList
|
||||
@ -56,6 +61,7 @@
|
||||
{branchService}
|
||||
{githubService}
|
||||
{isUnapplied}
|
||||
{selectedFiles}
|
||||
type="integrated"
|
||||
/>
|
||||
{/if}
|
||||
|
@ -1,10 +1,7 @@
|
||||
<script lang="ts">
|
||||
import BranchFilesHeader from './BranchFilesHeader.svelte';
|
||||
import BranchFilesList from './BranchFilesList.svelte';
|
||||
import FileTree from './FileTree.svelte';
|
||||
import Badge from '$lib/components/Badge.svelte';
|
||||
import Checkbox from '$lib/components/Checkbox.svelte';
|
||||
import Segment from '$lib/components/SegmentControl/Segment.svelte';
|
||||
import SegmentedControl from '$lib/components/SegmentControl/SegmentedControl.svelte';
|
||||
import { filesToFileTree } from '$lib/vbranches/filetree';
|
||||
import type { Ownership } from '$lib/vbranches/ownership';
|
||||
import type { Branch, File } from '$lib/vbranches/types';
|
||||
@ -17,39 +14,6 @@
|
||||
export let showCheckboxes = false;
|
||||
|
||||
let selectedListMode: string;
|
||||
|
||||
let headerElement: HTMLDivElement;
|
||||
|
||||
function isAllChecked(selectedOwnership: Ownership): boolean {
|
||||
return branch.files.every((f) =>
|
||||
f.hunks.every((h) => selectedOwnership.containsHunk(f.id, h.id))
|
||||
);
|
||||
}
|
||||
|
||||
$: checked = isAllChecked($selectedOwnership);
|
||||
|
||||
function isIndeterminate(selectedOwnership: Ownership): boolean {
|
||||
if (branch.files.length <= 1) return false;
|
||||
|
||||
let file = branch.files[0];
|
||||
let prev = selectedOwnership.containsHunk(file.id, ...file.hunkIds);
|
||||
for (let i = 1; i < branch.files.length; i++) {
|
||||
file = branch.files[i];
|
||||
const contained = selectedOwnership.containsHunk(file.id, ...file.hunkIds);
|
||||
if (contained != prev) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$: indeterminate = isIndeterminate($selectedOwnership);
|
||||
|
||||
function selectAll(selectedOwnership: Writable<Ownership>, files: File[]) {
|
||||
files.forEach((f) =>
|
||||
selectedOwnership.update((ownership) => ownership.addHunk(f.id, ...f.hunks.map((h) => h.id)))
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if branch.active && branch.conflicted}
|
||||
@ -64,38 +28,21 @@
|
||||
{/if}
|
||||
|
||||
<div class="branch-files" class:isUnapplied>
|
||||
<div class="header" bind:this={headerElement}>
|
||||
<div class="header__left">
|
||||
{#if showCheckboxes && selectedListMode == 'list' && branch.files.length > 1}
|
||||
<Checkbox
|
||||
small
|
||||
{checked}
|
||||
{indeterminate}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
selectAll(selectedOwnership, branch.files);
|
||||
} else {
|
||||
selectedOwnership.update((ownership) => ownership.clear());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div class="header__title text-base-13 text-semibold">
|
||||
<span>Changes</span>
|
||||
<Badge count={branch.files.length} />
|
||||
</div>
|
||||
</div>
|
||||
<SegmentedControl bind:selected={selectedListMode} selectedIndex={0}>
|
||||
<Segment id="list" icon="list-view" />
|
||||
<Segment id="tree" icon="tree-view" />
|
||||
</SegmentedControl>
|
||||
<div class="branch-files__header">
|
||||
<BranchFilesHeader
|
||||
files={branch.files}
|
||||
{selectedOwnership}
|
||||
{showCheckboxes}
|
||||
bind:selectedListMode
|
||||
/>
|
||||
</div>
|
||||
{#if branch.files.length > 0}
|
||||
<div class="scroll-container">
|
||||
<!-- TODO: This is an experiment in file sorting. Accept or reject! -->
|
||||
<div class="files-padding">
|
||||
{#if selectedListMode == 'list'}
|
||||
<BranchFilesList
|
||||
{branch}
|
||||
allowMultiple
|
||||
branchId={branch.id}
|
||||
files={branch.files}
|
||||
{selectedOwnership}
|
||||
{selectedFiles}
|
||||
{showCheckboxes}
|
||||
@ -103,6 +50,7 @@
|
||||
/>
|
||||
{:else}
|
||||
<FileTree
|
||||
allowMultiple
|
||||
node={filesToFileTree(branch.files)}
|
||||
{showCheckboxes}
|
||||
branchId={branch.id}
|
||||
@ -121,37 +69,21 @@
|
||||
flex: 1;
|
||||
background: var(--clr-theme-container-light);
|
||||
border-radius: var(--radius-m) var(--radius-m) 0 0;
|
||||
|
||||
&.isUnapplied {
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
}
|
||||
.scroll-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
padding-top: 0;
|
||||
padding-left: var(--space-12);
|
||||
padding-right: var(--space-12);
|
||||
padding-bottom: var(--space-16);
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.branch-files__header {
|
||||
padding-top: var(--space-12);
|
||||
padding-bottom: var(--space-12);
|
||||
padding-left: var(--space-20);
|
||||
padding-right: var(--space-12);
|
||||
border-color: var(--clr-theme-container-outline-light);
|
||||
}
|
||||
.header__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
color: var(--clr-theme-scale-ntrl-0);
|
||||
}
|
||||
.header__left {
|
||||
display: flex;
|
||||
gap: var(--space-10);
|
||||
.files-padding {
|
||||
padding-top: 0;
|
||||
padding-bottom: var(--space-12);
|
||||
padding-left: var(--space-12);
|
||||
padding-right: var(--space-12);
|
||||
}
|
||||
</style>
|
||||
|
88
gitbutler-ui/src/lib/components/BranchFilesHeader.svelte
Normal file
88
gitbutler-ui/src/lib/components/BranchFilesHeader.svelte
Normal file
@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import Badge from '$lib/components/Badge.svelte';
|
||||
import Checkbox from '$lib/components/Checkbox.svelte';
|
||||
import Segment from '$lib/components/SegmentControl/Segment.svelte';
|
||||
import SegmentedControl from '$lib/components/SegmentControl/SegmentedControl.svelte';
|
||||
import type { Ownership } from '$lib/vbranches/ownership';
|
||||
import type { File, RemoteFile } from '$lib/vbranches/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let files: (File | RemoteFile)[];
|
||||
export let selectedOwnership: Writable<Ownership>;
|
||||
export let showCheckboxes = false;
|
||||
|
||||
export let selectedListMode: string;
|
||||
|
||||
function selectAll(selectedOwnership: Writable<Ownership>, files: (File | RemoteFile)[]) {
|
||||
files.forEach((f) =>
|
||||
selectedOwnership.update((ownership) => ownership.addHunk(f.id, ...f.hunks.map((h) => h.id)))
|
||||
);
|
||||
}
|
||||
|
||||
function isAllChecked(selectedOwnership: Ownership): boolean {
|
||||
return files.every((f) => f.hunks.every((h) => selectedOwnership.containsHunk(f.id, h.id)));
|
||||
}
|
||||
|
||||
function isIndeterminate(selectedOwnership: Ownership): boolean {
|
||||
if (files.length <= 1) return false;
|
||||
|
||||
let file = files[0];
|
||||
let prev = selectedOwnership.containsHunk(file.id, ...file.hunkIds);
|
||||
for (let i = 1; i < files.length; i++) {
|
||||
file = files[i];
|
||||
const contained = selectedOwnership.containsHunk(file.id, ...file.hunkIds);
|
||||
if (contained != prev) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$: indeterminate = isIndeterminate($selectedOwnership);
|
||||
$: checked = isAllChecked($selectedOwnership);
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<div class="header__left">
|
||||
{#if showCheckboxes && selectedListMode == 'list' && files.length > 1}
|
||||
<Checkbox
|
||||
small
|
||||
{checked}
|
||||
{indeterminate}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
selectAll(selectedOwnership, files);
|
||||
} else {
|
||||
selectedOwnership.update((ownership) => ownership.clear());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div class="header__title text-base-13 text-semibold">
|
||||
<span>Changes</span>
|
||||
<Badge count={files.length} />
|
||||
</div>
|
||||
</div>
|
||||
<SegmentedControl bind:selected={selectedListMode} selectedIndex={0}>
|
||||
<Segment id="list" icon="list-view" />
|
||||
<Segment id="tree" icon="tree-view" />
|
||||
</SegmentedControl>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
color: var(--clr-theme-scale-ntrl-0);
|
||||
}
|
||||
.header__left {
|
||||
display: flex;
|
||||
gap: var(--space-10);
|
||||
}
|
||||
</style>
|
@ -2,21 +2,25 @@
|
||||
import FileListItem from './FileListItem.svelte';
|
||||
import { sortLikeFileTree } from '$lib/vbranches/filetree';
|
||||
import type { Ownership } from '$lib/vbranches/ownership';
|
||||
import type { Branch, File } from '$lib/vbranches/types';
|
||||
import type { File, RemoteFile } from '$lib/vbranches/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let branch: Branch;
|
||||
export let branchId: string;
|
||||
export let files: (File | RemoteFile)[];
|
||||
export let selectedOwnership: Writable<Ownership>;
|
||||
export let isUnapplied = false;
|
||||
export let showCheckboxes = false;
|
||||
export let selectedFiles: Writable<File[]>;
|
||||
export let selectedFiles: Writable<(File | RemoteFile)[]>;
|
||||
export let allowMultiple = false;
|
||||
|
||||
$: console.log(selectedFiles);
|
||||
</script>
|
||||
|
||||
{#each sortLikeFileTree(branch.files) as file (file.id)}
|
||||
{#each sortLikeFileTree(files) as file (file.id)}
|
||||
<FileListItem
|
||||
{file}
|
||||
{isUnapplied}
|
||||
branchId={branch.id}
|
||||
{branchId}
|
||||
{selectedOwnership}
|
||||
showCheckbox={showCheckboxes}
|
||||
{selectedFiles}
|
||||
@ -26,7 +30,7 @@
|
||||
selectedFiles.update((fileIds) => fileIds.filter((f) => f.id != file.id));
|
||||
} else if (isAlreadySelected) {
|
||||
$selectedFiles = [];
|
||||
} else if (e.shiftKey) {
|
||||
} else if (e.shiftKey && allowMultiple) {
|
||||
selectedFiles.update((files) => [file, ...files]);
|
||||
} else {
|
||||
$selectedFiles = [file];
|
||||
|
@ -2,13 +2,13 @@
|
||||
import BranchCard from './BranchCard.svelte';
|
||||
import FileCard from './FileCard.svelte';
|
||||
import { Ownership } from '$lib/vbranches/ownership';
|
||||
import { RemoteFile, type BaseBranch, type Branch, type File } from '$lib/vbranches/types';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import type { User, getCloudApiClient } from '$lib/backend/cloud';
|
||||
import type { Project } from '$lib/backend/projects';
|
||||
import type { BranchService } from '$lib/branches/service';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { BaseBranch, Branch, File } from '$lib/vbranches/types';
|
||||
|
||||
export let branch: Branch;
|
||||
export let isUnapplied = false;
|
||||
@ -29,8 +29,11 @@
|
||||
|
||||
let commitBoxOpen: Writable<boolean>;
|
||||
|
||||
function setSelected(files: File[], branch: Branch) {
|
||||
function setSelected(files: (File | RemoteFile)[], branch: Branch) {
|
||||
if (files.length == 0) return undefined;
|
||||
if (files.length == 1 && files[0] instanceof RemoteFile) return files[0];
|
||||
|
||||
// If regular file selected but not found in branch files then clear selection.
|
||||
const match = branch.files?.find((f) => files[0].id == f.id);
|
||||
if (!match) $selectedFiles = [];
|
||||
return match;
|
||||
|
@ -1,5 +1,8 @@
|
||||
<script lang="ts">
|
||||
import BranchFilesHeader from './BranchFilesHeader.svelte';
|
||||
import BranchFilesList from './BranchFilesList.svelte';
|
||||
import FileDiff from './FileDiff.svelte';
|
||||
import FileTree from './FileTree.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import Tag from '$lib/components/Tag.svelte';
|
||||
@ -7,9 +10,12 @@
|
||||
import { draggable } from '$lib/dragging/draggable';
|
||||
import { draggableCommit, nonDraggable } from '$lib/dragging/draggables';
|
||||
import { getVSIFileIcon } from '$lib/ext-icons';
|
||||
import { filesToFileTree } from '$lib/vbranches/filetree';
|
||||
import { Ownership } from '$lib/vbranches/ownership';
|
||||
import { listRemoteCommitFiles } from '$lib/vbranches/remoteCommits';
|
||||
import { RemoteCommit, Commit, RemoteFile } from '$lib/vbranches/types';
|
||||
import { File, RemoteCommit, Commit, RemoteFile } from '$lib/vbranches/types';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import type { ContentSection, HunkSection } from '$lib/utils/fileSections';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
|
||||
@ -21,8 +27,13 @@
|
||||
export let isUnapplied = false;
|
||||
export let branchController: BranchController;
|
||||
export let projectPath: string;
|
||||
export let selectedFiles: Writable<(File | RemoteFile)[]>;
|
||||
|
||||
const selectedOwnership = writable(Ownership.default());
|
||||
|
||||
let previewCommitModal: Modal;
|
||||
let showFiles = false;
|
||||
let selectedListMode: string;
|
||||
|
||||
let entries: [RemoteFile, (ContentSection | HunkSection)[]][] = [];
|
||||
let isLoading = false;
|
||||
@ -33,57 +44,88 @@
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
$: files = entries.map((entry) => entry[0]);
|
||||
|
||||
function onClick() {
|
||||
loadEntries();
|
||||
previewCommitModal.show();
|
||||
showFiles = !showFiles;
|
||||
// previewCommitModal.show();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
on:click={onClick}
|
||||
on:keyup={onClick}
|
||||
use:draggable={commit instanceof Commit
|
||||
? draggableCommit(commit.branchId, commit)
|
||||
: nonDraggable()}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="commit__card" class:is-head-commit={isHeadCommit}>
|
||||
<div class="commit__header">
|
||||
<span class="commit__description text-base-12 truncate">
|
||||
{commit.description}
|
||||
</span>
|
||||
{#if isHeadCommit && !isUnapplied}
|
||||
<Tag
|
||||
color="ghost"
|
||||
icon="undo-small"
|
||||
border
|
||||
clickable
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
resetHeadCommit();
|
||||
}}>Undo</Tag
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="commit__details">
|
||||
<div class="commit__author">
|
||||
<img
|
||||
class="commit__avatar"
|
||||
title="Gravatar for {commit.author.email}"
|
||||
alt="Gravatar for {commit.author.email}"
|
||||
srcset="{commit.author.gravatarUrl} 2x"
|
||||
width="100"
|
||||
height="100"
|
||||
on:error
|
||||
/>
|
||||
<span class="commit__author-name text-base-12 truncate">{commit.author.name}</span>
|
||||
<div on:click={onClick} on:keyup={onClick} role="button" tabindex="0">
|
||||
<div class="commit__header">
|
||||
<span class="commit__description text-base-12 truncate">
|
||||
{commit.description}
|
||||
</span>
|
||||
{#if isHeadCommit && !isUnapplied}
|
||||
<Tag
|
||||
color="ghost"
|
||||
icon="undo-small"
|
||||
border
|
||||
clickable
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
resetHeadCommit();
|
||||
}}>Undo</Tag
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="commit__details">
|
||||
<div class="commit__author">
|
||||
<img
|
||||
class="commit__avatar"
|
||||
title="Gravatar for {commit.author.email}"
|
||||
alt="Gravatar for {commit.author.email}"
|
||||
srcset="{commit.author.gravatarUrl} 2x"
|
||||
width="100"
|
||||
height="100"
|
||||
on:error
|
||||
/>
|
||||
<span class="commit__author-name text-base-12 truncate">{commit.author.name}</span>
|
||||
</div>
|
||||
<span class="commit__time text-base-11">
|
||||
<TimeAgo date={commit.createdAt} />
|
||||
</span>
|
||||
</div>
|
||||
<span class="commit__time text-base-11">
|
||||
<TimeAgo date={commit.createdAt} />
|
||||
</span>
|
||||
</div>
|
||||
{#if showFiles}
|
||||
<div class="commit__files-header">
|
||||
<BranchFilesHeader
|
||||
{files}
|
||||
{selectedOwnership}
|
||||
showCheckboxes={false}
|
||||
bind:selectedListMode
|
||||
/>
|
||||
</div>
|
||||
<div class="commit__files">
|
||||
{#if selectedListMode == 'list'}
|
||||
<BranchFilesList
|
||||
branchId="blah"
|
||||
{files}
|
||||
{selectedOwnership}
|
||||
{selectedFiles}
|
||||
{isUnapplied}
|
||||
/>
|
||||
{:else}
|
||||
<FileTree
|
||||
node={filesToFileTree(files)}
|
||||
branchId="blah"
|
||||
isRoot={true}
|
||||
{selectedOwnership}
|
||||
{selectedFiles}
|
||||
{isUnapplied}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -168,7 +210,6 @@
|
||||
flex-direction: column;
|
||||
cursor: default;
|
||||
gap: var(--space-10);
|
||||
padding: var(--space-12);
|
||||
border-radius: var(--space-6);
|
||||
background-color: var(--clr-theme-container-light);
|
||||
border: 1px solid var(--clr-theme-container-outline-light);
|
||||
@ -183,6 +224,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-8);
|
||||
padding: var(--space-12) var(--space-12) 0 var(--space-12);
|
||||
}
|
||||
|
||||
.commit__description {
|
||||
@ -197,6 +239,22 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-8);
|
||||
padding: 0 var(--space-12) var(--space-12) var(--space-12);
|
||||
}
|
||||
|
||||
.commit__files {
|
||||
padding-top: 0;
|
||||
padding-left: var(--space-12);
|
||||
padding-right: var(--space-12);
|
||||
padding-bottom: var(--space-12);
|
||||
}
|
||||
|
||||
.commit__files-header {
|
||||
border-top: 1px solid var(--clr-theme-container-outline-light);
|
||||
padding-top: var(--space-12);
|
||||
padding-bottom: var(--space-12);
|
||||
padding-left: var(--space-20);
|
||||
padding-right: var(--space-12);
|
||||
}
|
||||
|
||||
.commit__author {
|
||||
|
@ -6,7 +6,8 @@
|
||||
import type { BranchService } from '$lib/branches/service';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { BaseBranch, Branch, CommitStatus } from '$lib/vbranches/types';
|
||||
import type { BaseBranch, Branch, CommitStatus, File, RemoteFile } from '$lib/vbranches/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let branch: Branch;
|
||||
export let base: BaseBranch | undefined | null;
|
||||
@ -15,6 +16,7 @@
|
||||
export let type: CommitStatus;
|
||||
export let githubService: GitHubService;
|
||||
export let branchService: BranchService;
|
||||
export let selectedFiles: Writable<(File | RemoteFile)[]>;
|
||||
export let isUnapplied: boolean;
|
||||
export let branchCount: number = 0;
|
||||
|
||||
@ -47,6 +49,7 @@
|
||||
{base}
|
||||
{project}
|
||||
{isUnapplied}
|
||||
{selectedFiles}
|
||||
isChained={idx != commits.length - 1}
|
||||
isHeadCommit={commit.id === headCommit?.id}
|
||||
/>
|
||||
|
@ -11,8 +11,15 @@
|
||||
} from '$lib/dragging/draggables';
|
||||
import { dropzone } from '$lib/dragging/dropzone';
|
||||
import { filesToOwnership } from '$lib/vbranches/ownership';
|
||||
import { RemoteCommit, type BaseBranch, type Branch, type Commit } from '$lib/vbranches/types';
|
||||
import { get } from 'svelte/store';
|
||||
import {
|
||||
RemoteCommit,
|
||||
type BaseBranch,
|
||||
type Branch,
|
||||
type Commit,
|
||||
type File,
|
||||
RemoteFile
|
||||
} from '$lib/vbranches/types';
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
import type { Project } from '$lib/backend/projects';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
|
||||
@ -24,6 +31,7 @@
|
||||
export let isChained: boolean;
|
||||
export let isUnapplied = false;
|
||||
export let branchController: BranchController;
|
||||
export let selectedFiles: Writable<(File | RemoteFile)[]>;
|
||||
|
||||
function acceptAmend(commit: Commit | RemoteCommit) {
|
||||
if (commit instanceof RemoteCommit) {
|
||||
@ -139,6 +147,7 @@
|
||||
{resetHeadCommit}
|
||||
{isUnapplied}
|
||||
{branchController}
|
||||
{selectedFiles}
|
||||
projectPath={project.path}
|
||||
/>
|
||||
</div>
|
||||
|
@ -12,12 +12,12 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { Ownership } from '$lib/vbranches/ownership';
|
||||
import type { File } from '$lib/vbranches/types';
|
||||
import type { File, RemoteFile } from '$lib/vbranches/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let projectId: string;
|
||||
export let branchId: string;
|
||||
export let file: File;
|
||||
export let file: File | RemoteFile;
|
||||
export let conflicted: boolean;
|
||||
export let projectPath: string | undefined;
|
||||
export let branchController: BranchController;
|
||||
@ -35,7 +35,7 @@
|
||||
|
||||
let sections: (HunkSection | ContentSection)[] = [];
|
||||
|
||||
function parseFile(file: File) {
|
||||
function parseFile(file: File | RemoteFile) {
|
||||
// When we toggle expansion status on sections we need to assign
|
||||
// `sections = sections` to redraw, and why we do not use a reactive
|
||||
// variable.
|
||||
|
@ -6,9 +6,9 @@
|
||||
import { computeFileStatus } from '$lib/utils/fileStatus';
|
||||
import { computeAddedRemovedByFiles } from '$lib/utils/metrics';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { File } from '$lib/vbranches/types';
|
||||
import type { File, RemoteFile } from '$lib/vbranches/types';
|
||||
|
||||
export let file: File;
|
||||
export let file: File | RemoteFile;
|
||||
export let isFileLocked: boolean;
|
||||
|
||||
const dispatch = createEventDispatcher<{ close: void }>();
|
||||
|
@ -5,16 +5,16 @@
|
||||
import { draggableFile } from '$lib/dragging/draggables';
|
||||
import { getVSIFileIcon } from '$lib/ext-icons';
|
||||
import type { Ownership } from '$lib/vbranches/ownership';
|
||||
import type { File } from '$lib/vbranches/types';
|
||||
import type { File, RemoteFile } from '$lib/vbranches/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let branchId: string;
|
||||
export let file: File;
|
||||
export let file: File | RemoteFile;
|
||||
export let isUnapplied: boolean;
|
||||
export let selected: boolean;
|
||||
export let showCheckbox: boolean = false;
|
||||
export let selectedOwnership: Writable<Ownership>;
|
||||
export let selectedFiles: Writable<File[]>;
|
||||
export let selectedFiles: Writable<(File | RemoteFile)[]>;
|
||||
|
||||
let checked = false;
|
||||
let indeterminate = false;
|
||||
|
@ -2,9 +2,9 @@
|
||||
import FileStatusCircle from './FileStatusCircle.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { computeFileStatus } from '$lib/utils/fileStatus';
|
||||
import type { File } from '$lib/vbranches/types';
|
||||
import type { File, RemoteFile } from '$lib/vbranches/types';
|
||||
|
||||
export let file: File;
|
||||
export let file: File | RemoteFile;
|
||||
$: isLocked = file.hunks.some((h) => h.locked);
|
||||
</script>
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
import TreeListFolder from './TreeListFolder.svelte';
|
||||
import type { TreeNode } from '$lib/vbranches/filetree';
|
||||
import type { Ownership } from '$lib/vbranches/ownership';
|
||||
import type { File } from '$lib/vbranches/types';
|
||||
import type { File, RemoteFile } from '$lib/vbranches/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let expanded = true;
|
||||
@ -15,9 +15,10 @@
|
||||
export let isRoot = false;
|
||||
export let showCheckboxes = false;
|
||||
export let selectedOwnership: Writable<Ownership>;
|
||||
export let selectedFiles: Writable<File[]>;
|
||||
export let selectedFiles: Writable<(File | RemoteFile)[]>;
|
||||
export let branchId: string;
|
||||
export let isUnapplied: boolean;
|
||||
export let allowMultiple = false;
|
||||
|
||||
function isNodeChecked(selectedOwnership: Ownership, node: TreeNode): boolean {
|
||||
if (node.file) {
|
||||
@ -91,12 +92,13 @@
|
||||
{selectedFiles}
|
||||
showCheckbox={showCheckboxes}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
const isAlreadySelected = $selectedFiles.includes(file);
|
||||
if (isAlreadySelected && e.shiftKey) {
|
||||
selectedFiles.update((fileIds) => fileIds.filter((f) => f.id != file.id));
|
||||
} else if (isAlreadySelected) {
|
||||
$selectedFiles = [];
|
||||
} else if (e.shiftKey) {
|
||||
} else if (e.shiftKey && allowMultiple) {
|
||||
selectedFiles.update((files) => [file, ...files]);
|
||||
} else {
|
||||
$selectedFiles = [file];
|
||||
|
@ -2,12 +2,15 @@
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import CommitCard from '$lib/components/CommitCard.svelte';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { RemoteBranch } from '$lib/vbranches/types';
|
||||
import type { File, RemoteBranch, RemoteFile } from '$lib/vbranches/types';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let branch: RemoteBranch | undefined;
|
||||
export let projectId: string;
|
||||
export let projectPath: string;
|
||||
export let branchController: BranchController;
|
||||
|
||||
const selectedFiles = writable<(File | RemoteFile)[]>([]);
|
||||
</script>
|
||||
|
||||
{#if branch != undefined}
|
||||
@ -24,7 +27,7 @@
|
||||
{#if branch.commits && branch.commits.length > 0}
|
||||
<div class="flex w-full flex-col gap-y-2">
|
||||
{#each branch.commits as commit}
|
||||
<CommitCard {commit} {projectId} {branchController} {projectPath} />
|
||||
<CommitCard {commit} {projectId} {branchController} {projectPath} {selectedFiles} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -5,16 +5,16 @@
|
||||
import { draggableFile } from '$lib/dragging/draggables';
|
||||
import { getVSIFileIcon } from '$lib/ext-icons';
|
||||
import type { Ownership } from '$lib/vbranches/ownership';
|
||||
import type { File } from '$lib/vbranches/types';
|
||||
import type { File, RemoteFile } from '$lib/vbranches/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let branchId: string;
|
||||
export let file: File;
|
||||
export let file: File | RemoteFile;
|
||||
export let selected: boolean;
|
||||
export let isUnapplied: boolean;
|
||||
export let showCheckbox: boolean = false;
|
||||
export let selectedOwnership: Writable<Ownership>;
|
||||
export let selectedFiles: Writable<File[]>;
|
||||
export let selectedFiles: Writable<(File | RemoteFile)[]>;
|
||||
|
||||
let checked = false;
|
||||
let indeterminate = false;
|
||||
@ -103,16 +103,19 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-10);
|
||||
overflow: hidden;
|
||||
}
|
||||
.name-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-6);
|
||||
overflow: hidden;
|
||||
}
|
||||
.name {
|
||||
color: var(--clr-theme-scale-ntrl-0);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.selected {
|
||||
background-color: var(--clr-theme-scale-pop-80);
|
||||
|
@ -1,77 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CommitCard from './CommitCard.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import { draggable } from '$lib/dragging/draggable';
|
||||
import { draggableRemoteCommit } from '$lib/dragging/draggables';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { BaseBranch, RemoteBranch } from '$lib/vbranches/types';
|
||||
|
||||
export let branchId: string;
|
||||
export let projectId: string;
|
||||
export let projectPath: string;
|
||||
export let branchCount: number;
|
||||
export let upstream: RemoteBranch | undefined;
|
||||
export let branchController: BranchController;
|
||||
export let base: BaseBranch | undefined | null;
|
||||
|
||||
let upstreamCommitsShown = false;
|
||||
|
||||
$: if (upstreamCommitsShown && upstream?.commits.length === 0) {
|
||||
upstreamCommitsShown = false;
|
||||
}
|
||||
|
||||
function merge() {
|
||||
branchController.mergeUpstream(branchId);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if upstream}
|
||||
<div class="bg-zinc-300 p-2 dark:bg-zinc-800">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="p-1 text-purple-700">
|
||||
{upstream.commits.length}
|
||||
upstream {upstream.commits.length > 1 ? 'commits' : 'commit'}
|
||||
</div>
|
||||
<Button
|
||||
kind="outlined"
|
||||
color="primary"
|
||||
on:click={() => (upstreamCommitsShown = !upstreamCommitsShown)}
|
||||
>
|
||||
<span class="purple">
|
||||
{#if !upstreamCommitsShown}
|
||||
View
|
||||
{:else}
|
||||
Cancel
|
||||
{/if}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if upstreamCommitsShown}
|
||||
<div
|
||||
class="flex w-full flex-col gap-1 border-t border-light-400 bg-light-300 p-2 dark:border-dark-400 dark:bg-dark-800"
|
||||
id="upstreamCommits"
|
||||
>
|
||||
{#each upstream.commits as commit (commit.id)}
|
||||
<div use:draggable={draggableRemoteCommit(branchId, commit)}>
|
||||
<CommitCard
|
||||
{commit}
|
||||
{projectId}
|
||||
commitUrl={base?.commitUrl(commit.id)}
|
||||
{branchController}
|
||||
{projectPath}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex justify-end p-2">
|
||||
{#if branchCount > 1}
|
||||
<div class="px-2 text-sm">
|
||||
You have {branchCount} active branches. To merge upstream work, we will unapply all other
|
||||
branches.
|
||||
</div>
|
||||
{/if}
|
||||
<Button color="primary" on:click={merge}>Merge</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
@ -1,4 +1,4 @@
|
||||
import type { Commit, File, Hunk, RemoteCommit } from '../vbranches/types';
|
||||
import type { Commit, File, Hunk, RemoteCommit, RemoteFile } from '../vbranches/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export function nonDraggable() {
|
||||
@ -23,11 +23,15 @@ export function isDraggableHunk(obj: any): obj is DraggableHunk {
|
||||
|
||||
export type DraggableFile = {
|
||||
branchId: string;
|
||||
files: Writable<File[]>;
|
||||
files: Writable<(File | RemoteFile)[]>;
|
||||
current: File;
|
||||
};
|
||||
|
||||
export function draggableFile(branchId: string, current: File, files: Writable<File[]>) {
|
||||
export function draggableFile(
|
||||
branchId: string,
|
||||
current: File | RemoteFile,
|
||||
files: Writable<(File | RemoteFile)[]>
|
||||
) {
|
||||
return { data: { branchId, current, files } };
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,12 @@
|
||||
import type { File } from '$lib/vbranches/types';
|
||||
import { RemoteFile, type File } from '$lib/vbranches/types';
|
||||
|
||||
export type FileStatus = 'A' | 'M' | 'D';
|
||||
|
||||
export function computeFileStatus(file: File): FileStatus {
|
||||
export function computeFileStatus(file: File | RemoteFile): FileStatus {
|
||||
if (file instanceof RemoteFile) {
|
||||
// TODO: How do we compute this for remote files?
|
||||
return 'M';
|
||||
}
|
||||
if (file.hunks.length == 1) {
|
||||
const changeType = file.hunks[0].changeType;
|
||||
if (changeType == 'added') {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { HunkSection, type ContentSection } from './fileSections';
|
||||
import type { File } from '$lib/vbranches/types';
|
||||
import type { File, RemoteFile } from '$lib/vbranches/types';
|
||||
|
||||
export function computeAddedRemovedByFiles(...files: File[]) {
|
||||
export function computeAddedRemovedByFiles(...files: (File | RemoteFile)[]) {
|
||||
return files
|
||||
.flatMap((f) => f.hunks)
|
||||
.map((h) => h.diff.split('\n'))
|
||||
|
13
gitbutler-ui/src/lib/utils/string.ts
Normal file
13
gitbutler-ui/src/lib/utils/string.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export function hashCode(s: string) {
|
||||
let hash = 0;
|
||||
let chr;
|
||||
let i;
|
||||
|
||||
if (s.length === 0) return hash.toString();
|
||||
for (i = 0; i < s.length; i++) {
|
||||
chr = s.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash.toString();
|
||||
}
|
@ -4,11 +4,11 @@
|
||||
* This module provides support for tranforming a list of files into a
|
||||
* hirerarchical structure for easy rendering.
|
||||
*/
|
||||
import type { File } from './types';
|
||||
import type { File, RemoteFile } from './types';
|
||||
|
||||
export interface TreeNode {
|
||||
name: string;
|
||||
file?: File;
|
||||
file?: File | RemoteFile;
|
||||
children: TreeNode[];
|
||||
parent?: TreeNode;
|
||||
}
|
||||
@ -40,7 +40,7 @@ export function sortChildren(node: TreeNode) {
|
||||
}
|
||||
}
|
||||
|
||||
export function filesToFileTree(files: File[]): TreeNode {
|
||||
export function filesToFileTree(files: (File | RemoteFile)[]): TreeNode {
|
||||
const acc: TreeNode = { name: 'root', children: [] };
|
||||
files.forEach((f) => {
|
||||
const pathParts = f.path.split('/');
|
||||
@ -51,8 +51,8 @@ export function filesToFileTree(files: File[]): TreeNode {
|
||||
return acc;
|
||||
}
|
||||
|
||||
function fileTreeToList(node: TreeNode): File[] {
|
||||
const list: File[] = [];
|
||||
function fileTreeToList(node: TreeNode): (File | RemoteFile)[] {
|
||||
const list: (File | RemoteFile)[] = [];
|
||||
if (node.file) list.push(node.file);
|
||||
node.children.forEach((child) => {
|
||||
list.push(...fileTreeToList(child));
|
||||
@ -61,6 +61,6 @@ function fileTreeToList(node: TreeNode): File[] {
|
||||
}
|
||||
|
||||
// Sorts a file list the same way it is sorted in a file tree
|
||||
export function sortLikeFileTree(files: File[]): File[] {
|
||||
export function sortLikeFileTree(files: (File | RemoteFile)[]): (File | RemoteFile)[] {
|
||||
return fileTreeToList(filesToFileTree(files));
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { Branch, File } from './types';
|
||||
import type { Branch, File, RemoteFile } from './types';
|
||||
|
||||
export function filesToOwnership(files: File[]) {
|
||||
export function filesToOwnership(files: (File | RemoteFile)[]) {
|
||||
return files.map((f) => `${f.path}:${f.hunks.map(({ id }) => id).join(',')}`).join('\n');
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'reflect-metadata';
|
||||
import { Type, Transform } from 'class-transformer';
|
||||
import { hashCode } from '$lib/utils/string';
|
||||
|
||||
export type ChangeType =
|
||||
/// Entry does not exist in old version
|
||||
@ -130,6 +131,14 @@ export class RemoteCommit {
|
||||
|
||||
export class RemoteHunk {
|
||||
diff!: string;
|
||||
|
||||
get id(): string {
|
||||
return hashCode(this.diff);
|
||||
}
|
||||
|
||||
get locked() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteFile {
|
||||
@ -137,6 +146,34 @@ export class RemoteFile {
|
||||
@Type(() => RemoteHunk)
|
||||
hunks!: RemoteHunk[];
|
||||
binary!: boolean;
|
||||
|
||||
get id(): string {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
get filename(): string {
|
||||
return this.path.replace(/^.*[\\/]/, '');
|
||||
}
|
||||
|
||||
get justpath() {
|
||||
return this.path.split('/').slice(0, -1).join('/');
|
||||
}
|
||||
|
||||
get locked() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get large() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get conflicted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get hunkIds() {
|
||||
return this.hunks.map((h) => h.id);
|
||||
}
|
||||
}
|
||||
|
||||
export interface Author {
|
||||
|
Loading…
Reference in New Issue
Block a user