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:
Mattias Granlund 2024-02-01 10:30:14 +01:00
parent a69b6d9279
commit f5428dcec7
25 changed files with 349 additions and 252 deletions

View File

@ -4,8 +4,9 @@
import Modal from '$lib/components/Modal.svelte'; import Modal from '$lib/components/Modal.svelte';
import { projectMergeUpstreamWarningDismissed } from '$lib/config/config'; import { projectMergeUpstreamWarningDismissed } from '$lib/config/config';
import { tooltip } from '$lib/utils/tooltip'; import { tooltip } from '$lib/utils/tooltip';
import { writable } from 'svelte/store';
import type { BranchController } from '$lib/vbranches/branchController'; 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 base: BaseBranch;
export let projectId: string; export let projectId: string;
@ -15,6 +16,7 @@
const mergeUpstreamWarningDismissed = projectMergeUpstreamWarningDismissed( const mergeUpstreamWarningDismissed = projectMergeUpstreamWarningDismissed(
branchController.projectId branchController.projectId
); );
const selectedFiles = writable<(File | RemoteFile)[]>([]);
let updateTargetModal: Modal; let updateTargetModal: Modal;
let mergeUpstreamWarningDismissedCheckbox = false; let mergeUpstreamWarningDismissedCheckbox = false;
@ -52,6 +54,7 @@
<CommitCard <CommitCard
{commit} {commit}
{projectId} {projectId}
{selectedFiles}
commitUrl={base.commitUrl(commit.id)} commitUrl={base.commitUrl(commit.id)}
{projectPath} {projectPath}
{branchController} {branchController}
@ -76,6 +79,7 @@
<CommitCard <CommitCard
{commit} {commit}
{projectId} {projectId}
{selectedFiles}
commitUrl={base.commitUrl(commit.id)} commitUrl={base.commitUrl(commit.id)}
{projectPath} {projectPath}
{branchController} {branchController}

View File

@ -28,7 +28,7 @@
import type { BranchService } from '$lib/branches/service'; import type { BranchService } from '$lib/branches/service';
import type { GitHubService } from '$lib/github/service'; import type { GitHubService } from '$lib/github/service';
import type { BranchController } from '$lib/vbranches/branchController'; 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 branch: Branch;
export let isUnapplied = false; export let isUnapplied = false;
@ -245,6 +245,7 @@
{branchService} {branchService}
{branchCount} {branchCount}
{isUnapplied} {isUnapplied}
{selectedFiles}
/> />
</div> </div>
</div> </div>

View File

@ -4,7 +4,8 @@
import type { BranchService } from '$lib/branches/service'; import type { BranchService } from '$lib/branches/service';
import type { GitHubService } from '$lib/github/service'; import type { GitHubService } from '$lib/github/service';
import type { BranchController } from '$lib/vbranches/branchController'; 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 project: Project;
export let branch: Branch; export let branch: Branch;
@ -12,6 +13,7 @@
export let githubService: GitHubService; export let githubService: GitHubService;
export let branchController: BranchController; export let branchController: BranchController;
export let branchService: BranchService; export let branchService: BranchService;
export let selectedFiles: Writable<(File | RemoteFile)[]>;
export let isUnapplied: boolean; export let isUnapplied: boolean;
export let branchCount: number; export let branchCount: number;
</script> </script>
@ -26,6 +28,7 @@
{branchCount} {branchCount}
{githubService} {githubService}
{isUnapplied} {isUnapplied}
{selectedFiles}
type="upstream" type="upstream"
/> />
<CommitList <CommitList
@ -36,6 +39,7 @@
{branchService} {branchService}
{githubService} {githubService}
{isUnapplied} {isUnapplied}
{selectedFiles}
type="local" type="local"
/> />
<CommitList <CommitList
@ -46,6 +50,7 @@
{branchService} {branchService}
{githubService} {githubService}
{isUnapplied} {isUnapplied}
{selectedFiles}
type="remote" type="remote"
/> />
<CommitList <CommitList
@ -56,6 +61,7 @@
{branchService} {branchService}
{githubService} {githubService}
{isUnapplied} {isUnapplied}
{selectedFiles}
type="integrated" type="integrated"
/> />
{/if} {/if}

View File

@ -1,10 +1,7 @@
<script lang="ts"> <script lang="ts">
import BranchFilesHeader from './BranchFilesHeader.svelte';
import BranchFilesList from './BranchFilesList.svelte'; import BranchFilesList from './BranchFilesList.svelte';
import FileTree from './FileTree.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 { filesToFileTree } from '$lib/vbranches/filetree';
import type { Ownership } from '$lib/vbranches/ownership'; import type { Ownership } from '$lib/vbranches/ownership';
import type { Branch, File } from '$lib/vbranches/types'; import type { Branch, File } from '$lib/vbranches/types';
@ -17,39 +14,6 @@
export let showCheckboxes = false; export let showCheckboxes = false;
let selectedListMode: string; 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> </script>
{#if branch.active && branch.conflicted} {#if branch.active && branch.conflicted}
@ -64,38 +28,21 @@
{/if} {/if}
<div class="branch-files" class:isUnapplied> <div class="branch-files" class:isUnapplied>
<div class="header" bind:this={headerElement}> <div class="branch-files__header">
<div class="header__left"> <BranchFilesHeader
{#if showCheckboxes && selectedListMode == 'list' && branch.files.length > 1} files={branch.files}
<Checkbox {selectedOwnership}
small {showCheckboxes}
{checked} bind:selectedListMode
{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> </div>
{#if branch.files.length > 0} {#if branch.files.length > 0}
<div class="scroll-container"> <div class="files-padding">
<!-- TODO: This is an experiment in file sorting. Accept or reject! -->
{#if selectedListMode == 'list'} {#if selectedListMode == 'list'}
<BranchFilesList <BranchFilesList
{branch} allowMultiple
branchId={branch.id}
files={branch.files}
{selectedOwnership} {selectedOwnership}
{selectedFiles} {selectedFiles}
{showCheckboxes} {showCheckboxes}
@ -103,6 +50,7 @@
/> />
{:else} {:else}
<FileTree <FileTree
allowMultiple
node={filesToFileTree(branch.files)} node={filesToFileTree(branch.files)}
{showCheckboxes} {showCheckboxes}
branchId={branch.id} branchId={branch.id}
@ -121,37 +69,21 @@
flex: 1; flex: 1;
background: var(--clr-theme-container-light); background: var(--clr-theme-container-light);
border-radius: var(--radius-m) var(--radius-m) 0 0; border-radius: var(--radius-m) var(--radius-m) 0 0;
&.isUnapplied { &.isUnapplied {
border-radius: var(--radius-m); border-radius: var(--radius-m);
} }
} }
.scroll-container { .branch-files__header {
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;
padding-top: var(--space-12); padding-top: var(--space-12);
padding-bottom: var(--space-12); padding-bottom: var(--space-12);
padding-left: var(--space-20); padding-left: var(--space-20);
padding-right: var(--space-12); padding-right: var(--space-12);
border-color: var(--clr-theme-container-outline-light);
} }
.header__title { .files-padding {
display: flex; padding-top: 0;
align-items: center; padding-bottom: var(--space-12);
gap: var(--space-4); padding-left: var(--space-12);
color: var(--clr-theme-scale-ntrl-0); padding-right: var(--space-12);
}
.header__left {
display: flex;
gap: var(--space-10);
} }
</style> </style>

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

View File

@ -2,21 +2,25 @@
import FileListItem from './FileListItem.svelte'; import FileListItem from './FileListItem.svelte';
import { sortLikeFileTree } from '$lib/vbranches/filetree'; import { sortLikeFileTree } from '$lib/vbranches/filetree';
import type { Ownership } from '$lib/vbranches/ownership'; 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'; 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 selectedOwnership: Writable<Ownership>;
export let isUnapplied = false; export let isUnapplied = false;
export let showCheckboxes = false; export let showCheckboxes = false;
export let selectedFiles: Writable<File[]>; export let selectedFiles: Writable<(File | RemoteFile)[]>;
export let allowMultiple = false;
$: console.log(selectedFiles);
</script> </script>
{#each sortLikeFileTree(branch.files) as file (file.id)} {#each sortLikeFileTree(files) as file (file.id)}
<FileListItem <FileListItem
{file} {file}
{isUnapplied} {isUnapplied}
branchId={branch.id} {branchId}
{selectedOwnership} {selectedOwnership}
showCheckbox={showCheckboxes} showCheckbox={showCheckboxes}
{selectedFiles} {selectedFiles}
@ -26,7 +30,7 @@
selectedFiles.update((fileIds) => fileIds.filter((f) => f.id != file.id)); selectedFiles.update((fileIds) => fileIds.filter((f) => f.id != file.id));
} else if (isAlreadySelected) { } else if (isAlreadySelected) {
$selectedFiles = []; $selectedFiles = [];
} else if (e.shiftKey) { } else if (e.shiftKey && allowMultiple) {
selectedFiles.update((files) => [file, ...files]); selectedFiles.update((files) => [file, ...files]);
} else { } else {
$selectedFiles = [file]; $selectedFiles = [file];

View File

@ -2,13 +2,13 @@
import BranchCard from './BranchCard.svelte'; import BranchCard from './BranchCard.svelte';
import FileCard from './FileCard.svelte'; import FileCard from './FileCard.svelte';
import { Ownership } from '$lib/vbranches/ownership'; 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 { writable, type Writable } from 'svelte/store';
import type { User, getCloudApiClient } from '$lib/backend/cloud'; import type { User, getCloudApiClient } from '$lib/backend/cloud';
import type { Project } from '$lib/backend/projects'; import type { Project } from '$lib/backend/projects';
import type { BranchService } from '$lib/branches/service'; import type { BranchService } from '$lib/branches/service';
import type { GitHubService } from '$lib/github/service'; import type { GitHubService } from '$lib/github/service';
import type { BranchController } from '$lib/vbranches/branchController'; import type { BranchController } from '$lib/vbranches/branchController';
import type { BaseBranch, Branch, File } from '$lib/vbranches/types';
export let branch: Branch; export let branch: Branch;
export let isUnapplied = false; export let isUnapplied = false;
@ -29,8 +29,11 @@
let commitBoxOpen: Writable<boolean>; 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 == 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); const match = branch.files?.find((f) => files[0].id == f.id);
if (!match) $selectedFiles = []; if (!match) $selectedFiles = [];
return match; return match;

View File

@ -1,5 +1,8 @@
<script lang="ts"> <script lang="ts">
import BranchFilesHeader from './BranchFilesHeader.svelte';
import BranchFilesList from './BranchFilesList.svelte';
import FileDiff from './FileDiff.svelte'; import FileDiff from './FileDiff.svelte';
import FileTree from './FileTree.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Modal.svelte'; import Modal from '$lib/components/Modal.svelte';
import Tag from '$lib/components/Tag.svelte'; import Tag from '$lib/components/Tag.svelte';
@ -7,9 +10,12 @@
import { draggable } from '$lib/dragging/draggable'; import { draggable } from '$lib/dragging/draggable';
import { draggableCommit, nonDraggable } from '$lib/dragging/draggables'; import { draggableCommit, nonDraggable } from '$lib/dragging/draggables';
import { getVSIFileIcon } from '$lib/ext-icons'; 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 { 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 { open } from '@tauri-apps/api/shell';
import { writable, type Writable } from 'svelte/store';
import type { ContentSection, HunkSection } from '$lib/utils/fileSections'; import type { ContentSection, HunkSection } from '$lib/utils/fileSections';
import type { BranchController } from '$lib/vbranches/branchController'; import type { BranchController } from '$lib/vbranches/branchController';
@ -21,8 +27,13 @@
export let isUnapplied = false; export let isUnapplied = false;
export let branchController: BranchController; export let branchController: BranchController;
export let projectPath: string; export let projectPath: string;
export let selectedFiles: Writable<(File | RemoteFile)[]>;
const selectedOwnership = writable(Ownership.default());
let previewCommitModal: Modal; let previewCommitModal: Modal;
let showFiles = false;
let selectedListMode: string;
let entries: [RemoteFile, (ContentSection | HunkSection)[]][] = []; let entries: [RemoteFile, (ContentSection | HunkSection)[]][] = [];
let isLoading = false; let isLoading = false;
@ -33,22 +44,22 @@
isLoading = false; isLoading = false;
} }
$: files = entries.map((entry) => entry[0]);
function onClick() { function onClick() {
loadEntries(); loadEntries();
previewCommitModal.show(); showFiles = !showFiles;
// previewCommitModal.show();
} }
</script> </script>
<div <div
on:click={onClick}
on:keyup={onClick}
use:draggable={commit instanceof Commit use:draggable={commit instanceof Commit
? draggableCommit(commit.branchId, commit) ? draggableCommit(commit.branchId, commit)
: nonDraggable()} : nonDraggable()}
role="button"
tabindex="0"
> >
<div class="commit__card" class:is-head-commit={isHeadCommit}> <div class="commit__card" class:is-head-commit={isHeadCommit}>
<div on:click={onClick} on:keyup={onClick} role="button" tabindex="0">
<div class="commit__header"> <div class="commit__header">
<span class="commit__description text-base-12 truncate"> <span class="commit__description text-base-12 truncate">
{commit.description} {commit.description}
@ -85,6 +96,37 @@
</span> </span>
</div> </div>
</div> </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> </div>
<Modal <Modal
@ -168,7 +210,6 @@
flex-direction: column; flex-direction: column;
cursor: default; cursor: default;
gap: var(--space-10); gap: var(--space-10);
padding: var(--space-12);
border-radius: var(--space-6); border-radius: var(--space-6);
background-color: var(--clr-theme-container-light); background-color: var(--clr-theme-container-light);
border: 1px solid var(--clr-theme-container-outline-light); border: 1px solid var(--clr-theme-container-outline-light);
@ -183,6 +224,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-8); gap: var(--space-8);
padding: var(--space-12) var(--space-12) 0 var(--space-12);
} }
.commit__description { .commit__description {
@ -197,6 +239,22 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-8); 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 { .commit__author {

View File

@ -6,7 +6,8 @@
import type { BranchService } from '$lib/branches/service'; import type { BranchService } from '$lib/branches/service';
import type { GitHubService } from '$lib/github/service'; import type { GitHubService } from '$lib/github/service';
import type { BranchController } from '$lib/vbranches/branchController'; 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 branch: Branch;
export let base: BaseBranch | undefined | null; export let base: BaseBranch | undefined | null;
@ -15,6 +16,7 @@
export let type: CommitStatus; export let type: CommitStatus;
export let githubService: GitHubService; export let githubService: GitHubService;
export let branchService: BranchService; export let branchService: BranchService;
export let selectedFiles: Writable<(File | RemoteFile)[]>;
export let isUnapplied: boolean; export let isUnapplied: boolean;
export let branchCount: number = 0; export let branchCount: number = 0;
@ -47,6 +49,7 @@
{base} {base}
{project} {project}
{isUnapplied} {isUnapplied}
{selectedFiles}
isChained={idx != commits.length - 1} isChained={idx != commits.length - 1}
isHeadCommit={commit.id === headCommit?.id} isHeadCommit={commit.id === headCommit?.id}
/> />

View File

@ -11,8 +11,15 @@
} from '$lib/dragging/draggables'; } from '$lib/dragging/draggables';
import { dropzone } from '$lib/dragging/dropzone'; import { dropzone } from '$lib/dragging/dropzone';
import { filesToOwnership } from '$lib/vbranches/ownership'; import { filesToOwnership } from '$lib/vbranches/ownership';
import { RemoteCommit, type BaseBranch, type Branch, type Commit } from '$lib/vbranches/types'; import {
import { get } from 'svelte/store'; 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 { Project } from '$lib/backend/projects';
import type { BranchController } from '$lib/vbranches/branchController'; import type { BranchController } from '$lib/vbranches/branchController';
@ -24,6 +31,7 @@
export let isChained: boolean; export let isChained: boolean;
export let isUnapplied = false; export let isUnapplied = false;
export let branchController: BranchController; export let branchController: BranchController;
export let selectedFiles: Writable<(File | RemoteFile)[]>;
function acceptAmend(commit: Commit | RemoteCommit) { function acceptAmend(commit: Commit | RemoteCommit) {
if (commit instanceof RemoteCommit) { if (commit instanceof RemoteCommit) {
@ -139,6 +147,7 @@
{resetHeadCommit} {resetHeadCommit}
{isUnapplied} {isUnapplied}
{branchController} {branchController}
{selectedFiles}
projectPath={project.path} projectPath={project.path}
/> />
</div> </div>

View File

@ -12,12 +12,12 @@
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import type { BranchController } from '$lib/vbranches/branchController'; import type { BranchController } from '$lib/vbranches/branchController';
import type { Ownership } from '$lib/vbranches/ownership'; 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'; import type { Writable } from 'svelte/store';
export let projectId: string; export let projectId: string;
export let branchId: string; export let branchId: string;
export let file: File; export let file: File | RemoteFile;
export let conflicted: boolean; export let conflicted: boolean;
export let projectPath: string | undefined; export let projectPath: string | undefined;
export let branchController: BranchController; export let branchController: BranchController;
@ -35,7 +35,7 @@
let sections: (HunkSection | ContentSection)[] = []; let sections: (HunkSection | ContentSection)[] = [];
function parseFile(file: File) { function parseFile(file: File | RemoteFile) {
// When we toggle expansion status on sections we need to assign // When we toggle expansion status on sections we need to assign
// `sections = sections` to redraw, and why we do not use a reactive // `sections = sections` to redraw, and why we do not use a reactive
// variable. // variable.

View File

@ -6,9 +6,9 @@
import { computeFileStatus } from '$lib/utils/fileStatus'; import { computeFileStatus } from '$lib/utils/fileStatus';
import { computeAddedRemovedByFiles } from '$lib/utils/metrics'; import { computeAddedRemovedByFiles } from '$lib/utils/metrics';
import { createEventDispatcher } from 'svelte'; 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; export let isFileLocked: boolean;
const dispatch = createEventDispatcher<{ close: void }>(); const dispatch = createEventDispatcher<{ close: void }>();

View File

@ -5,16 +5,16 @@
import { draggableFile } from '$lib/dragging/draggables'; import { draggableFile } from '$lib/dragging/draggables';
import { getVSIFileIcon } from '$lib/ext-icons'; import { getVSIFileIcon } from '$lib/ext-icons';
import type { Ownership } from '$lib/vbranches/ownership'; 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'; import type { Writable } from 'svelte/store';
export let branchId: string; export let branchId: string;
export let file: File; export let file: File | RemoteFile;
export let isUnapplied: boolean; export let isUnapplied: boolean;
export let selected: boolean; export let selected: boolean;
export let showCheckbox: boolean = false; export let showCheckbox: boolean = false;
export let selectedOwnership: Writable<Ownership>; export let selectedOwnership: Writable<Ownership>;
export let selectedFiles: Writable<File[]>; export let selectedFiles: Writable<(File | RemoteFile)[]>;
let checked = false; let checked = false;
let indeterminate = false; let indeterminate = false;

View File

@ -2,9 +2,9 @@
import FileStatusCircle from './FileStatusCircle.svelte'; import FileStatusCircle from './FileStatusCircle.svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/Icon.svelte';
import { computeFileStatus } from '$lib/utils/fileStatus'; 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); $: isLocked = file.hunks.some((h) => h.locked);
</script> </script>

View File

@ -7,7 +7,7 @@
import TreeListFolder from './TreeListFolder.svelte'; import TreeListFolder from './TreeListFolder.svelte';
import type { TreeNode } from '$lib/vbranches/filetree'; import type { TreeNode } from '$lib/vbranches/filetree';
import type { Ownership } from '$lib/vbranches/ownership'; 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'; import type { Writable } from 'svelte/store';
export let expanded = true; export let expanded = true;
@ -15,9 +15,10 @@
export let isRoot = false; export let isRoot = false;
export let showCheckboxes = false; export let showCheckboxes = false;
export let selectedOwnership: Writable<Ownership>; export let selectedOwnership: Writable<Ownership>;
export let selectedFiles: Writable<File[]>; export let selectedFiles: Writable<(File | RemoteFile)[]>;
export let branchId: string; export let branchId: string;
export let isUnapplied: boolean; export let isUnapplied: boolean;
export let allowMultiple = false;
function isNodeChecked(selectedOwnership: Ownership, node: TreeNode): boolean { function isNodeChecked(selectedOwnership: Ownership, node: TreeNode): boolean {
if (node.file) { if (node.file) {
@ -91,12 +92,13 @@
{selectedFiles} {selectedFiles}
showCheckbox={showCheckboxes} showCheckbox={showCheckboxes}
on:click={(e) => { on:click={(e) => {
e.stopPropagation();
const isAlreadySelected = $selectedFiles.includes(file); const isAlreadySelected = $selectedFiles.includes(file);
if (isAlreadySelected && e.shiftKey) { if (isAlreadySelected && e.shiftKey) {
selectedFiles.update((fileIds) => fileIds.filter((f) => f.id != file.id)); selectedFiles.update((fileIds) => fileIds.filter((f) => f.id != file.id));
} else if (isAlreadySelected) { } else if (isAlreadySelected) {
$selectedFiles = []; $selectedFiles = [];
} else if (e.shiftKey) { } else if (e.shiftKey && allowMultiple) {
selectedFiles.update((files) => [file, ...files]); selectedFiles.update((files) => [file, ...files]);
} else { } else {
$selectedFiles = [file]; $selectedFiles = [file];

View File

@ -2,12 +2,15 @@
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import CommitCard from '$lib/components/CommitCard.svelte'; import CommitCard from '$lib/components/CommitCard.svelte';
import type { BranchController } from '$lib/vbranches/branchController'; 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 branch: RemoteBranch | undefined;
export let projectId: string; export let projectId: string;
export let projectPath: string; export let projectPath: string;
export let branchController: BranchController; export let branchController: BranchController;
const selectedFiles = writable<(File | RemoteFile)[]>([]);
</script> </script>
{#if branch != undefined} {#if branch != undefined}
@ -24,7 +27,7 @@
{#if branch.commits && branch.commits.length > 0} {#if branch.commits && branch.commits.length > 0}
<div class="flex w-full flex-col gap-y-2"> <div class="flex w-full flex-col gap-y-2">
{#each branch.commits as commit} {#each branch.commits as commit}
<CommitCard {commit} {projectId} {branchController} {projectPath} /> <CommitCard {commit} {projectId} {branchController} {projectPath} {selectedFiles} />
{/each} {/each}
</div> </div>
{/if} {/if}

View File

@ -5,16 +5,16 @@
import { draggableFile } from '$lib/dragging/draggables'; import { draggableFile } from '$lib/dragging/draggables';
import { getVSIFileIcon } from '$lib/ext-icons'; import { getVSIFileIcon } from '$lib/ext-icons';
import type { Ownership } from '$lib/vbranches/ownership'; 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'; import type { Writable } from 'svelte/store';
export let branchId: string; export let branchId: string;
export let file: File; export let file: File | RemoteFile;
export let selected: boolean; export let selected: boolean;
export let isUnapplied: boolean; export let isUnapplied: boolean;
export let showCheckbox: boolean = false; export let showCheckbox: boolean = false;
export let selectedOwnership: Writable<Ownership>; export let selectedOwnership: Writable<Ownership>;
export let selectedFiles: Writable<File[]>; export let selectedFiles: Writable<(File | RemoteFile)[]>;
let checked = false; let checked = false;
let indeterminate = false; let indeterminate = false;
@ -103,16 +103,19 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-10); gap: var(--space-10);
overflow: hidden;
} }
.name-wrapper { .name-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-6); gap: var(--space-6);
overflow: hidden;
} }
.name { .name {
color: var(--clr-theme-scale-ntrl-0); color: var(--clr-theme-scale-ntrl-0);
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap;
} }
.selected { .selected {
background-color: var(--clr-theme-scale-pop-80); background-color: var(--clr-theme-scale-pop-80);

View File

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

View File

@ -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'; import type { Writable } from 'svelte/store';
export function nonDraggable() { export function nonDraggable() {
@ -23,11 +23,15 @@ export function isDraggableHunk(obj: any): obj is DraggableHunk {
export type DraggableFile = { export type DraggableFile = {
branchId: string; branchId: string;
files: Writable<File[]>; files: Writable<(File | RemoteFile)[]>;
current: File; 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 } }; return { data: { branchId, current, files } };
} }

View File

@ -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 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) { if (file.hunks.length == 1) {
const changeType = file.hunks[0].changeType; const changeType = file.hunks[0].changeType;
if (changeType == 'added') { if (changeType == 'added') {

View File

@ -1,7 +1,7 @@
import { HunkSection, type ContentSection } from './fileSections'; 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 return files
.flatMap((f) => f.hunks) .flatMap((f) => f.hunks)
.map((h) => h.diff.split('\n')) .map((h) => h.diff.split('\n'))

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

View File

@ -4,11 +4,11 @@
* This module provides support for tranforming a list of files into a * This module provides support for tranforming a list of files into a
* hirerarchical structure for easy rendering. * hirerarchical structure for easy rendering.
*/ */
import type { File } from './types'; import type { File, RemoteFile } from './types';
export interface TreeNode { export interface TreeNode {
name: string; name: string;
file?: File; file?: File | RemoteFile;
children: TreeNode[]; children: TreeNode[];
parent?: 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: [] }; const acc: TreeNode = { name: 'root', children: [] };
files.forEach((f) => { files.forEach((f) => {
const pathParts = f.path.split('/'); const pathParts = f.path.split('/');
@ -51,8 +51,8 @@ export function filesToFileTree(files: File[]): TreeNode {
return acc; return acc;
} }
function fileTreeToList(node: TreeNode): File[] { function fileTreeToList(node: TreeNode): (File | RemoteFile)[] {
const list: File[] = []; const list: (File | RemoteFile)[] = [];
if (node.file) list.push(node.file); if (node.file) list.push(node.file);
node.children.forEach((child) => { node.children.forEach((child) => {
list.push(...fileTreeToList(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 // 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)); return fileTreeToList(filesToFileTree(files));
} }

View File

@ -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'); return files.map((f) => `${f.path}:${f.hunks.map(({ id }) => id).join(',')}`).join('\n');
} }

View File

@ -1,5 +1,6 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { Type, Transform } from 'class-transformer'; import { Type, Transform } from 'class-transformer';
import { hashCode } from '$lib/utils/string';
export type ChangeType = export type ChangeType =
/// Entry does not exist in old version /// Entry does not exist in old version
@ -130,6 +131,14 @@ export class RemoteCommit {
export class RemoteHunk { export class RemoteHunk {
diff!: string; diff!: string;
get id(): string {
return hashCode(this.diff);
}
get locked() {
return false;
}
} }
export class RemoteFile { export class RemoteFile {
@ -137,6 +146,34 @@ export class RemoteFile {
@Type(() => RemoteHunk) @Type(() => RemoteHunk)
hunks!: RemoteHunk[]; hunks!: RemoteHunk[];
binary!: boolean; 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 { export interface Author {