mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: show PR file diff
This commit is contained in:
parent
916a120d22
commit
cdabd7a51f
11 changed files with 249 additions and 19 deletions
37
src/components/DiffStatGraph.tsx
Normal file
37
src/components/DiffStatGraph.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
34
src/components/DiffStatText.tsx
Normal file
34
src/components/DiffStatText.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
6
src/global.d.ts
vendored
|
|
@ -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> = {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
29
src/utils/general.test.ts
Normal 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])
|
||||
})
|
||||
})
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue