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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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 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') {

View File

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

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
* 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));
}

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');
}

View File

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