Improve implementation of diff-file-tree

This commit is contained in:
Kerwin Bryant 2024-12-09 06:23:59 +00:00
parent 145b583631
commit 3be6a1dc15
7 changed files with 223 additions and 14 deletions

View File

@ -448,12 +448,20 @@ func getCommitFileLineCount(commit *git.Commit, filePath string) int {
return lineCount
}
type FileTreeNode struct {
IsFile bool
Name string
File *DiffFile
Children []*FileTreeNode
}
// Diff represents a difference between two git trees.
type Diff struct {
Start, End string
NumFiles int
TotalAddition, TotalDeletion int
Files []*DiffFile
FileTree []*FileTreeNode
IsIncomplete bool
NumViewedFiles int // user-specific
}
@ -1212,6 +1220,8 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
}
}
diff.FileTree = buildTree(diff.Files)
if opts.FileOnly {
return diff, nil
}
@ -1384,3 +1394,65 @@ func GetWhitespaceFlag(whitespaceBehavior string) git.TrustedCmdArgs {
log.Warn("unknown whitespace behavior: %q, default to 'show-all'", whitespaceBehavior)
return nil
}
func buildTree(files []*DiffFile) []*FileTreeNode {
result := make(map[string]*FileTreeNode)
for _, file := range files {
splits := strings.Split(file.Name, "/")
currentNode := &FileTreeNode{Name: splits[0], IsFile: false}
if _, exists := result[splits[0]]; !exists {
result[splits[0]] = currentNode
} else {
currentNode = result[splits[0]]
}
parent := currentNode
for _, split := range splits[1:] {
found := false
for _, child := range parent.Children {
if child.Name == split {
parent = child
found = true
break
}
}
if !found {
newNode := &FileTreeNode{Name: split, IsFile: false}
parent.Children = append(parent.Children, newNode)
parent = newNode
}
}
lastNode := parent
lastNode.IsFile = true
lastNode.File = file
}
var roots []*FileTreeNode
for _, node := range result {
if len(node.Children) > 0 {
mergedNode := mergeSingleChildDirs(node)
roots = append(roots, mergedNode)
}
}
return roots
}
func mergeSingleChildDirs(node *FileTreeNode) *FileTreeNode {
if len(node.Children) == 1 && !node.Children[0].IsFile {
merged := &FileTreeNode{
Name: fmt.Sprintf("%s/%s", node.Name, node.Children[0].Name),
Children: node.Children[0].Children,
IsFile: node.Children[0].IsFile,
File: node.Children[0].File,
}
if merged.File != nil {
merged.IsFile = true
}
return merged
}
for _, child := range node.Children {
mergeSingleChildDirs(child)
}
return node
}

View File

@ -61,35 +61,29 @@
</div>
{{end}}
<script id="diff-data-script" type="module">
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}}];
const diffData = {
isIncomplete: {{.Diff.IsIncomplete}},
tooManyFilesMessage: "{{ctx.Locale.Tr "repo.diff.too_many_files"}}",
binaryFileMessage: "{{ctx.Locale.Tr "repo.diff.bin"}}",
showMoreMessage: "{{ctx.Locale.Tr "repo.diff.show_more"}}",
statisticsMessage: "{{ctx.Locale.Tr "repo.diff.stats_desc_file"}}",
linkLoadMore: "?skip-to={{.Diff.End}}&file-only=true",
};
// for first time loading, the diffFileInfo is a plain object
// after the Vue component is mounted, the diffFileInfo is a reactive object
// keep in mind that this script block would be executed many times when loading more files, by "loadMoreFiles"
let diffFileInfo = window.config.pageData.diffFileInfo || {
files:[],
fileTreeIsVisible: false,
fileListIsVisible: false,
isLoadingNewData: false,
selectedItem: '',
};
diffFileInfo = Object.assign(diffFileInfo, diffData);
diffFileInfo.files.push(...diffDataFiles);
window.config.pageData.diffFileInfo = diffFileInfo;
</script>
<div id="diff-file-list"></div>
{{end}}
<div id="diff-container">
{{if $showFileTree}}
<div id="diff-file-tree" class="tw-hidden not-mobile"></div>
{{template "repo/diff/file_tree" dict "Files" .Diff.FileTree "IsIncomplete" .Diff.IsIncomplete "LoadMoreLink" "?skip-to={{.Diff.End}}&file-only=true"}}
<script>
if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');
</script>
@ -228,7 +222,7 @@
<div class="diff-file-box diff-box file-content tw-mt-2" id="diff-incomplete">
<h4 class="ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between">
{{ctx.Locale.Tr "repo.diff.too_many_files"}}
<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>
<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>
</h4>
</div>
{{end}}

View File

@ -0,0 +1,12 @@
<div id="diff-file-tree" class="file-tree tw-hidden not-mobile">
<div class="file-tree-items">
{{range .Files}}
{{template "repo/diff/file_tree_item" .}}
{{end}}
</div>
{{if .IsIncomplete}}
<div class="tw-pt-1">
<a class="ui basic tiny button diff-show-more-files" data-href="{{ .LoadMoreLink }}">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
</div>
{{end}}
</div>

View File

@ -0,0 +1,32 @@
{{if .IsFile}}
<a class="item-file {{if .File.IsViewed}} viewed {{end}}" title="{{ .Name }}" href="#diff-{{ .File.NameHash }}">
<!-- file -->
{{svg "octicon-file"}}
<span class="gt-ellipsis tw-flex-1">{{ .Name }}</span>
{{if eq .File.Type 1}}
{{svg "octicon-diff-added" 16 "text green"}}
{{else if eq .File.Type 2}}
{{svg "octicon-diff-modified" 16 "text yellow"}}
{{else if eq .File.Type 3}}
{{svg "octicon-diff-removed" 16 "text red"}}
{{else if eq .File.Type 4}}
{{svg "octicon-diff-renamed" 16 "text teal"}}
{{else if eq .File.Type 5}}
{{svg "octicon-diff-renamed" 16 "text green"}}
{{end}}
</a>
{{else}}
<div class="item-directory" title="{{ .Name }}">
<!-- directory -->
{{svg "octicon-chevron-down"}}
{{svg "octicon-file-directory-open-fill" 16 "text primary"}}
<span class="gt-ellipsis">{{ .Name }}</span>
</div>
{{end}}
{{if and .Children (gt (len .Children) 0)}}
<div class="sub-items">
{{range .Children}}
{{template "repo/diff/file_tree_item" .}}
{{end}}
</div>
{{end}}

View File

@ -2377,7 +2377,7 @@ tbody.commit-list {
gap: 8px;
}
#diff-file-tree {
.file-tree {
flex: 0 0 20%;
max-width: 380px;
line-height: inherit;
@ -2389,6 +2389,60 @@ tbody.commit-list {
overflow-y: auto;
}
.file-tree .file-tree-items {
display: flex;
flex-direction: column;
gap: 1px;
margin-right: .5rem;
}
.file-tree .file-tree-items a, a:hover {
text-decoration: none;
color: var(--color-text);
}
.file-tree .file-tree-items .sub-items {
display: flex;
flex-direction: column;
gap: 1px;
margin-left: 13px;
border-left: 1px solid var(--color-secondary);
}
.file-tree .file-tree-items .sub-items .item-file {
padding-left: 18px;
}
.file-tree .file-tree-items .item-file.selected {
color: var(--color-text);
background: var(--color-active);
border-radius: 4px;
}
.file-tree .file-tree-items .item-file.viewed {
color: var(--color-text-light-3);
}
.file-tree .file-tree-items .item-directory {
user-select: none;
}
.file-tree .file-tree-items .item-file,
.file-tree .file-tree-items .item-directory {
display: flex;
align-items: center;
gap: 0.25em;
padding: 6px;
}
.file-tree .file-tree-items .item-file:hover,
.file-tree .file-tree-items .item-directory:hover {
color: var(--color-text);
background: var(--color-hover);
border-radius: 4px;
cursor: pointer;
}
.ui.message.unicode-escape-prompt {
margin-bottom: 0;
border-radius: 0;

View File

@ -1,13 +1,58 @@
import {createApp} from 'vue';
import DiffFileTree from '../components/DiffFileTree.vue';
import {toggleElem} from '../utils/dom.ts';
import {diffTreeStore} from '../modules/stores.ts';
import {setFileFolding} from './file-fold.ts';
import DiffFileList from '../components/DiffFileList.vue';
const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';
function hashChangeListener() {
for (const el of document.querySelectorAll<HTMLAnchorElement>('.file-tree-items .item-file')) {
el.classList.toggle('selected', el.hash === `${window.location.hash}`);
}
expandSelectedFile(window.location.hash);
}
function expandSelectedFile(selectedItem) {
// expand file if the selected file is folded
if (selectedItem) {
const box = document.querySelector(selectedItem);
const folded = box?.getAttribute('data-folded') === 'true';
if (folded) setFileFolding(box, box.querySelector('.fold-file'), false);
}
}
function updateState(visible) {
const btn = document.querySelector('.diff-toggle-file-tree-button');
const [toShow, toHide] = btn.querySelectorAll('.icon');
const tree = document.querySelector('#diff-file-tree');
const newTooltip = btn.getAttribute(visible ? 'data-hide-text' : 'data-show-text');
btn.setAttribute('data-tooltip-content', newTooltip);
toggleElem(tree, visible);
toggleElem(toShow, !visible);
toggleElem(toHide, visible);
}
export function initDiffFileTree() {
const el = document.querySelector('#diff-file-tree');
if (!el) return;
const fileTreeView = createApp(DiffFileTree);
fileTreeView.mount(el);
const store = diffTreeStore();
store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false';
document.querySelector('.diff-toggle-file-tree-button').addEventListener('click', () => {
store.fileTreeIsVisible = !store.fileTreeIsVisible;
localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible);
updateState(store.fileTreeIsVisible);
});
hashChangeListener();
window.addEventListener('hashchange', hashChangeListener);
for (const el of document.querySelectorAll<HTMLInputElement>('.file-tree-items .item-directory')) {
el.addEventListener('click', () => {
toggleElem(el.nextElementSibling);
});
}
}
export function initDiffFileList() {

View File

@ -166,7 +166,7 @@ function onShowMoreFiles() {
}
export async function loadMoreFiles(url) {
const target = document.querySelector('a#diff-show-more-files');
const target = document.querySelector('a.diff-show-more-files');
if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) {
return;
}
@ -195,7 +195,7 @@ export async function loadMoreFiles(url) {
}
function initRepoDiffShowMore() {
$(document).on('click', 'a#diff-show-more-files', (e) => {
$(document).on('click', 'a.diff-show-more-files', (e) => {
e.preventDefault();
const linkLoadMore = e.target.getAttribute('data-href');