Skip to content

Commit 3be6a1d

Browse files
committed
Improve implementation of diff-file-tree
1 parent 145b583 commit 3be6a1d

File tree

7 files changed

+223
-14
lines changed

7 files changed

+223
-14
lines changed

services/gitdiff/gitdiff.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,12 +448,20 @@ func getCommitFileLineCount(commit *git.Commit, filePath string) int {
448448
return lineCount
449449
}
450450

451+
type FileTreeNode struct {
452+
IsFile bool
453+
Name string
454+
File *DiffFile
455+
Children []*FileTreeNode
456+
}
457+
451458
// Diff represents a difference between two git trees.
452459
type Diff struct {
453460
Start, End string
454461
NumFiles int
455462
TotalAddition, TotalDeletion int
456463
Files []*DiffFile
464+
FileTree []*FileTreeNode
457465
IsIncomplete bool
458466
NumViewedFiles int // user-specific
459467
}
@@ -1212,6 +1220,8 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
12121220
}
12131221
}
12141222

1223+
diff.FileTree = buildTree(diff.Files)
1224+
12151225
if opts.FileOnly {
12161226
return diff, nil
12171227
}
@@ -1384,3 +1394,65 @@ func GetWhitespaceFlag(whitespaceBehavior string) git.TrustedCmdArgs {
13841394
log.Warn("unknown whitespace behavior: %q, default to 'show-all'", whitespaceBehavior)
13851395
return nil
13861396
}
1397+
1398+
func buildTree(files []*DiffFile) []*FileTreeNode {
1399+
result := make(map[string]*FileTreeNode)
1400+
for _, file := range files {
1401+
splits := strings.Split(file.Name, "/")
1402+
currentNode := &FileTreeNode{Name: splits[0], IsFile: false}
1403+
if _, exists := result[splits[0]]; !exists {
1404+
result[splits[0]] = currentNode
1405+
} else {
1406+
currentNode = result[splits[0]]
1407+
}
1408+
1409+
parent := currentNode
1410+
for _, split := range splits[1:] {
1411+
found := false
1412+
for _, child := range parent.Children {
1413+
if child.Name == split {
1414+
parent = child
1415+
found = true
1416+
break
1417+
}
1418+
}
1419+
if !found {
1420+
newNode := &FileTreeNode{Name: split, IsFile: false}
1421+
parent.Children = append(parent.Children, newNode)
1422+
parent = newNode
1423+
}
1424+
}
1425+
1426+
lastNode := parent
1427+
lastNode.IsFile = true
1428+
lastNode.File = file
1429+
}
1430+
1431+
var roots []*FileTreeNode
1432+
for _, node := range result {
1433+
if len(node.Children) > 0 {
1434+
mergedNode := mergeSingleChildDirs(node)
1435+
roots = append(roots, mergedNode)
1436+
}
1437+
}
1438+
return roots
1439+
}
1440+
1441+
func mergeSingleChildDirs(node *FileTreeNode) *FileTreeNode {
1442+
if len(node.Children) == 1 && !node.Children[0].IsFile {
1443+
merged := &FileTreeNode{
1444+
Name: fmt.Sprintf("%s/%s", node.Name, node.Children[0].Name),
1445+
Children: node.Children[0].Children,
1446+
IsFile: node.Children[0].IsFile,
1447+
File: node.Children[0].File,
1448+
}
1449+
if merged.File != nil {
1450+
merged.IsFile = true
1451+
}
1452+
return merged
1453+
}
1454+
for _, child := range node.Children {
1455+
mergeSingleChildDirs(child)
1456+
}
1457+
return node
1458+
}

templates/repo/diff/box.tmpl

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,35 +61,29 @@
6161
</div>
6262
{{end}}
6363
<script id="diff-data-script" type="module">
64-
const diffDataFiles = [{{range $i, $file := .Diff.Files}}{Name:"{{$file.Name}}",NameHash:"{{$file.NameHash}}",Type:{{$file.Type}},IsBin:{{$file.IsBin}},Addition:{{$file.Addition}},Deletion:{{$file.Deletion}},IsViewed:{{$file.IsViewed}}},{{end}}];
6564
const diffData = {
66-
isIncomplete: {{.Diff.IsIncomplete}},
6765
tooManyFilesMessage: "{{ctx.Locale.Tr "repo.diff.too_many_files"}}",
6866
binaryFileMessage: "{{ctx.Locale.Tr "repo.diff.bin"}}",
69-
showMoreMessage: "{{ctx.Locale.Tr "repo.diff.show_more"}}",
7067
statisticsMessage: "{{ctx.Locale.Tr "repo.diff.stats_desc_file"}}",
71-
linkLoadMore: "?skip-to={{.Diff.End}}&file-only=true",
7268
};
7369

7470
// for first time loading, the diffFileInfo is a plain object
7571
// after the Vue component is mounted, the diffFileInfo is a reactive object
7672
// keep in mind that this script block would be executed many times when loading more files, by "loadMoreFiles"
7773
let diffFileInfo = window.config.pageData.diffFileInfo || {
78-
files:[],
7974
fileTreeIsVisible: false,
8075
fileListIsVisible: false,
8176
isLoadingNewData: false,
8277
selectedItem: '',
8378
};
8479
diffFileInfo = Object.assign(diffFileInfo, diffData);
85-
diffFileInfo.files.push(...diffDataFiles);
8680
window.config.pageData.diffFileInfo = diffFileInfo;
8781
</script>
8882
<div id="diff-file-list"></div>
8983
{{end}}
9084
<div id="diff-container">
9185
{{if $showFileTree}}
92-
<div id="diff-file-tree" class="tw-hidden not-mobile"></div>
86+
{{template "repo/diff/file_tree" dict "Files" .Diff.FileTree "IsIncomplete" .Diff.IsIncomplete "LoadMoreLink" "?skip-to={{.Diff.End}}&file-only=true"}}
9387
<script>
9488
if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');
9589
</script>
@@ -228,7 +222,7 @@
228222
<div class="diff-file-box diff-box file-content tw-mt-2" id="diff-incomplete">
229223
<h4 class="ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between">
230224
{{ctx.Locale.Tr "repo.diff.too_many_files"}}
231-
<a class="ui basic tiny button" id="diff-show-more-files" data-href="?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
225+
<a class="ui basic tiny button diff-show-more-files" data-href="?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
232226
</h4>
233227
</div>
234228
{{end}}

templates/repo/diff/file_tree.tmpl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<div id="diff-file-tree" class="file-tree tw-hidden not-mobile">
2+
<div class="file-tree-items">
3+
{{range .Files}}
4+
{{template "repo/diff/file_tree_item" .}}
5+
{{end}}
6+
</div>
7+
{{if .IsIncomplete}}
8+
<div class="tw-pt-1">
9+
<a class="ui basic tiny button diff-show-more-files" data-href="{{ .LoadMoreLink }}">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
10+
</div>
11+
{{end}}
12+
</div>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{{if .IsFile}}
2+
<a class="item-file {{if .File.IsViewed}} viewed {{end}}" title="{{ .Name }}" href="#diff-{{ .File.NameHash }}">
3+
<!-- file -->
4+
{{svg "octicon-file"}}
5+
<span class="gt-ellipsis tw-flex-1">{{ .Name }}</span>
6+
{{if eq .File.Type 1}}
7+
{{svg "octicon-diff-added" 16 "text green"}}
8+
{{else if eq .File.Type 2}}
9+
{{svg "octicon-diff-modified" 16 "text yellow"}}
10+
{{else if eq .File.Type 3}}
11+
{{svg "octicon-diff-removed" 16 "text red"}}
12+
{{else if eq .File.Type 4}}
13+
{{svg "octicon-diff-renamed" 16 "text teal"}}
14+
{{else if eq .File.Type 5}}
15+
{{svg "octicon-diff-renamed" 16 "text green"}}
16+
{{end}}
17+
</a>
18+
{{else}}
19+
<div class="item-directory" title="{{ .Name }}">
20+
<!-- directory -->
21+
{{svg "octicon-chevron-down"}}
22+
{{svg "octicon-file-directory-open-fill" 16 "text primary"}}
23+
<span class="gt-ellipsis">{{ .Name }}</span>
24+
</div>
25+
{{end}}
26+
{{if and .Children (gt (len .Children) 0)}}
27+
<div class="sub-items">
28+
{{range .Children}}
29+
{{template "repo/diff/file_tree_item" .}}
30+
{{end}}
31+
</div>
32+
{{end}}

web_src/css/repo.css

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2377,7 +2377,7 @@ tbody.commit-list {
23772377
gap: 8px;
23782378
}
23792379

2380-
#diff-file-tree {
2380+
.file-tree {
23812381
flex: 0 0 20%;
23822382
max-width: 380px;
23832383
line-height: inherit;
@@ -2389,6 +2389,60 @@ tbody.commit-list {
23892389
overflow-y: auto;
23902390
}
23912391

2392+
.file-tree .file-tree-items {
2393+
display: flex;
2394+
flex-direction: column;
2395+
gap: 1px;
2396+
margin-right: .5rem;
2397+
}
2398+
2399+
.file-tree .file-tree-items a, a:hover {
2400+
text-decoration: none;
2401+
color: var(--color-text);
2402+
}
2403+
2404+
.file-tree .file-tree-items .sub-items {
2405+
display: flex;
2406+
flex-direction: column;
2407+
gap: 1px;
2408+
margin-left: 13px;
2409+
border-left: 1px solid var(--color-secondary);
2410+
}
2411+
2412+
.file-tree .file-tree-items .sub-items .item-file {
2413+
padding-left: 18px;
2414+
}
2415+
2416+
.file-tree .file-tree-items .item-file.selected {
2417+
color: var(--color-text);
2418+
background: var(--color-active);
2419+
border-radius: 4px;
2420+
}
2421+
2422+
.file-tree .file-tree-items .item-file.viewed {
2423+
color: var(--color-text-light-3);
2424+
}
2425+
2426+
.file-tree .file-tree-items .item-directory {
2427+
user-select: none;
2428+
}
2429+
2430+
.file-tree .file-tree-items .item-file,
2431+
.file-tree .file-tree-items .item-directory {
2432+
display: flex;
2433+
align-items: center;
2434+
gap: 0.25em;
2435+
padding: 6px;
2436+
}
2437+
2438+
.file-tree .file-tree-items .item-file:hover,
2439+
.file-tree .file-tree-items .item-directory:hover {
2440+
color: var(--color-text);
2441+
background: var(--color-hover);
2442+
border-radius: 4px;
2443+
cursor: pointer;
2444+
}
2445+
23922446
.ui.message.unicode-escape-prompt {
23932447
margin-bottom: 0;
23942448
border-radius: 0;

web_src/js/features/repo-diff-filetree.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,58 @@
11
import {createApp} from 'vue';
2-
import DiffFileTree from '../components/DiffFileTree.vue';
2+
import {toggleElem} from '../utils/dom.ts';
3+
import {diffTreeStore} from '../modules/stores.ts';
4+
import {setFileFolding} from './file-fold.ts';
35
import DiffFileList from '../components/DiffFileList.vue';
46

7+
const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';
8+
9+
function hashChangeListener() {
10+
for (const el of document.querySelectorAll<HTMLAnchorElement>('.file-tree-items .item-file')) {
11+
el.classList.toggle('selected', el.hash === `${window.location.hash}`);
12+
}
13+
expandSelectedFile(window.location.hash);
14+
}
15+
16+
function expandSelectedFile(selectedItem) {
17+
// expand file if the selected file is folded
18+
if (selectedItem) {
19+
const box = document.querySelector(selectedItem);
20+
const folded = box?.getAttribute('data-folded') === 'true';
21+
if (folded) setFileFolding(box, box.querySelector('.fold-file'), false);
22+
}
23+
}
24+
25+
function updateState(visible) {
26+
const btn = document.querySelector('.diff-toggle-file-tree-button');
27+
const [toShow, toHide] = btn.querySelectorAll('.icon');
28+
const tree = document.querySelector('#diff-file-tree');
29+
const newTooltip = btn.getAttribute(visible ? 'data-hide-text' : 'data-show-text');
30+
btn.setAttribute('data-tooltip-content', newTooltip);
31+
toggleElem(tree, visible);
32+
toggleElem(toShow, !visible);
33+
toggleElem(toHide, visible);
34+
}
35+
536
export function initDiffFileTree() {
637
const el = document.querySelector('#diff-file-tree');
738
if (!el) return;
839

9-
const fileTreeView = createApp(DiffFileTree);
10-
fileTreeView.mount(el);
40+
const store = diffTreeStore();
41+
store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false';
42+
document.querySelector('.diff-toggle-file-tree-button').addEventListener('click', () => {
43+
store.fileTreeIsVisible = !store.fileTreeIsVisible;
44+
localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible);
45+
updateState(store.fileTreeIsVisible);
46+
});
47+
48+
hashChangeListener();
49+
window.addEventListener('hashchange', hashChangeListener);
50+
51+
for (const el of document.querySelectorAll<HTMLInputElement>('.file-tree-items .item-directory')) {
52+
el.addEventListener('click', () => {
53+
toggleElem(el.nextElementSibling);
54+
});
55+
}
1156
}
1257

1358
export function initDiffFileList() {

web_src/js/features/repo-diff.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ function onShowMoreFiles() {
166166
}
167167

168168
export async function loadMoreFiles(url) {
169-
const target = document.querySelector('a#diff-show-more-files');
169+
const target = document.querySelector('a.diff-show-more-files');
170170
if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) {
171171
return;
172172
}
@@ -195,7 +195,7 @@ export async function loadMoreFiles(url) {
195195
}
196196

197197
function initRepoDiffShowMore() {
198-
$(document).on('click', 'a#diff-show-more-files', (e) => {
198+
$(document).on('click', 'a.diff-show-more-files', (e) => {
199199
e.preventDefault();
200200

201201
const linkLoadMore = e.target.getAttribute('data-href');

0 commit comments

Comments
 (0)