mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 09:11:56 +00:00
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:**  **For Git Repos:**  ### 2. Tabs on Revision Picker **For Perforce Depots:** Since we only need one tab for changelists, no tabs are shown.  **For Git Repos:** We have tabs for Branches, Tags, and Commits.  ### 3. Commits/Changelists Page **For Git Repos:** The page displays Git commits.  **For Perforce Depots:** The page displays Perforce changelists.  ### 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:**  - **For Perforce Changelists:**  ### 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:**  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:
parent
f403bfc1ff
commit
cff1669bc1
29
client/web-sveltekit/src/lib/Changelist.gql
Normal file
29
client/web-sveltekit/src/lib/Changelist.gql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
157
client/web-sveltekit/src/lib/Changelist.svelte
Normal file
157
client/web-sveltekit/src/lib/Changelist.svelte
Normal 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>
|
||||
@ -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}
|
||||
|
||||
@ -40,3 +40,14 @@ fragment RepositoryGitRevAuthor on GitCommit {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment RevPickerChangelist on GitCommit {
|
||||
__typename
|
||||
id
|
||||
subject
|
||||
perforceChangelist {
|
||||
cid
|
||||
canonicalURL
|
||||
}
|
||||
...DepotChangelistAuthor
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(?:/[^/]+)/?$'),
|
||||
isRepoRoot: false,
|
||||
},
|
||||
{
|
||||
id: '/[...repo=reporev]/-/commit/[...revspec]',
|
||||
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/commit(?:/.*)?/?$'),
|
||||
|
||||
@ -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')}>
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
})
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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> <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>
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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 =
|
||||
|
||||
@ -10,6 +10,10 @@ query CommitPage_CommitQuery($repoName: String!, $revspec: String!) {
|
||||
abbreviatedOID
|
||||
canonicalURL
|
||||
}
|
||||
perforceChangelist {
|
||||
cid
|
||||
canonicalURL
|
||||
}
|
||||
externalURLs {
|
||||
url
|
||||
serviceKind
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +63,7 @@ const defaultMocks: TypeMocks = {
|
||||
GitCommit: () => ({
|
||||
abbreviatedOID: faker.git.commitSha({ length: 7 }),
|
||||
subject: faker.git.commitMessage(),
|
||||
perforceChangelist: null,
|
||||
}),
|
||||
JSONCString: () => '{}',
|
||||
}
|
||||
|
||||
@ -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(?:/[^/]+)/?$'),
|
||||
isRepoRoot: false,
|
||||
},
|
||||
{
|
||||
id: '/[...repo=reporev]/-/commit/[...revspec]',
|
||||
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/commit(?:/.*)?/?$'),
|
||||
|
||||
@ -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(?:/[^/]+)/?$"),
|
||||
Tag: tags.EnableOptIn,
|
||||
},
|
||||
{
|
||||
Id: "/[...repo=reporev]/-/commit/[...revspec]",
|
||||
Pattern: regexp.MustCompile("^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/commit(?:/.*)?/?$"),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user