Skip to content

Commit c68c7f8

Browse files
committed
refactor: resturcuture
1 parent 7b61ff0 commit c68c7f8

File tree

10 files changed

+291
-261
lines changed

10 files changed

+291
-261
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { exec } from 'tinyexec'
2+
3+
export interface GitResult {
4+
stdout: string
5+
stderr: string
6+
ok: boolean
7+
}
8+
9+
export async function git(args: string[], cwd: string): Promise<GitResult> {
10+
try {
11+
const result = await exec('git', args, { nodeOptions: { cwd }, throwOnError: true })
12+
return { stdout: result.stdout, stderr: result.stderr, ok: true }
13+
}
14+
catch (e: any) {
15+
return { stdout: e.stdout ?? '', stderr: e.stderr ?? String(e), ok: false }
16+
}
17+
}
18+
19+
export interface GitState {
20+
branch: string
21+
commits: Array<{ hash: string, message: string, author: string, date: string }>
22+
staged: Array<{ status: string, file: string }>
23+
unstaged: Array<{ status: string, file: string }>
24+
}
25+
26+
export async function getGitState(gitRoot: string): Promise<GitState> {
27+
const [branchResult, logResult, statusResult] = await Promise.all([
28+
git(['branch', '--show-current'], gitRoot),
29+
git(['log', '--oneline', '-20', '--format=%h\t%s\t%an\t%cr'], gitRoot),
30+
git(['status', '--porcelain'], gitRoot),
31+
])
32+
const branch = branchResult.stdout
33+
const log = logResult.stdout
34+
const status = statusResult.stdout
35+
36+
const staged: GitState['staged'] = []
37+
const unstaged: GitState['unstaged'] = []
38+
for (const line of status.split('\n').filter(Boolean)) {
39+
const x = line[0] ?? ' '
40+
const y = line[1] ?? ' '
41+
const file = line.slice(3)
42+
if (x !== ' ' && x !== '?')
43+
staged.push({ status: x, file })
44+
if (y !== ' ' && y !== '?')
45+
unstaged.push({ status: y, file })
46+
if (x === '?')
47+
unstaged.push({ status: '?', file })
48+
}
49+
50+
return {
51+
branch: branch.trim(),
52+
commits: log.split('\n').filter(Boolean).map((l) => {
53+
const parts = l.split('\t')
54+
return { hash: parts[0] ?? '', message: parts[1] ?? '', author: parts[2] ?? '', date: parts[3] ?? '' }
55+
}),
56+
staged,
57+
unstaged,
58+
}
59+
}

examples/plugin-git-ui/src/node/plugin.ts

Lines changed: 4 additions & 246 deletions
Original file line numberDiff line numberDiff line change
@@ -1,250 +1,8 @@
1-
import type { JsonRenderElement, JsonRenderer, JsonRenderSpec, PluginWithDevTools } from '@vitejs/devtools-kit'
2-
import { defineJsonRenderSpec, defineRpcFunction } from '@vitejs/devtools-kit'
1+
import type { JsonRenderer, PluginWithDevTools } from '@vitejs/devtools-kit'
2+
import { defineRpcFunction } from '@vitejs/devtools-kit'
33
import { exec } from 'tinyexec'
4-
5-
interface GitResult {
6-
stdout: string
7-
stderr: string
8-
ok: boolean
9-
}
10-
11-
async function git(args: string[], cwd: string): Promise<GitResult> {
12-
try {
13-
const result = await exec('git', args, { nodeOptions: { cwd }, throwOnError: true })
14-
return { stdout: result.stdout, stderr: result.stderr, ok: true }
15-
}
16-
catch (e: any) {
17-
return { stdout: e.stdout ?? '', stderr: e.stderr ?? String(e), ok: false }
18-
}
19-
}
20-
21-
interface GitState {
22-
branch: string
23-
commits: Array<{ hash: string, message: string, author: string, date: string }>
24-
staged: Array<{ status: string, file: string }>
25-
unstaged: Array<{ status: string, file: string }>
26-
}
27-
28-
async function getGitState(gitRoot: string): Promise<GitState> {
29-
const [branchResult, logResult, statusResult] = await Promise.all([
30-
git(['branch', '--show-current'], gitRoot),
31-
git(['log', '--oneline', '-20', '--format=%h\t%s\t%an\t%cr'], gitRoot),
32-
git(['status', '--porcelain'], gitRoot),
33-
])
34-
const branch = branchResult.stdout
35-
const log = logResult.stdout
36-
const status = statusResult.stdout
37-
38-
const staged: GitState['staged'] = []
39-
const unstaged: GitState['unstaged'] = []
40-
for (const line of status.split('\n').filter(Boolean)) {
41-
const x = line[0]
42-
const y = line[1]
43-
const file = line.slice(3)
44-
if (x !== ' ' && x !== '?')
45-
staged.push({ status: x, file })
46-
if (y !== ' ' && y !== '?')
47-
unstaged.push({ status: y, file })
48-
if (x === '?')
49-
unstaged.push({ status: '?', file })
50-
}
51-
52-
return {
53-
branch: branch.trim(),
54-
commits: log.split('\n').filter(Boolean).map((l) => {
55-
const [hash, message, author, date] = l.split('\t')
56-
return { hash, message, author, date }
57-
}),
58-
staged,
59-
unstaged,
60-
}
61-
}
62-
63-
function buildFileRows(
64-
files: Array<{ status: string, file: string }>,
65-
prefix: string,
66-
actionName: string,
67-
actionIcon: string,
68-
): { children: string[], elements: Record<string, JsonRenderElement> } {
69-
const children: string[] = []
70-
const elements: Record<string, JsonRenderElement> = {}
71-
72-
for (let i = 0; i < files.length; i++) {
73-
const { status, file } = files[i]
74-
const rowId = `${prefix}-row-${i}`
75-
const statusId = `${prefix}-status-${i}`
76-
const fileId = `${prefix}-file-${i}`
77-
const btnId = `${prefix}-btn-${i}`
78-
79-
children.push(rowId)
80-
elements[rowId] = {
81-
type: 'Stack',
82-
props: { direction: 'horizontal', gap: 8, align: 'center' },
83-
children: [statusId, fileId, btnId],
84-
}
85-
elements[statusId] = {
86-
type: 'Badge',
87-
props: {
88-
text: status,
89-
variant: status === '?' ? 'warning' : status === 'D' ? 'error' : 'info',
90-
},
91-
}
92-
elements[fileId] = {
93-
type: 'Text',
94-
props: { content: file, variant: 'code' },
95-
}
96-
elements[btnId] = {
97-
type: 'Button',
98-
props: { icon: actionIcon, variant: 'ghost' },
99-
on: { press: { action: actionName, params: { file } } },
100-
}
101-
}
102-
103-
return { children, elements }
104-
}
105-
106-
function buildSpec(gitState: GitState): JsonRenderSpec {
107-
const stagedRows = buildFileRows(gitState.staged, 'staged', 'git-ui:unstage', 'ph:minus-circle')
108-
const unstagedRows = buildFileRows(gitState.unstaged, 'unstaged', 'git-ui:stage', 'ph:plus-circle')
109-
110-
return defineJsonRenderSpec({
111-
root: 'root',
112-
state: {
113-
commitMessage: '',
114-
},
115-
elements: {
116-
'root': {
117-
type: 'Stack',
118-
props: { direction: 'vertical', gap: 12, padding: 4 },
119-
children: ['header', 'branch-info', 'commit-section', 'divider1', 'staged-card', 'unstaged-card', 'commits-card'],
120-
},
121-
'header': {
122-
type: 'Stack',
123-
props: { direction: 'horizontal', gap: 8, align: 'center', justify: 'space-between' },
124-
children: ['title', 'refresh-btn'],
125-
},
126-
'title': {
127-
type: 'Text',
128-
props: { content: 'Git', variant: 'heading' },
129-
},
130-
'refresh-btn': {
131-
type: 'Button',
132-
props: { label: 'Refresh', variant: 'secondary', icon: 'ph:arrows-clockwise' },
133-
on: { press: { action: 'git-ui:refresh' } },
134-
},
135-
'branch-info': {
136-
type: 'Stack',
137-
props: { direction: 'horizontal', gap: 8, align: 'center' },
138-
children: ['branch-icon', 'branch-text', 'changes-badge'],
139-
},
140-
'branch-icon': {
141-
type: 'Icon',
142-
props: { name: 'ph:git-branch', size: 16 },
143-
},
144-
'branch-text': {
145-
type: 'Text',
146-
props: { content: gitState.branch || '(detached)', variant: 'code' },
147-
},
148-
'changes-badge': {
149-
type: 'Badge',
150-
props: {
151-
text: `${gitState.staged.length + gitState.unstaged.length} changes`,
152-
variant: (gitState.staged.length + gitState.unstaged.length) > 0 ? 'warning' : 'success',
153-
},
154-
},
155-
'commit-section': {
156-
type: 'Stack',
157-
props: { direction: 'horizontal', gap: 8 },
158-
children: ['commit-input', 'commit-btn'],
159-
},
160-
'commit-input': {
161-
type: 'TextInput',
162-
props: {
163-
placeholder: 'Commit message...',
164-
value: { $bindState: '/commitMessage' } as any,
165-
},
166-
},
167-
'commit-btn': {
168-
type: 'Button',
169-
props: { label: 'Commit', variant: 'primary', icon: 'ph:check' },
170-
on: {
171-
press: {
172-
action: 'git-ui:commit',
173-
params: { message: { $state: '/commitMessage' } },
174-
},
175-
},
176-
},
177-
'divider1': {
178-
type: 'Divider',
179-
props: {},
180-
},
181-
182-
// Staged files
183-
'staged-card': {
184-
type: 'Card',
185-
props: { title: `Staged (${gitState.staged.length})`, collapsible: true },
186-
children: gitState.staged.length > 0 ? ['staged-files'] : ['staged-empty'],
187-
},
188-
'staged-files': {
189-
type: 'Stack',
190-
props: { direction: 'vertical', gap: 4 },
191-
children: stagedRows.children,
192-
},
193-
...stagedRows.elements,
194-
'staged-empty': {
195-
type: 'Text',
196-
props: { content: 'No staged files', variant: 'caption' },
197-
},
198-
199-
// Unstaged files
200-
'unstaged-card': {
201-
type: 'Card',
202-
props: { title: `Unstaged (${gitState.unstaged.length})`, collapsible: true },
203-
children: gitState.unstaged.length > 0 ? ['unstaged-header', 'unstaged-files'] : ['unstaged-empty'],
204-
},
205-
'unstaged-header': {
206-
type: 'Stack',
207-
props: { direction: 'horizontal', justify: 'end' },
208-
children: ['stage-all-btn'],
209-
},
210-
'stage-all-btn': {
211-
type: 'Button',
212-
props: { label: 'Stage All', variant: 'secondary', icon: 'ph:plus-circle' },
213-
on: { press: { action: 'git-ui:stage-all' } },
214-
},
215-
'unstaged-files': {
216-
type: 'Stack',
217-
props: { direction: 'vertical', gap: 4 },
218-
children: unstagedRows.children,
219-
},
220-
...unstagedRows.elements,
221-
'unstaged-empty': {
222-
type: 'Text',
223-
props: { content: 'No unstaged files', variant: 'caption' },
224-
},
225-
226-
// Commits
227-
'commits-card': {
228-
type: 'Card',
229-
props: { title: 'Recent Commits', collapsible: true },
230-
children: ['commits-table'],
231-
},
232-
'commits-table': {
233-
type: 'DataTable',
234-
props: {
235-
columns: [
236-
{ key: 'hash', label: 'Hash', width: '80px' },
237-
{ key: 'message', label: 'Message' },
238-
{ key: 'author', label: 'Author', width: '120px' },
239-
{ key: 'date', label: 'Date', width: '100px' },
240-
],
241-
rows: gitState.commits,
242-
maxHeight: '300px',
243-
},
244-
},
245-
},
246-
})
247-
}
4+
import { getGitState, git } from './git'
5+
import { buildSpec } from './spec'
2486

2497
async function refreshUi(ctx: { cwd: string, docks: any }, ui: JsonRenderer) {
2508
const gitState = await getGitState(ctx.cwd)

0 commit comments

Comments
 (0)