Svelte: Perforce UI elements refactor (#64279)

## UI Updates for Perforce Depots and Git Repos

Fixes SRCH-530

**NOTE: This PR is a refactor of an earlier
[PR](https://github.com/sourcegraph/sourcegraph/pull/64014) that was
reverted. For that reason, the PR description is largely the same.**

This PR introduces changes to the UI to differentiate between Perforce
Depots and Git repositories. Below are the key changes included in this
commit:

### 1. Dynamic Top-Level Navigation

**For Perforce Depots:**

![Screenshot 2024-07-31 at 10 10 37
AM](https://github.com/user-attachments/assets/2d261b51-f8fa-4599-acae-3520d38996f3)

**For Git Repos:**

![Screenshot 2024-07-31 at 10 10 14
AM](https://github.com/user-attachments/assets/0f9ee3f7-918a-42d8-908f-04593ed52ebd)

### 2. Tabs on Revision Picker

**For Perforce Depots:**

Since we only need one tab for changelists, no tabs are shown.

![Screenshot 2024-07-31 at 10 20 24
AM](https://github.com/user-attachments/assets/f1006d56-67aa-41ab-a13b-905e157cb283)

**For Git Repos:**

We have tabs for Branches, Tags, and Commits.

![Screenshot 2024-07-31 at 10 23 02
AM](https://github.com/user-attachments/assets/38907d51-0407-4cd7-ad4c-1c5967dfddf3)

### 3. Commits/Changelists Page

**For Git Repos:**

The page displays Git commits.

![Screenshot 2024-07-31 at 10 26 23
AM](https://github.com/user-attachments/assets/85245d1d-708f-4d51-9da3-0425c3f085d0)

**For Perforce Depots:**

The page displays Perforce changelists.

![Screenshot 2024-07-31 at 10 26 39
AM](https://github.com/user-attachments/assets/2f6f16aa-d498-4763-949d-d1a13f9a26ac)

### 4. Vocabulary Adjustments

- We display either Git commit SHAs or Changelist IDs based on the
project type.
- For authorship, we use "submitted by" for Perforce and "committed by"
for Git.
- We refer to "Commits" for Git projects and "Changelists" for Perforce
projects.

**Examples:**

- **For Git Commits:**

![Screenshot 2024-07-31 at 10 37 08
AM](https://github.com/user-attachments/assets/ac15b0b3-4c85-4a4c-80c0-ec9384b72eca)

- **For Perforce Changelists:**

![Screenshot 2024-07-31 at 10 37 35
AM](https://github.com/user-attachments/assets/4230cb32-5285-4141-b374-f3ea23042e1d)

### 5. URL Mapping

URLs are now structured differently based on the project type:

- **Commits Page:**
  - Git: `/[repo-name]/-/commits`
  - Perforce: `/[repo-name]/-/changelists`
  
- **Individual Item Page:**
  - Git: `/[repo-name]/-/commit/[commit-hash]`
  - Perforce: `/[depot-name]/-/changelist/[changelist-ID]`

When viewing a specific commit or changelist:
- **Git:** `/[repo-name]@[git-commit-hash]`
- **Perforce:** `/[repo-name]@changelist/[changelist-id]`

_NOTE: The value displayed in the search field will also change
accordingly._


### What is left to be done?
**On repo search results, when searching a revision, we still show the
git commit SHA instead of the changelist ID for perforce depots:**
![Screenshot 2024-07-31 at 10 59 12
AM](https://github.com/user-attachments/assets/38bc2a3e-be8b-4585-9fe0-776149a7f230)

I plan to make a follow-up issue for this and begin work on it
immediately. It's a little trickier than the other changes because in
the RepositoryMatch type, there is no value that can help us determine
whether a project is a depot or a repo. We need to find another way to
fetch that data.

### Request for reviewers: 
1. Please try to break these new features and tell me what you find. I
stumbled on a number of little gotchas while working on this, and I'm
sure I've missed some.

## Test plan
<!-- REQUIRED; info at
https://docs-legacy.sourcegraph.com/dev/background-information/testing_principles
-->
- Manual/Visual testing
- Adjust e2e and integration tests to obtain a passing CI
- Test directly visiting a URL versus getting there via click
- Add unit tests for new/updated helper functions

---------

Co-authored-by: Camden Cheek <camden@ccheek.com>
This commit is contained in:
Jason Hawk Harris 2024-08-14 14:18:24 -05:00 committed by GitHub
parent f403bfc1ff
commit cff1669bc1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1166 additions and 109 deletions

View File

@ -0,0 +1,29 @@
fragment Changelist on PerforceChangelist {
cid
canonicalURL
commit {
message
oid
body
subject
author {
person {
...Avatar_Person
}
date
}
parents {
id
oid
abbreviatedOID
parent: perforceChangelist {
cid
canonicalURL
}
}
perforceChangelist {
cid
canonicalURL
}
}
}

View File

@ -0,0 +1,157 @@
<svelte:options immutable />
<script lang="ts">
import Avatar from '$lib/Avatar.svelte'
import Icon from '$lib/Icon.svelte'
import Timestamp from '$lib/Timestamp.svelte'
import Tooltip from '$lib/Tooltip.svelte'
import type { Changelist } from './Changelist.gql'
import { isViewportMobile } from './stores'
import Button from './wildcard/Button.svelte'
export let changelist: Changelist
export let alwaysExpanded: boolean = false
$: expanded = alwaysExpanded
$: author = changelist.commit.author
$: commitDate = new Date(author.date)
$: authorAvatarTooltip = author.person.name + (author ? ' (author)' : '')
</script>
<div class="root">
<div class="avatar">
<Tooltip tooltip={authorAvatarTooltip}>
<Avatar avatar={author.person} />
</Tooltip>
</div>
<div class="title">
<!-- TODO need subject-->
<a class="subject" href={changelist.canonicalURL}>{changelist.commit.subject}</a>
{#if !alwaysExpanded && changelist.commit.body && !$isViewportMobile}
<Button
variant="secondary"
size="sm"
on:click={() => (expanded = !expanded)}
aria-label="{expanded ? 'Hide' : 'Show'} changelist message"
>
<Icon icon={ILucideEllipsis} inline aria-hidden />
</Button>
{/if}
</div>
<div class="author">
submitted by <strong>{author.person.name}</strong>
<Timestamp date={commitDate} />
</div>
{#if changelist.commit.body}
<div class="message" class:expanded>
{#if $isViewportMobile}
{#if expanded}
<Button variant="secondary" size="lg" display="block" on:click={() => (expanded = false)}>
Close
</Button>
{:else}
<Button variant="secondary" size="sm" display="block" on:click={() => (expanded = true)}>
Show changelist message
</Button>
{/if}
{/if}
<pre>{changelist.commit.body}</pre>
</div>
{/if}
</div>
<style lang="scss">
.root {
display: grid;
overflow: hidden;
grid-template-columns: auto 1fr;
grid-template-areas: 'avatar title' 'avatar author' '. message';
column-gap: 1rem;
@media (--mobile) {
grid-template-columns: auto 1fr;
grid-template-areas: 'avatar title' 'author author' 'message message';
row-gap: 0.5rem;
}
}
.avatar {
grid-area: avatar;
display: flex;
gap: 0.25rem;
align-self: center;
}
.title {
grid-area: title;
align-self: center;
display: flex;
gap: 0.5rem;
align-items: center;
overflow: hidden;
.subject {
font-weight: 600;
flex: 0 1 auto;
color: var(--body-color);
min-width: 0;
@media (--sm-breakpoint-up) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.author {
grid-area: author;
color: var(--text-muted);
}
.message {
grid-area: message;
overflow: hidden;
@media (--mobile) {
&.expanded {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
display: flex;
flex-direction: column;
background-color: var(--color-bg-1);
}
}
}
pre {
display: none;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
font-size: 0.75rem;
max-width: 100%;
word-wrap: break-word;
white-space: pre-wrap;
.expanded & {
display: block;
}
@media (--mobile) {
padding: 0.5rem;
overflow: auto;
margin: 0;
}
}
</style>

View File

@ -11,7 +11,8 @@
import Tooltip from '$lib/Tooltip.svelte'
import { parseBrowserRepoURL } from '$lib/web'
export let commitID: string
export let revID: string
export let tooltip: string
const hotkey = createHotkey({
keys: { key: 'y' },
@ -19,7 +20,7 @@
handler: () => {
const { revision } = parseBrowserRepoURL($page.url.pathname)
// Only navigate if necessary. We don't want to add unnecessary history entries.
if (revision !== commitID) {
if (revision !== revID) {
goto(href, { noScroll: true, keepFocus: true }).catch(() => {
// TODO: log error with Sentry
})
@ -27,7 +28,7 @@
},
})
$: href = commitID ? replaceRevisionInURL($page.url.toString(), commitID) : ''
$: href = revID ? replaceRevisionInURL($page.url.toString(), revID) : ''
$: if (href) {
hotkey.enable()
} else {
@ -36,7 +37,7 @@
</script>
{#if href}
<Tooltip tooltip="Permalink (with full git commit SHA)">
<Tooltip {tooltip}>
<a {href}><Icon icon={ILucideLink} inline aria-hidden /> <span data-action-label>Permalink</span></a>
</Tooltip>
{/if}

View File

@ -40,3 +40,14 @@ fragment RepositoryGitRevAuthor on GitCommit {
}
}
}
fragment RevPickerChangelist on GitCommit {
__typename
id
subject
perforceChangelist {
cid
canonicalURL
}
...DepotChangelistAuthor
}

View File

@ -1,7 +1,7 @@
<script context="module" lang="ts">
import type { Keys } from '$lib/Hotkey'
import type { RepositoryGitRefs, RevPickerGitCommit } from './RepositoryRevPicker.gql'
import type { RepositoryGitRefs, RevPickerChangelist, RevPickerGitCommit } from './RepositoryRevPicker.gql'
export type RepositoryBranches = RepositoryGitRefs['gitRefs']
export type RepositoryBranch = RepositoryBranches['nodes'][number]
@ -13,6 +13,9 @@
export type RepositoryCommits = { nodes: RevPickerGitCommit[] }
export type RepositoryGitCommit = RevPickerGitCommit
export type DepotChangelists = { nodes: RevPickerChangelist[] }
export type DepotChangelist = RevPickerChangelist
const branchesHotkey: Keys = {
key: 'shift+b',
}
@ -28,6 +31,7 @@
<script lang="ts">
import type { Placement } from '@floating-ui/dom'
import type { ComponentProps } from 'svelte'
import type { HTMLButtonAttributes } from 'svelte/elements'
import { goto } from '$app/navigation'
@ -44,7 +48,6 @@
import Picker from './Picker.svelte'
import RepositoryRevPickerItem from './RepositoryRevPickerItem.svelte'
import type { ComponentProps } from 'svelte'
type $$Props = HTMLButtonAttributes & {
repoURL: string
@ -54,10 +57,16 @@
display?: ComponentProps<ButtonGroup>['display']
placement?: Placement
onSelect?: (revision: string) => void
getRepositoryTags: (query: string) => PromiseLike<RepositoryTags>
getRepositoryCommits: (query: string) => PromiseLike<RepositoryCommits>
getRepositoryBranches: (query: string) => PromiseLike<RepositoryBranches>
}
} & (
| {
getRepositoryTags: (query: string) => PromiseLike<RepositoryTags>
getRepositoryCommits: (query: string) => PromiseLike<RepositoryCommits>
getRepositoryBranches: (query: string) => PromiseLike<RepositoryBranches>
}
| {
getDepotChangelists: (query: string) => PromiseLike<DepotChangelists>
}
)
export let repoURL: $$Props['repoURL']
export let revision: $$Props['revision'] = undefined
@ -72,9 +81,10 @@
export let onSelect = defaultHandleSelect
// Pickers data sources
export let getRepositoryTags: (query: string) => PromiseLike<RepositoryTags>
export let getRepositoryCommits: (query: string) => PromiseLike<RepositoryCommits>
export let getRepositoryBranches: (query: string) => PromiseLike<RepositoryBranches>
export let getRepositoryTags: ((query: string) => PromiseLike<RepositoryTags>) | undefined = undefined
export let getRepositoryCommits: ((query: string) => PromiseLike<RepositoryCommits>) | undefined = undefined
export let getRepositoryBranches: ((query: string) => PromiseLike<RepositoryBranches>) | undefined = undefined
export let getDepotChangelists: ((query: string) => PromiseLike<DepotChangelists>) | undefined = undefined
function defaultHandleSelect(revision: string) {
goto(replaceRevisionInURL(location.pathname + location.search + location.hash, revision))
@ -107,7 +117,7 @@
</CopyButton>
{#if isOnSpecificRev}
<Tooltip tooltip="Go to default branch">
<Tooltip tooltip={getDepotChangelists ? 'Go to most recent changelist' : 'Go to default branch'}>
<button
class="{buttonClass} close-button hoverable-button"
on:click={() => onSelect(defaultBranch)}
@ -121,73 +131,97 @@
<div slot="content" class="content" let:toggle>
<Tabs>
<TabPanel title="Branches" shortcut={branchesHotkey}>
<Picker
name="branches"
seeAllItemsURL={`${repoURL}/-/branches`}
getData={getRepositoryBranches}
toOption={branch => ({ value: branch.id, label: branch.displayName })}
onSelect={branch => {
toggle(false)
onSelect(branch.abbrevName)
}}
let:value
>
<RepositoryRevPickerItem
icon={ILucideGitBranch}
label={value.displayName}
author={value.target.commit?.author}
{#if getRepositoryCommits && getRepositoryTags && getRepositoryBranches}
<TabPanel title="Branches" shortcut={branchesHotkey}>
<Picker
name="branches"
seeAllItemsURL={`${repoURL}/-/branches`}
getData={getRepositoryBranches}
toOption={branch => ({ value: branch.id, label: branch.displayName })}
onSelect={branch => {
toggle(false)
onSelect(branch.abbrevName)
}}
let:value
>
<svelte:fragment slot="title">
<Icon icon={ILucideGitBranch} inline aria-hidden="true" />
<Badge variant="link">{value.displayName}</Badge>
{#if value.displayName === defaultBranch}
<Badge variant="secondary" small>DEFAULT</Badge>
{/if}
</svelte:fragment>
</RepositoryRevPickerItem>
</Picker>
</TabPanel>
<TabPanel title="Tags" shortcut={tagsHotkey}>
<Picker
name="tags"
seeAllItemsURL={`${repoURL}/-/tags`}
getData={getRepositoryTags}
toOption={tag => ({ value: tag.id, label: tag.displayName })}
onSelect={tag => {
toggle(false)
onSelect(tag.abbrevName)
}}
let:value
>
<RepositoryRevPickerItem
icon={ILucideTag}
label={value.displayName}
author={value.target.commit?.author}
/>
</Picker>
</TabPanel>
<TabPanel title="Commits" shortcut={commitsHotkey}>
<Picker
name="commits"
seeAllItemsURL={`${repoURL}/-/commits`}
getData={getRepositoryCommits}
toOption={commit => ({ value: commit.id, label: commit.oid })}
onSelect={commit => {
toggle(false)
onSelect(commit.oid)
}}
let:value
>
<RepositoryRevPickerItem label="" author={value.author}>
<svelte:fragment slot="title">
<Icon icon={ILucideGitCommitVertical} inline aria-hidden="true" />
<Badge variant="link">{value.abbreviatedOID}</Badge>
<span class="commit-subject">{value.subject}</span>
</svelte:fragment>
</RepositoryRevPickerItem>
</Picker>
</TabPanel>
<RepositoryRevPickerItem
icon={ILucideGitBranch}
label={value.displayName}
author={value.target.commit?.author}
>
<svelte:fragment slot="title">
<Icon icon={ILucideGitBranch} inline aria-hidden="true" />
<Badge variant="link">{value.displayName}</Badge>
{#if value.displayName === defaultBranch}
<Badge variant="secondary" small>DEFAULT</Badge>
{/if}
</svelte:fragment>
</RepositoryRevPickerItem>
</Picker>
</TabPanel>
<TabPanel title="Tags" shortcut={tagsHotkey}>
<Picker
name="tags"
seeAllItemsURL={`${repoURL}/-/tags`}
getData={getRepositoryTags}
toOption={tag => ({ value: tag.id, label: tag.displayName })}
onSelect={tag => {
toggle(false)
onSelect(tag.abbrevName)
}}
let:value
>
<RepositoryRevPickerItem
icon={ILucideTag}
label={value.displayName}
author={value.target.commit?.author}
/>
</Picker>
</TabPanel>
<TabPanel title="Commits" shortcut={commitsHotkey}>
<Picker
name="commits"
seeAllItemsURL={`${repoURL}/-/commits`}
getData={getRepositoryCommits}
toOption={commit => ({ value: commit.id, label: commit.oid })}
onSelect={commit => {
toggle(false)
onSelect(commit.oid)
}}
let:value
>
<RepositoryRevPickerItem label="" author={value.author}>
<svelte:fragment slot="title">
<Icon icon={ILucideGitCommitVertical} inline aria-hidden="true" />
<Badge variant="link">{value.abbreviatedOID}</Badge>
<span class="subject">{value.subject}</span>
</svelte:fragment>
</RepositoryRevPickerItem>
</Picker>
</TabPanel>
{:else if getDepotChangelists}
<TabPanel title="Changelists" shortcut={commitsHotkey}>
<Picker
name="changelists"
seeAllItemsURL={`${repoURL}/-/changelists`}
getData={getDepotChangelists}
toOption={changelist => ({ value: changelist.id, label: changelist.perforceChangelist?.cid })}
onSelect={changelist => {
toggle(false)
onSelect(`changelist/${changelist.perforceChangelist?.cid}` ?? '')
}}
let:value
>
<RepositoryRevPickerItem label="" author={value.author}>
<svelte:fragment slot="title">
<Icon icon={ILucideGitCommitVertical} inline aria-hidden="true" />
<Badge variant="link">{value.perforceChangelist?.cid}</Badge>
<span class="subject">{value.subject}</span>
</svelte:fragment>
</RepositoryRevPickerItem>
</Picker>
</TabPanel>
{/if}
</Tabs>
</div>
</Popover>
@ -258,13 +292,14 @@
}
}
.commit-subject {
.subject {
overflow: hidden;
text-overflow: ellipsis;
}
// Local override for commits picker abbreviatedOID badge
:global([data-tab-panel='Commits']) :global([data-badge]) {
// Local override for commits/changelists picker abbreviatedOID/ChangelistID badge
:global([data-tab-panel='Commits']) :global([data-badge]),
:global([data-tab-panel='Changelists']) :global([data-badge]) {
flex-shrink: 0;
}
}

View File

@ -26,6 +26,11 @@ export const svelteKitRoutes: SvelteKitRoute[] = [
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/tree(?:/.*)?/?$'),
isRepoRoot: false,
},
{
id: '/[...repo=reporev]/(validrev)/-/changelists/[...path]',
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/changelists(?:/.*)?/?$'),
isRepoRoot: false,
},
{
id: '/[...repo=reporev]/(validrev)/-/commits/[...path]',
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/commits(?:/.*)?/?$'),
@ -41,6 +46,11 @@ export const svelteKitRoutes: SvelteKitRoute[] = [
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/branches/all/?$'),
isRepoRoot: false,
},
{
id: '/[...repo=reporev]/-/changelist/[changelistID]',
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/changelist(?:/[^/]&#43;)/?$'),
isRepoRoot: false,
},
{
id: '/[...repo=reporev]/-/commit/[...revspec]',
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/commit(?:/.*)?/?$'),

View File

@ -212,16 +212,27 @@
</button>
</Tooltip>
<RepositoryRevPicker
display="block"
repoURL={data.repoURL}
revision={data.revision}
commitID={data.resolvedRevision.commitID}
defaultBranch={data.defaultBranch}
getRepositoryBranches={data.getRepoBranches}
getRepositoryCommits={data.getRepoCommits}
getRepositoryTags={data.getRepoTags}
/>
{#if data.isPerforceDepot}
<RepositoryRevPicker
display="block"
repoURL={data.repoURL}
revision={data.revision}
commitID={data.resolvedRevision.commitID}
defaultBranch={data.defaultBranch}
getDepotChangelists={data.getDepotChangelists}
/>
{:else}
<RepositoryRevPicker
display="block"
repoURL={data.repoURL}
revision={data.revision}
commitID={data.resolvedRevision.commitID}
defaultBranch={data.defaultBranch}
getRepositoryBranches={data.getRepoBranches}
getRepositoryCommits={data.getRepoCommits}
getRepositoryTags={data.getRepoTags}
/>
{/if}
<Tooltip tooltip={isCollapsed ? 'Open search fuzzy finder' : ''}>
<button class="{sidebarButtonClass} search-files-button" on:click={() => openFuzzyFinder('files')}>

View File

@ -27,7 +27,7 @@ import {
BlobViewCodeGraphDataNextPage,
} from './page.gql'
function loadDiffView({ params, url }: PageLoadEvent) {
async function loadDiffView({ params, url }: PageLoadEvent) {
const client = getGraphQLClient()
const revisionOverride = url.searchParams.get('rev')
const { repoName } = parseRepoRevision(params.repo)
@ -220,5 +220,6 @@ export const load: PageLoad = event => {
if (showDiff && revisionOverride) {
return loadDiffView(event)
}
return loadFileView(event)
}

View File

@ -117,6 +117,7 @@
$: selectedCodeGraphDataOccurrences = codeGraphData?.find(datum =>
datum.commit.startsWith($selectedCodeGraphDataDebugOption)
)?.occurrences // TODO: we should probably use the nonoverlapping occurrences here
$: isPerforceDepot = data.isPerforceDepot
function viewModeURL(viewMode: CodeViewMode) {
switch (viewMode) {
@ -181,7 +182,10 @@
{#if blob}
<OpenInCodeHostAction data={blob} lineOrPosition={data.lineOrPosition} />
{/if}
<Permalink {commitID} />
<Permalink
revID={commitID}
tooltip={isPerforceDepot ? 'Permalink (with full changelist ID)' : 'Permalink (with full commit SHA)'}
/>
{#if $isCodyAvailable}
<OpenCodyAction />
{/if}

View File

@ -3,6 +3,10 @@ query BlobDiffViewCommitQuery($repoName: String!, $revspec: String!, $path: Stri
id
commit(rev: $revspec) {
id
perforceChangelist {
cid
canonicalURL
}
blob(path: $path) {
...DiffViewGitBlob
@ -23,6 +27,10 @@ query BlobFileViewBlobQuery($repoName: String!, $revspec: String!, $path: String
blob(path: $path) {
...FileViewGitBlob
}
perforceChangelist {
cid
canonicalURL
}
}
}
}
@ -82,6 +90,10 @@ query BlobFileViewCommitQuery_revisionOverride($repoName: String!, $revspec: Str
repository(name: $repoName) {
commit(rev: $revspec) {
...FileViewCommit
perforceChangelist {
cid
canonicalURL
}
}
}
}

View File

@ -6,11 +6,11 @@
import FileHeader from '$lib/repo/FileHeader.svelte'
import type { TreeEntryWithCommitInfo } from '$lib/repo/FileTable.gql'
import FileTable from '$lib/repo/FileTable.svelte'
import OpenCodyAction from '$lib/repo/OpenCodyAction.svelte'
import Permalink from '$lib/repo/Permalink.svelte'
import Readme from '$lib/repo/Readme.svelte'
import { createPromiseStore } from '$lib/utils'
import { Alert } from '$lib/wildcard'
import OpenCodyAction from '$lib/repo/OpenCodyAction.svelte'
import { getRepositoryPageContext } from '../../../../../context'
@ -23,6 +23,8 @@
$: treeEntriesWithCommitInfo.set(data.treeEntriesWithCommitInfo)
$: isCodyAvailable = data.isCodyAvailable
$: isPerforceDepot = data.isPerforceDepot
$: tooltip = isPerforceDepot ? 'Permalink (with full changelist ID)' : 'Permalink (with full commit SHA)'
afterNavigate(() => {
repositoryContext.set({ directoryPath: data.filePath })
@ -38,7 +40,7 @@
<FileHeader type="tree" repoName={data.repoName} revision={data.revision} path={data.filePath}>
<svelte:fragment slot="actions">
<Permalink commitID={data.resolvedRevision.commitID} />
<Permalink revID={data.resolvedRevision.commitID} {tooltip} />
{#if isCodyAvailable}
<OpenCodyAction />
{/if}

View File

@ -7,7 +7,7 @@ import { parseRepoRevision } from '$lib/shared'
import type { PageLoad } from './$types'
import { TreePageCommitInfoQuery, TreePageReadmeQuery } from './page.gql'
export const load: PageLoad = ({ parent, params }) => {
export const load: PageLoad = async ({ parent, params }) => {
const client = getGraphQLClient()
const { repoName, revision = '' } = parseRepoRevision(params.repo)
const resolvedRevision = resolveRevision(parent, revision)

View File

@ -1,11 +1,11 @@
import { error } from '@sveltejs/kit'
import { error, redirect } from '@sveltejs/kit'
import type { ResolvedRevision } from '$lib/repo/utils'
import { RevisionNotFoundError } from '$lib/shared'
import { RevisionNotFoundError, replaceRevisionInURL } from '$lib/shared'
import type { LayoutLoad } from './$types'
export const load: LayoutLoad = async ({ parent }) => {
export const load: LayoutLoad = async ({ parent, url }) => {
// By validating the resolved revision here we can guarantee to
// subpages that if they load the requested revision exists. This
// relieves subpages from testing whether the revision is valid.
@ -17,6 +17,16 @@ export const load: LayoutLoad = async ({ parent }) => {
error(404, new RevisionNotFoundError(revision))
}
const isPerforceDepot = !!resolvedRepository.commit?.perforceChangelist
if (isPerforceDepot && !revision.includes('changelist')) {
const redirectURL = replaceRevisionInURL(
url.toString(),
'changelist/' + resolvedRepository.commit?.perforceChangelist?.cid
)
redirect(301, redirectURL)
}
return {
resolvedRevision: {
repo: resolvedRepository,

View File

@ -0,0 +1,209 @@
<script lang="ts">
// @sg EnableRollout
import { get } from 'svelte/store'
import { navigating } from '$app/stores'
import Changelist from '$lib/Changelist.svelte'
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import RepositoryRevPicker from '$lib/repo/RepositoryRevPicker.svelte'
import Scroller, { type Capture as ScrollerCapture } from '$lib/Scroller.svelte'
import { Alert, Badge } from '$lib/wildcard'
import type { PageData, Snapshot } from './$types'
export let data: PageData
// This tracks the number of changelists that have been loaded and the current scroll
// position, so both can be restored when the user refreshes the page or navigates
// back to it.
export const snapshot: Snapshot<{
changelists: ReturnType<typeof data.changelistsQuery.capture>
scroller: ScrollerCapture
}> = {
capture() {
return {
changelists: changelistsQuery.capture(),
scroller: scroller.capture(),
}
},
async restore(snapshot) {
if (get(navigating)?.type === 'popstate') {
await changelistsQuery?.restore(snapshot.changelists)
}
scroller.restore(snapshot.scroller)
},
}
function fetchMore() {
changelistsQuery?.fetchMore()
}
let scroller: Scroller
$: changelistsQuery = data.changelistsQuery
$: changelists = $changelistsQuery.data
$: pageTitle = (() => {
const parts = ['Changelists']
if (data.path) {
parts.push(data.path)
}
parts.push(data.displayRepoName, 'Sourcegraph')
return parts.join(' - ')
})()
</script>
<svelte:head>
<title>{pageTitle}</title>
</svelte:head>
<header>
<h2>
Changelists
{#if data.path}
in <code>{data.path}</code>
{/if}
</h2>
<div>
<RepositoryRevPicker
repoURL={data.repoURL}
revision={data.revision}
commitID={data.resolvedRevision.commitID}
defaultBranch={data.defaultBranch}
placement="bottom-start"
getDepotChangelists={data.getDepotChangelists}
/>
</div>
</header>
<section>
<Scroller bind:this={scroller} margin={600} on:more={fetchMore}>
{#if changelists}
<ul class="changelists">
{#each changelists as changelistCommit (changelistCommit.perforceChangelist?.canonicalURL)}
{@const changelist = changelistCommit.perforceChangelist}
{#if changelist !== null}
<li>
<div class="changelist">
<Changelist {changelist} />
</div>
<ul class="actions">
<li>
Changelist ID:
<Badge variant="link">
<a href={changelist?.canonicalURL} title="View changelist">{changelist?.cid}</a>
</Badge>
</li>
<li> <a href="/{data.repoName}@changelist/{changelist?.cid}">Browse files</a></li>
</ul>
</li>
{/if}
{:else}
<li>
<Alert variant="info">No changelists found</Alert>
</li>
{/each}
</ul>
{/if}
{#if $changelistsQuery.fetching}
<div class="footer">
<LoadingSpinner />
</div>
{:else if !$changelistsQuery.fetching && $changelistsQuery.error}
<div class="footer">
<Alert variant="danger">
Unable to fetch changelists: {$changelistsQuery.error.message}
</Alert>
</div>
{/if}
</Scroller>
</section>
<style lang="scss">
section {
overflow: hidden;
}
ul {
margin: 0;
padding: 0;
}
h2 {
display: flex;
align-items: center;
gap: 0.5rem;
}
header {
border-bottom: 1px solid var(--border-color);
div {
width: min-content;
}
}
header,
ul.changelists,
.footer {
max-width: var(--viewport-xl);
width: 100%;
margin: 0 auto;
padding: 1rem;
@media (--mobile) {
padding: 0.5rem;
}
}
ul {
list-style: none;
}
ul.changelists {
--avatar-size: 2.5rem;
padding-top: 0;
> li {
border-bottom: 1px solid var(--border-color);
display: flex;
padding: 0.5rem 0;
gap: 1rem;
@media (--mobile) {
display: block;
.actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
li:not(:last-child)::after {
content: '•';
padding-left: 0.5rem;
color: var(--text-muted);
}
}
}
.changelist {
flex: 1;
min-width: 0;
}
.actions {
--icon-color: currentColor;
text-align: right;
flex-shrink: 0;
}
&:last-child {
border: none;
}
}
}
.footer {
&:not(:first-child) {
border-top: 1px solid var(--border-color);
}
}
</style>

View File

@ -0,0 +1,51 @@
import { parseRepoRevision } from '@sourcegraph/shared/src/util/url'
import { IncrementalRestoreStrategy, getGraphQLClient, infinityQuery } from '$lib/graphql'
import { resolveRevision } from '$lib/repo/utils'
import type { PageLoad } from './$types'
import { ChangelistsPage_ChangelistsQuery } from './page.gql'
const PAGE_SIZE = 20
export const load: PageLoad = ({ parent, params }) => {
const client = getGraphQLClient()
const { repoName, revision = '' } = parseRepoRevision(params.repo)
const path = params.path ? decodeURIComponent(params.path) : ''
const resolvedRevision = resolveRevision(parent, revision)
const changelistsQuery = infinityQuery({
client,
query: ChangelistsPage_ChangelistsQuery,
variables: resolvedRevision.then(revision => ({
depotName: repoName,
revision,
first: PAGE_SIZE,
path,
afterCursor: null as string | null,
})),
map: result => {
const ancestors = result.data?.repository?.commit?.ancestors
return {
nextVariables:
ancestors?.pageInfo?.endCursor && ancestors?.pageInfo?.hasNextPage
? { afterCursor: ancestors.pageInfo.endCursor }
: undefined,
data: ancestors?.nodes,
error: result.error,
}
},
merge: (previous, next) => (previous ?? []).concat(next ?? []),
createRestoreStrategy: api =>
new IncrementalRestoreStrategy(
api,
n => n.length,
n => ({ first: n.length })
),
})
return {
changelistsQuery,
path,
}
}

View File

@ -0,0 +1,56 @@
fragment ChangelistsPage_GitCommitConnection on GitCommitConnection {
nodes {
id
oid
abbreviatedOID
canonicalURL
subject
body
externalURLs {
serviceKind
url
}
author {
date
person {
name
email
...Avatar_Person
}
}
committer {
date
person {
name
email
...Avatar_Person
}
}
perforceChangelist {
...Changelist
}
}
pageInfo {
hasNextPage
endCursor
}
}
query ChangelistsPage_ChangelistsQuery(
$depotName: String!
$revision: String!
$first: Int
$path: String
$afterCursor: String
) {
repository(name: $depotName) {
id
commit(rev: $revision) {
id
ancestors(first: $first, afterCursor: $afterCursor, path: $path) {
...ChangelistsPage_GitCommitConnection
}
}
}
}

View File

@ -5,13 +5,12 @@
import { navigating } from '$app/stores'
import Commit from '$lib/Commit.svelte'
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import RepositoryRevPicker from '$lib/repo/RepositoryRevPicker.svelte'
import { getHumanNameForCodeHost } from '$lib/repo/shared/codehost'
import Scroller, { type Capture as ScrollerCapture } from '$lib/Scroller.svelte'
import CodeHostIcon from '$lib/search/CodeHostIcon.svelte'
import { Alert, Badge } from '$lib/wildcard'
import RepositoryRevPicker from '$lib/repo/RepositoryRevPicker.svelte'
import type { PageData, Snapshot } from './$types'
export let data: PageData

View File

@ -48,7 +48,7 @@
export let data: LayoutData
const menuOpen = writable(false)
const navEntries: MenuEntry[] = [
const gitNavEntries: MenuEntry[] = [
{ path: '', icon: ILucideCode, label: 'Code', visibility: 'user', preserveRevision: true },
{
path: '/-/commits',
@ -61,6 +61,22 @@
{ path: '/-/tags', icon: ILucideTag, label: 'Tags', visibility: 'user' },
{ path: '/-/stats/contributors', icon: ILucideUsers, label: 'Contributors', visibility: 'user' },
]
const perforceNavEntries: MenuEntry[] = [
{ path: '', icon: ILucideCode, label: 'Code', visibility: 'user', preserveRevision: true },
{
path: '/-/changelists',
icon: ILucideGitCommitVertical,
label: 'Changelists',
visibility: 'user',
preserveRevision: true,
},
{
path: '/-/stats/contributors',
icon: ILucideUsers,
label: 'Contributors',
visibility: 'user',
},
]
const menuEntries: MenuEntry[] = [
{ path: '/-/compare', icon: ILucideGitCompare, label: 'Compare', visibility: 'user' },
{ path: '/-/own', icon: ILucideUsers, label: 'Ownership', visibility: 'admin' },
@ -85,7 +101,7 @@
setRepositoryPageContext(repositoryContext)
$: viewableNavEntries = navEntries.filter(
$: viewableNavEntries = (data.isPerforceDepot ? perforceNavEntries : gitNavEntries).filter(
entry => entry.visibility === 'user' || (entry.visibility === 'admin' && data.user?.siteAdmin)
)
$: visibleNavEntryCount = viewableNavEntries.length

View File

@ -7,6 +7,7 @@ import { CloneInProgressError, RepoNotFoundError, displayRepoName, parseRepoRevi
import type { LayoutLoad } from './$types'
import {
DepotChangelists,
RepositoryGitCommits,
RepositoryGitRefs,
ResolveRepoRevision,
@ -27,6 +28,7 @@ export const load: LayoutLoad = async ({ params, url, depends }) => {
// An empty revision means we are at the default branch
const { repoName, revision = '' } = parseRepoRevision(params.repo)
const resolvedRepository = await resolveRepoRevision({
client,
repoName,
@ -50,8 +52,11 @@ export const load: LayoutLoad = async ({ params, url, depends }) => {
* - a symbolic revision (e.g. a branch or tag name)
*/
displayRevision: displayRevision(revision, resolvedRepository),
defaultBranch: resolvedRepository.defaultBranch?.abbrevName || 'HEAD',
defaultBranch: resolvedRepository.defaultBranch?.target.commit?.perforceChangelist?.cid
? `changelist/${resolvedRepository.defaultBranch?.target.commit?.perforceChangelist?.cid}`
: resolvedRepository.defaultBranch?.abbrevName || 'HEAD',
resolvedRepository: resolvedRepository,
isPerforceDepot: resolvedRepository.externalRepository.serviceType === 'perforce',
// Repository pickers queries (branch, tags and commits)
getRepoBranches: (searchTerm: string) =>
@ -111,6 +116,26 @@ export const load: LayoutLoad = async ({ params, url, depends }) => {
return { nodes }
})
),
// Depot pickers queries (changelists, @TODO: labels)
getDepotChangelists: (searchTerm: string) =>
client
.query(DepotChangelists, {
depotName: repoName,
query: searchTerm,
revision: resolvedRepository.commit?.oid || '',
})
.then(
mapOrThrow(({ data, error }) => {
let nodes = data?.repository?.ancestorChangelists?.ancestors.nodes ?? []
if (error) {
throw new Error('Could not load depot changelists:', error)
}
return { nodes }
})
),
}
}

View File

@ -0,0 +1,192 @@
<script lang="ts">
import { get } from 'svelte/store'
import { afterNavigate, beforeNavigate } from '$app/navigation'
import { navigating } from '$app/stores'
import Changelist from '$lib/Changelist.svelte'
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import FileDiff from '$lib/repo/FileDiff.svelte'
import Scroller, { type Capture as ScrollerCapture } from '$lib/Scroller.svelte'
import { isViewportMobile } from '$lib/stores'
import Alert from '$lib/wildcard/Alert.svelte'
import Badge from '$lib/wildcard/Badge.svelte'
import CopyButton from '$lib/wildcard/CopyButton.svelte'
import { getRepositoryPageContext } from '../../../context'
import type { PageData, Snapshot } from './$types'
interface Capture {
scroll: ScrollerCapture
diffs?: ReturnType<NonNullable<typeof data.diff>['capture']>
expandedDiffs: Array<[number, boolean]>
}
export let data: PageData
export const snapshot: Snapshot<Capture> = {
capture: () => ({
scroll: scroller.capture(),
diffs: diffQuery?.capture(),
expandedDiffs: expandedDiffsSnapshot,
}),
restore: async capture => {
expandedDiffs = new Map(capture.expandedDiffs)
if (get(navigating)?.type === 'popstate') {
await data.diff?.restore(capture.diffs)
}
scroller.restore(capture.scroll)
},
}
const repositoryContext = getRepositoryPageContext()
let scroller: Scroller
let expandedDiffs = new Map<number, boolean>()
let expandedDiffsSnapshot: Array<[number, boolean]> = []
$: diffQuery = data.diff
$: diffs = $diffQuery?.data
$: cid = data.changelist.cid
afterNavigate(() => {
repositoryContext.set({ revision: data.changelist.commit.oid })
})
beforeNavigate(() => {
expandedDiffsSnapshot = Array.from(expandedDiffs.entries())
expandedDiffs = new Map()
repositoryContext.set({})
})
</script>
<svelte:head>
<title>Changelist: {data.changelist.commit.message ?? ''} - {data.displayRepoName} - Sourcegraph</title>
</svelte:head>
<section>
{#if data.changelist}
<Scroller bind:this={scroller} margin={600} on:more={diffQuery?.fetchMore}>
<div class="header">
<div class="info">
<Changelist changelist={data.changelist} alwaysExpanded={!$isViewportMobile} />
</div>
<ul class="actions">
<li>
<span>Changelist ID:</span>
<Badge variant="secondary"><code>{cid}</code></Badge>&nbsp;<CopyButton value={cid} />
</li>
<li>
<a href="/{data.repoName}@changelist/{cid}"
>Browse files at <Badge variant="link">{cid}</Badge></a
>
</li>
</ul>
</div>
<hr />
{#if diffs}
<ul class="diffs">
{#each diffs as node, index (index)}
<li>
<FileDiff
fileDiff={node}
expanded={expandedDiffs.get(index)}
on:toggle={event => {
expandedDiffs.set(index, event.detail.expanded)
// This is needed to for Svelte to consider that expandedDiffs has changed
expandedDiffs = expandedDiffs
}}
/>
</li>
{/each}
</ul>
{/if}
{#if $diffQuery?.fetching}
<LoadingSpinner />
{:else if $diffQuery?.error}
<div class="error">
<Alert variant="danger">
Unable to fetch file diffs: {$diffQuery.error.message}
</Alert>
</div>
{/if}
</Scroller>
{/if}
</section>
<style lang="scss">
section {
overflow: auto;
isolation: isolate;
}
.header {
display: flex;
margin: 1rem;
@media (--mobile) {
flex-direction: column;
margin: 1rem 0.5rem;
}
}
ul.actions {
--icon-color: currentColor;
all: unset;
list-style: none;
text-align: right;
span,
a {
vertical-align: middle;
}
@media (--mobile) {
display: flex;
flex-flow: row wrap;
gap: 0.5rem;
margin-top: 1rem;
li:not(:first-child)::before {
content: '•';
padding-right: 0.5rem;
color: var(--text-muted);
}
}
}
.info {
flex: 1;
--avatar-size: 2.5rem;
// This seems necessary to ensure that the commit message is
// overlaying the sticky file diff headers on mobile.
z-index: 1;
}
code {
font-family: monospace;
font-size: inherit;
}
.error,
ul.diffs {
margin: 1rem;
@media (--mobile) {
margin: 0rem;
}
}
ul.diffs {
padding: 0;
list-style: none;
li + li {
margin-top: 1rem;
@media (--mobile) {
margin-top: 0;
border-top: 1px solid var(--border-color);
}
}
}
</style>

View File

@ -0,0 +1,65 @@
import { error } from '@sveltejs/kit'
import { IncrementalRestoreStrategy, getGraphQLClient, infinityQuery } from '$lib/graphql'
import type { PageLoad } from './$types'
import { ChangelistPage_ChangelistQuery, ChangelistPage_DiffQuery } from './page.gql'
const PAGE_SIZE = 20
export const load: PageLoad = async ({ params }) => {
const client = getGraphQLClient()
const result = await client.query(ChangelistPage_ChangelistQuery, {
repoName: params.repo,
cid: params.changelistID,
})
if (result.error) {
error(500, `Unable to load commit data: ${result.error}`)
}
const changelist = result.data?.repository?.changelist
if (!changelist) {
error(404, 'Changelist not found')
}
// parents is an empty array for the initial commit
// We currently don't support diffs for the initial commit on the backend
const diff =
changelist.cid && changelist?.commit.parents[0]?.parent?.cid
? infinityQuery({
client,
query: ChangelistPage_DiffQuery,
variables: {
repoName: params.repo,
base: changelist.commit.parents[0].oid,
head: changelist.commit.oid,
first: PAGE_SIZE,
after: null as string | null,
},
map: result => {
const diffs = result.data?.repository?.comparison.fileDiffs
return {
nextVariables: diffs?.pageInfo.hasNextPage ? { after: diffs?.pageInfo.endCursor } : undefined,
data: diffs?.nodes,
error: result.error,
}
},
merge: (previous, next) => (previous ?? []).concat(next ?? []),
createRestoreStrategy: api =>
new IncrementalRestoreStrategy(
api,
n => n.length,
n => ({ first: n.length })
),
})
: null
return {
changelist,
diff,
}
}

View File

@ -0,0 +1,80 @@
query ChangelistPage_ChangelistQuery($repoName: String!, $cid: String!) {
repository(name: $repoName) {
id
changelist(cid: $cid) {
cid
canonicalURL
commit {
message
oid
body
subject
author {
person {
...Avatar_Person
}
date
}
parents {
id
oid
abbreviatedOID
parent: perforceChangelist {
cid
canonicalURL
}
}
perforceChangelist {
cid
canonicalURL
}
}
}
}
}
query ChangelistPage_DiffQuery($repoName: String!, $base: String, $head: String, $first: Int, $after: String) {
repository(name: $repoName) {
id
comparison(base: $base, head: $head) {
fileDiffs(first: $first, after: $after) {
...ChangelistPage_DiffConnection
}
}
}
}
fragment ChangelistPage_DiffConnection on FileDiffConnection {
nodes {
...ChangelistFileDiff_Diff
}
pageInfo {
endCursor
hasNextPage
}
}
fragment ChangelistFileDiff_Diff on FileDiff {
mostRelevantFile {
canonicalURL # key field
url
path
}
newFile {
canonicalURL # key field
path
binary
}
oldFile {
canonicalURL # key field
path
binary
}
stat {
added
deleted
}
hunks {
...FileDiffHunks_Hunk
}
}

View File

@ -1,4 +1,4 @@
import { error } from '@sveltejs/kit'
import { error, redirect } from '@sveltejs/kit'
import { IncrementalRestoreStrategy, getGraphQLClient, infinityQuery } from '$lib/graphql'
import { parseRepoRevision } from '$lib/shared'
@ -8,7 +8,7 @@ import { CommitPage_CommitQuery, CommitPage_DiffQuery } from './page.gql'
const PAGE_SIZE = 20
export const load: PageLoad = async ({ params }) => {
export const load: PageLoad = async ({ url, params }) => {
const client = getGraphQLClient()
const { repoName } = parseRepoRevision(params.repo)
@ -19,11 +19,16 @@ export const load: PageLoad = async ({ params }) => {
}
const commit = result.data?.repository?.commit
if (!commit) {
error(404, 'Commit not found')
}
if (commit.perforceChangelist !== null) {
const redirectURL = new URL(url)
redirectURL.pathname = `${params.repo}/-/changelist/${commit.perforceChangelist?.cid}`
redirect(301, redirectURL)
}
// parents is an empty array for the initial commit
// We currently don't support diffs for the initial commit on the backend
const diff =

View File

@ -10,6 +10,10 @@ query CommitPage_CommitQuery($repoName: String!, $revspec: String!) {
abbreviatedOID
canonicalURL
}
perforceChangelist {
cid
canonicalURL
}
externalURLs {
url
serviceKind

View File

@ -18,6 +18,7 @@ query RepositoryGitCommits($repoName: String!, $query: String!, $revision: Strin
}
}
}
query ResolveRepoRevision($repoName: String!, $revision: String!) {
repositoryRedirect(name: $repoName) {
__typename
@ -34,8 +35,14 @@ fragment ResolvedRepository on Repository {
id
commit(rev: $revision) {
oid
perforceChangelist {
cid
canonicalURL
}
}
changelist(cid: $revision) {
cid
canonicalURL
commit {
oid
}
@ -47,6 +54,14 @@ fragment ResolvedRepository on Repository {
}
defaultBranch {
abbrevName
target {
commit {
perforceChangelist {
cid
canonicalURL
}
}
}
}
externalURLs {
url
@ -56,3 +71,39 @@ fragment ResolvedRepository on Repository {
...BlobPage_ResolvedRevision
...CodySidebar_ResolvedRevision
}
query DepotChangelists($depotName: String!, $revision: String!) {
repository(name: $depotName) {
changelist: commit(rev: $revision) {
...RevPickerChangelist
}
ancestorChangelists: commit(rev: $revision) {
ancestors(first: 15) {
nodes {
...RevPickerChangelist
}
}
}
}
}
query DepotChangelist($depotName: String!, $revision: String!) {
repository(name: $depotName) {
commit(rev: $revision) {
perforceChangelist {
cid
canonicalURL
}
}
}
}
fragment DepotChangelistAuthor on GitCommit {
author {
date
person {
__typename
...Avatar_Person
}
}
}

View File

@ -63,6 +63,7 @@ const defaultMocks: TypeMocks = {
GitCommit: () => ({
abbreviatedOID: faker.git.commitSha({ length: 7 }),
subject: faker.git.commitMessage(),
perforceChangelist: null,
}),
JSONCString: () => '{}',
}

View File

@ -26,6 +26,11 @@ export const svelteKitRoutes: SvelteKitRoute[] = [
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/tree(?:/.*)?/?$'),
isRepoRoot: false,
},
{
id: '/[...repo=reporev]/(validrev)/-/changelists/[...path]',
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/changelists(?:/.*)?/?$'),
isRepoRoot: false,
},
{
id: '/[...repo=reporev]/(validrev)/-/commits/[...path]',
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/commits(?:/.*)?/?$'),
@ -41,6 +46,11 @@ export const svelteKitRoutes: SvelteKitRoute[] = [
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/branches/all/?$'),
isRepoRoot: false,
},
{
id: '/[...repo=reporev]/-/changelist/[changelistID]',
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/changelist(?:/[^/]&#43;)/?$'),
isRepoRoot: false,
},
{
id: '/[...repo=reporev]/-/commit/[...revspec]',
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/commit(?:/.*)?/?$'),

View File

@ -33,6 +33,11 @@ var svelteKitRoutes = []svelteKitRoute{
Pattern: regexp.MustCompile("^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/tree(?:/.*)?/?$"),
Tag: tags.EnableOptIn | tags.EnableRollout,
},
{
Id: "/[...repo=reporev]/(validrev)/-/changelists/[...path]",
Pattern: regexp.MustCompile("^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/changelists(?:/.*)?/?$"),
Tag: tags.EnableOptIn | tags.EnableRollout,
},
{
Id: "/[...repo=reporev]/(validrev)/-/commits/[...path]",
Pattern: regexp.MustCompile("^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/commits(?:/.*)?/?$"),
@ -48,6 +53,11 @@ var svelteKitRoutes = []svelteKitRoute{
Pattern: regexp.MustCompile("^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/branches/all/?$"),
Tag: tags.EnableOptIn | tags.EnableRollout,
},
{
Id: "/[...repo=reporev]/-/changelist/[changelistID]",
Pattern: regexp.MustCompile("^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/changelist(?:/[^/]&#43;)/?$"),
Tag: tags.EnableOptIn,
},
{
Id: "/[...repo=reporev]/-/commit/[...revspec]",
Pattern: regexp.MustCompile("^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/commit(?:/.*)?/?$"),