Skip to content

Commit 8fd7b38

Browse files
committed
feat: file diff tree with folder
1 parent c64e003 commit 8fd7b38

File tree

6 files changed

+323
-22
lines changed

6 files changed

+323
-22
lines changed

demo.html

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Git Graph</title>
8+
<style>
9+
body {
10+
background: #2b2b2b;
11+
margin: 0;
12+
padding: 20px;
13+
}
14+
15+
.commit-node {
16+
fill: #4a9eff;
17+
}
18+
19+
.commit-node-develop {
20+
fill: #ffeb3b;
21+
}
22+
23+
.branch-line {
24+
stroke-width: 2;
25+
fill: none;
26+
}
27+
28+
.main-branch {
29+
stroke: #4a9eff;
30+
}
31+
32+
.develop-branch {
33+
stroke: #ffeb3b;
34+
}
35+
36+
.commit-text {
37+
font-family: Arial, sans-serif;
38+
font-size: 12px;
39+
fill: #fff;
40+
}
41+
42+
.branch-label {
43+
font-family: Arial, sans-serif;
44+
font-size: 14px;
45+
font-weight: bold;
46+
}
47+
48+
.main-label {
49+
fill: #4a9eff;
50+
}
51+
52+
.develop-label {
53+
fill: #ffeb3b;
54+
}
55+
56+
.git-table {
57+
border-collapse: collapse;
58+
width: 100%;
59+
position: relative;
60+
}
61+
62+
.git-table td {
63+
padding: 0;
64+
height: 30px;
65+
position: relative;
66+
}
67+
68+
.git-table td:hover {
69+
background-color: rgba(255, 255, 255, 0.3);
70+
}
71+
72+
.git-cell {
73+
width: 300px;
74+
height: 30px;
75+
position: relative;
76+
}
77+
78+
.branch-svg {
79+
position: absolute;
80+
top: 0;
81+
left: 0;
82+
pointer-events: none;
83+
z-index: 1;
84+
}
85+
86+
.commit-container {
87+
position: relative;
88+
z-index: 2;
89+
}
90+
</style>
91+
</head>
92+
93+
<body>
94+
<div id="app">
95+
<table class="git-table" id="gitTable"></table>
96+
97+
<div id="branchSvg" style="position:relative;width: 500px;height: 500px;">
98+
<svg style="position: absolute;" width="100%" height="100%" viewBox="0 0 500 500">
99+
<text x="10" y="10" class="branch-label main-label" style="z-index: 3;" font-size="10" fill="blue">Main</text>
100+
<text x="30" y="40" class="branch-label develop-label" style="z-index: 3;" font-size="10" fill="yellow">Develop</text>
101+
<path d="M 10 0 V 1800" stroke="blue" stroke-width="2" fill="none" />
102+
<path d="M 10 40 H 30 Q 40 40 40 50 V 210 Q 40 220 30 220 H 10" stroke="yellow" stroke-width="2" fill="none" />
103+
</svg>
104+
<svg style="position: absolute;" width="100%" height="100%" viewBox="0 0 500 500">
105+
<circle cx="10" cy="10" r="10" fill="blue" />
106+
<circle cx="10" cy="40" r="10" fill="blue" />
107+
<circle cx="40" cy="70" r="10" fill="yellow" />
108+
<circle cx="40" cy="100" r="10" fill="yellow" />
109+
<circle cx="40" cy="130" r="10" fill="yellow" />
110+
<circle cx="40" cy="160" r="10" fill="yellow" />
111+
<circle cx="40" cy="190" r="10" fill="yellow" />
112+
113+
<circle cx="10" cy="220" r="10" fill="blue" />
114+
115+
<!-- <circle cx="30" cy="210" r="10" fill="red" /> -->
116+
</svg>
117+
</div>
118+
</div>
119+
<script>
120+
const gitCommits = [
121+
"commit",
122+
"commit",
123+
"branch develop",
124+
"checkout develop",
125+
"commit",
126+
"commit",
127+
"checkout main",
128+
"commit",
129+
"commit",
130+
"checkout develop",
131+
"commit",
132+
"commit",
133+
"commit",
134+
"checkout main",
135+
"merge develop",
136+
"commit",
137+
"commit",
138+
"commit",
139+
];
140+
141+
function createBranchSvg() {
142+
const height = gitCommits.length * 30;
143+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
144+
svg.setAttribute("class", "branch-svg");
145+
svg.setAttribute("width", "300");
146+
svg.setAttribute("height", height.toString());
147+
148+
// 主分支线
149+
const mainLine = document.createElementNS("http://www.w3.org/2000/svg", "path");
150+
mainLine.setAttribute("class", "branch-line main-branch");
151+
mainLine.setAttribute("d", `M50,15 L50,${height}`);
152+
svg.appendChild(mainLine);
153+
154+
// 开发分支线和合并线
155+
const developStartY = 75;
156+
const developEndY = height - 120;
157+
158+
// 分支线 - 使用简单圆角
159+
const developLine = document.createElementNS("http://www.w3.org/2000/svg", "path");
160+
developLine.setAttribute("class", "branch-line develop-branch");
161+
developLine.setAttribute("d", `M50,${developStartY} Q100,${developStartY} 150,${developStartY + 30} L150,${developEndY}`);
162+
svg.appendChild(developLine);
163+
164+
// 合并线 - 使用简单圆角
165+
const mergeLine = document.createElementNS("http://www.w3.org/2000/svg", "path");
166+
mergeLine.setAttribute("class", "branch-line develop-branch");
167+
mergeLine.setAttribute("d", `M150,${developEndY} Q100,${developEndY + 30} 50,${developEndY + 30}`);
168+
svg.appendChild(mergeLine);
169+
170+
return svg;
171+
}
172+
173+
function createCommitNode(x, isDevelop = false) {
174+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
175+
svg.setAttribute("width", "300");
176+
svg.setAttribute("height", "30");
177+
178+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
179+
circle.setAttribute("cx", x.toString());
180+
circle.setAttribute("cy", "15");
181+
circle.setAttribute("r", "6");
182+
circle.setAttribute("class", isDevelop ? "commit-node-develop" : "commit-node");
183+
184+
svg.appendChild(circle);
185+
return svg;
186+
}
187+
188+
function createGitGraph() {
189+
const table = document.getElementById('gitTable');
190+
const branchSvg = createBranchSvg();
191+
document.getElementById('app').insertBefore(branchSvg, table);
192+
193+
let skipNext = false;
194+
195+
gitCommits.forEach((commit, index) => {
196+
// 跳过分支切换的命令行
197+
if (skipNext) {
198+
skipNext = false;
199+
return;
200+
}
201+
202+
if (commit.includes('branch') || commit.includes('checkout')) {
203+
skipNext = true;
204+
return;
205+
}
206+
207+
const row = document.createElement('tr');
208+
const cell = document.createElement('td');
209+
const container = document.createElement('div');
210+
container.className = 'git-cell';
211+
212+
if (commit === "commit") {
213+
// 确定当前提交属于哪个分支
214+
let isDevelop = false;
215+
for (let i = 0; i <= index; i++) {
216+
if (gitCommits[i].includes('checkout')) {
217+
isDevelop = gitCommits[i].split(' ')[1] === 'develop';
218+
}
219+
}
220+
221+
// 根据是否在develop分支上调整x坐标
222+
const x = isDevelop ? 150 : 50;
223+
const commitNode = createCommitNode(x, isDevelop);
224+
container.appendChild(commitNode);
225+
}
226+
227+
cell.appendChild(container);
228+
row.appendChild(cell);
229+
table.appendChild(row);
230+
});
231+
}
232+
233+
// createGitGraph();
234+
</script>
235+
</body>
236+
237+
</html>

src/git/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class GitService {
1515
throw new Error('No workspace folder found. Please open a folder first.')
1616

1717
this.rootRepoPath = vscode.workspace.workspaceFolders[0].uri.fsPath
18-
18+
1919
try {
2020
this._git = simpleGit(this.rootRepoPath, {
2121
binary: 'git',
@@ -33,7 +33,11 @@ export class GitService {
3333

3434
async getHistory(): Promise<ExtendedLogResult> {
3535
try {
36-
const logResult = await this.git.log(['--max-count=100']) as ExtendedLogResult
36+
const logResult = await this.git.log([
37+
'--all',
38+
'--max-count=100',
39+
'--decorate=full',
40+
]) as ExtendedLogResult
3741

3842
// Get stats for each commit
3943
for (const commit of logResult.all) {

src/git/types.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import type { ListLogLine, LogResult } from 'simple-git'
22

3+
export interface CommitFile {
4+
path: string
5+
status: string
6+
}
7+
38
export interface Commit extends ListLogLine {
49
hash: string
510
authorName: string
@@ -12,10 +17,7 @@ export interface Commit extends ListLogLine {
1217
additions: number
1318
deletions: number
1419
}
15-
files?: Array<{
16-
path: string
17-
status: string
18-
}>
20+
files?: Array<CommitFile>
1921
}
2022

2123
export interface CommitStats {

src/views/diff/FileTreeProvider.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FileNode } from './entity/FileNode'
2-
import type { GitService } from '@/git'
2+
import { FolderNode } from './entity/FolderNode'
3+
import type { CommitFile, GitService } from '@/git'
34
import type { StorageService } from '@/storage'
45

56
export class FileTreeProvider {
@@ -19,7 +20,51 @@ export class FileTreeProvider {
1920
this.commitHash = commitHash
2021
}
2122

22-
async getChildren(): Promise<FileNode[]> {
23+
private buildFileTree(files: CommitFile[]): Array<FileNode | FolderNode> {
24+
const topLevelNodes = new Map<string, FolderNode | FileNode>()
25+
26+
for (const file of files) {
27+
const parts = file.path.split('/')
28+
29+
if (parts.length === 1) {
30+
topLevelNodes.set(file.path, new FileNode(file.path, file.status))
31+
continue
32+
}
33+
34+
const topLevelName = parts[0]
35+
let currentNode: FolderNode
36+
37+
if (!topLevelNodes.has(topLevelName)) {
38+
currentNode = new FolderNode(topLevelName, topLevelName)
39+
topLevelNodes.set(topLevelName, currentNode)
40+
}
41+
else {
42+
currentNode = topLevelNodes.get(topLevelName)! as FolderNode
43+
}
44+
45+
for (let i = 1; i < parts.length - 1; i++) {
46+
const part = parts[i]
47+
const currentPath = parts.slice(0, i + 1).join('/')
48+
49+
let folderNode = currentNode.children.find(
50+
child => child instanceof FolderNode && child.name === part,
51+
) as FolderNode
52+
53+
if (!folderNode) {
54+
folderNode = new FolderNode(part, currentPath)
55+
currentNode.addChild(folderNode)
56+
}
57+
58+
currentNode = folderNode
59+
}
60+
61+
currentNode.addChild(new FileNode(file.path, file.status))
62+
}
63+
64+
return Array.from(topLevelNodes.values())
65+
}
66+
67+
async getChildren(): Promise<Array<FileNode | FolderNode>> {
2368
if (!this.commitHash)
2469
return []
2570

@@ -31,7 +76,7 @@ export class FileTreeProvider {
3176
}
3277

3378
if (commit.files && commit.files.length > 0) {
34-
return commit.files.map(file => new FileNode(file.path, file.status))
79+
return this.buildFileTree(commit.files)
3580
}
3681

3782
const showResult = await this.gitService.git.show([
@@ -54,10 +99,7 @@ export class FileTreeProvider {
5499

55100
this.storageService.updateCommitFiles(this.commitHash, fileChanges)
56101

57-
return fileChanges.map(({ status, path }) => new FileNode(
58-
path,
59-
this.parseGitStatus(status),
60-
))
102+
return this.buildFileTree(fileChanges)
61103
}
62104
catch (error) {
63105
console.error('Error getting commit files:', error)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as vscode from 'vscode'
2+
import type { FileNode } from './FileNode'
3+
4+
export class FolderNode extends vscode.TreeItem {
5+
children: (FolderNode | FileNode)[] = []
6+
7+
constructor(
8+
public readonly name: string,
9+
public readonly path: string,
10+
) {
11+
super(name, vscode.TreeItemCollapsibleState.Expanded)
12+
this.iconPath = new vscode.ThemeIcon('folder')
13+
this.contextValue = 'folder'
14+
}
15+
16+
addChild(child: FolderNode | FileNode) {
17+
this.children.push(child)
18+
}
19+
}

0 commit comments

Comments
 (0)