feat: show PR file diff

This commit is contained in:
EnixCoda 2021-12-24 17:12:13 +08:00
parent 916a120d22
commit cdabd7a51f
11 changed files with 249 additions and 19 deletions

View file

@ -0,0 +1,37 @@
import * as React from 'react'
import { resolveDiffGraphMeta } from 'utils/general'
import { Icon } from './Icon'
export function DiffStatGraph({
diff: { status, changes, additions, deletions },
}: {
diff: Required<TreeNode>['diff']
}) {
const { g, r, w } = resolveDiffGraphMeta(additions, deletions, changes)
const children: React.ReactNode[] = []
for (let i = 0; i < g; i++)
children.push(<span key={`g-${i}`} className="diff-stat-graph-addition" />)
for (let i = 0; i < r; i++)
children.push(<span key={`r-${i}`} className="diff-stat-graph-deletion" />)
for (let i = 0; i < w; i++)
children.push(<span key={`w-${i}`} className="diff-stat-graph-no-change" />)
return (
<span className={'diff-stat-graph'}>
<Icon
className={status}
type={
{
added: 'diffAdded',
ignored: 'diffIgnored',
modified: 'diffModified',
removed: 'diffRemoved',
renamed: 'diffRenamed',
}[status]
}
/>
{children}
</span>
)
}

View file

@ -0,0 +1,34 @@
import * as React from 'react'
import { Icon } from './Icon'
export function DiffStatText({
diff: { status, changes, additions, deletions },
}: {
diff: Required<TreeNode>['diff']
}) {
return (
<span className={'diff-stat-text'}>
{status !== 'modified' && (
<Icon
className={status}
type={
{
added: 'diffAdded',
ignored: 'diffIgnored',
// modified: 'diffModified', // hide modified icon
removed: 'diffRemoved',
renamed: 'diffRenamed',
}[status]
}
/>
)}
{additions > 0 && (
<span className={'additions'}>{status === 'modified' ? `+${additions}` : additions}</span>
)}
{additions > 0 && deletions > 0 && '/'}
{deletions > 0 && (
<span className={'deletions'}>{status === 'modified' ? `-${deletions}` : deletions}</span>
)}
</span>
)
}

View file

@ -17,6 +17,8 @@ import { useOnLocationChange } from 'utils/hooks/useOnLocationChange'
import { useOnPJAXDone } from 'utils/hooks/usePJAX'
import { VisibleNodes } from 'utils/VisibleNodesGenerator'
import { SideBarStateContext } from '../containers/SideBarState'
import { DiffStatGraph } from './DiffStatGraph'
import { DiffStatText } from './DiffStatText'
import { Icon } from './Icon'
import { SearchMode, searchModes } from './searchModes'
import { SizeObserver } from './SizeObserver'
@ -52,6 +54,7 @@ const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplor
searchMode,
commentToggle,
restoreExpandedFolders,
showDiffInText,
},
} = useConfigs()
@ -121,16 +124,26 @@ const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplor
<Icon type={'comment'} /> {node.comments > 9 ? '9+' : node.comments}
</span>
)
const renderFileStatus = ({ diff }: TreeNode): React.ReactNode =>
diff && (
<span
className={'node-item-diff'}
title={`${diff.status}, ${diff.changes} changes: +${diff.additions} & -${diff.deletions}`}
>
{showDiffInText ? <DiffStatText diff={diff} /> : <DiffStatGraph diff={diff} />}
</span>
)
const renders: ((node: TreeNode) => React.ReactNode)[] = []
if (commentToggle) renders.push(renderFileCommentAmounts)
renders.push(renderFileStatus)
if (searchMode === 'fuzzy') renders.push(renderFindInFolderButton)
if (searched) renders.push(renderGoToButton)
return renders.length
? node => renders.map((render, i) => <React.Fragment key={i}>{render(node)}</React.Fragment>)
: undefined
}, [goTo, onSearch, searched, searchMode, commentToggle])
}, [goTo, onSearch, searched, searchMode, commentToggle, showDiffInText])
const renderLabelText = React.useCallback(
(node: TreeNode) => searchModes[searchMode].renderNodeLabelText(node, searchKey),

View file

@ -3,6 +3,12 @@ import {
ChevronRightIcon as ChevronRight,
ClockIcon as Clock,
CommentIcon as Comment,
DiffAddedIcon as DiffAdded,
DiffIcon as Diff,
DiffIgnoredIcon as DiffIgnored,
DiffModifiedIcon as DiffModified,
DiffRemovedIcon as DiffRemoved,
DiffRenamedIcon as DiffRenamed,
FileCodeIcon as FileCode,
FileIcon as File,
FileMediaIcon as FileMedia,
@ -18,7 +24,7 @@ import {
ReplyIcon as Reply,
SearchIcon as Search,
TabIcon as Tab,
XIcon as X,
XIcon as X
} from '@primer/octicons-react'
import * as React from 'react'
import { cx } from 'utils/cx'
@ -27,6 +33,12 @@ const iconToComponentMap = {
Search,
Clock,
Comment,
Diff,
DiffAdded,
DiffIgnored,
DiffModified,
DiffRemoved,
DiffRenamed,
Hourglass,
Submodule,
Grabber,
@ -60,6 +72,12 @@ const typeToIconComponentMap: {
pin: 'Pin',
tab: 'Tab',
gear: 'Gear',
diff: 'Diff',
diffAdded: 'DiffAdded',
diffIgnored: 'DiffIgnored',
diffModified: 'DiffModified',
diffRemoved: 'DiffRemoved',
diffRenamed: 'DiffRenamed',
folder: 'ChevronRight',
'chevron-down': 'ChevronDown',
'go-to': 'Reply',

View file

@ -90,6 +90,13 @@ export function FileTreeSettings(props: React.PropsWithChildren<Props>) {
tooltip: 'Show number of comments next to file names in Pull Requests.',
}}
/>
<SimpleToggleField
field={{
key: 'showDiffInText',
label: 'Show PR file diff in text',
tooltip: 'Glance diff in a more precise way.',
}}
/>
<SimpleToggleField
field={{
key: 'compactFileTree',

6
src/global.d.ts vendored
View file

@ -17,6 +17,12 @@ type TreeNode = {
sha?: string
accessDenied?: boolean
comments?: number
diff?: {
status: 'modified' | 'added' | 'removed' | 'renamed'
additions: number
deletions: number
changes: number
}
}
type IO<T, ChangeT = T> = {

View file

@ -323,14 +323,22 @@ async function getPullRequestTreeData(
}
const urlMainPart = `https://${window.location.host}/${userName}/${repoName}/pull/${pullId}/files${window.location.search}`
const nodes: TreeNode[] = treeData.map(item => ({
path: item.filename || '',
type: 'blob',
name: item.filename?.replace(/^.*\//, '') || '',
url: `${urlMainPart}${formatHash(getFileElementHash(item.filename))}`,
sha: item.sha,
comments: commentData?.filter(comment => item.filename === comment.path).length,
}))
const nodes: TreeNode[] = treeData.map(
({ filename, sha, additions, deletions, changes, status }) => ({
path: filename || '',
type: 'blob',
name: filename?.replace(/^.*\//, '') || '',
url: `${urlMainPart}${formatHash(getFileElementHash(filename))}`,
sha: sha,
comments: commentData?.filter(comment => filename === comment.path).length,
diff: {
status,
additions,
deletions,
changes,
},
}),
)
const root = processTree(nodes)
return { root }

View file

@ -81,7 +81,6 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
}
}
.#{$name}-ready {
// github
// code folding start
@ -732,8 +731,9 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
padding-right: 8px;
.node-item-comment {
display: inline-block;
width: 48px;
display: inline-flex;
align-items: center;
min-width: 36px;
padding: 0 4px;
color: var(--gitako-fg-muted);
@ -742,6 +742,69 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
}
}
.node-item-diff {
display: inline-flex;
align-items: center;
padding: 0 4px;
color: var(--gitako-fg-muted);
.octicon-wrapper {
margin: 0; // make it closer to the diff details
}
.added .octicon {
color: var(--gitako-success-emphasis);
}
.removed .octicon {
color: var(--gitako-danger-emphasis);
}
.renamed .octicon {
color: var(--gitako-done-emphasis);
}
.modified .octicon {
color: var(--gitako-accent-emphasis);
}
.ignored .octicon {
color: var(--gitako-neutral-emphasis);
}
.diff-stat-graph {
display: inline-block;
white-space: nowrap;
> span {
display: inline-block;
width: 2px;
height: 10px;
margin: 0 1px;
border-radius: 2px;
background: var(--gitako-neutral-emphasis);
}
.diff-stat-graph-addition {
background: var(--gitako-success-emphasis);
}
.diff-stat-graph-deletion {
background: var(--gitako-danger-emphasis);
}
}
.diff-stat-text {
display: inline-block;
white-space: nowrap;
font-family: 'Cascadia Code', 'Courier New', Courier, monospace; // Use some commonly available monospace font
.additions {
color: var(--gitako-success-emphasis);
}
.delimiter {
color: var(--gitako-neutral-emphasis);
}
.deletions {
color: var(--gitako-danger-emphasis);
}
}
}
.go-to-button,
.find-in-folder-button {
@include icon-button();

View file

@ -20,6 +20,7 @@ export type Config = {
codeFolding: boolean
compactFileTree: boolean
restoreExpandedFolders: boolean
showDiffInText: boolean
}
enum configKeys {
@ -40,6 +41,7 @@ enum configKeys {
codeFolding = 'codeFolding',
compactFileTree = 'compactFileTree',
restoreExpandedFolders = 'restoreExpandedFolders',
showDiffInText = 'showDiffInText',
}
// do NOT use platform name
@ -63,6 +65,7 @@ export const defaultConfigs: Config = {
codeFolding: true,
compactFileTree: false,
restoreExpandedFolders: true,
showDiffInText: false,
}
const configKeyArray = Object.values(configKeys)

29
src/utils/general.test.ts Normal file
View file

@ -0,0 +1,29 @@
import { resolveDiffGraphMeta } from './general'
it(`should resolve diff stat graph meta properly`, () => {
const example = `
2 10 0 4
3 10 1 3
3 17 0 4
4 17 0 4
4 26 0 4
5 17 1 3
6 17 1 3
12 0 5 0
0 12 0 5
17 74 0 4
11 23 1 3
18 28 1 3
34 24 2 2
`
example
.split(/\n/)
.map(line => line.trim())
.filter(line => line.length)
.map(line => line.split(/\s+/).map(_ => parseInt(_)))
.forEach(([additions, deletions, g, r]) => {
const meta = resolveDiffGraphMeta(additions, deletions, additions + deletions)
expect([meta.g, meta.r]).toEqual([g, r])
})
})

View file

@ -21,12 +21,14 @@ export enum OperatingSystems {
}
function detectOS(): OperatingSystems {
const {
navigator: { userAgent },
} = window
if (userAgent.indexOf(OperatingSystems.Windows) !== -1) return OperatingSystems.Windows
else if (userAgent.indexOf(OperatingSystems.macOS) !== -1) return OperatingSystems.macOS
else if (userAgent.indexOf(OperatingSystems.Linux) !== -1) return OperatingSystems.Linux
if (typeof window !== 'undefined') {
const {
navigator: { userAgent },
} = window
if (userAgent.indexOf(OperatingSystems.Windows) !== -1) return OperatingSystems.Windows
else if (userAgent.indexOf(OperatingSystems.macOS) !== -1) return OperatingSystems.macOS
else if (userAgent.indexOf(OperatingSystems.Linux) !== -1) return OperatingSystems.Linux
}
return OperatingSystems.others
}
@ -200,3 +202,13 @@ export function isOpenInNewWindowClick(event: React.MouseEvent<HTMLElement, Mous
(os === OperatingSystems.Windows && event.ctrlKey)
)
}
export function resolveDiffGraphMeta(additions: number, deletions: number, changes: number) {
const both = additions > 0 && deletions > 0,
overflow = changes > 5,
preserved = both && overflow ? 1 : 0,
g = overflow ? Math.floor(((5 - preserved) * (additions + 1)) / (changes + 1)) : additions,
r = overflow ? 5 - preserved - g : deletions,
w = 5 - g - r
return { g, r, w }
}