Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve implementation of diff-file-tree #32768

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
85 changes: 85 additions & 0 deletions services/gitdiff/gitdiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -1384,3 +1394,78 @@ func GetWhitespaceFlag(whitespaceBehavior string) git.TrustedCmdArgs {
log.Warn("unknown whitespace behavior: %q, default to 'show-all'", whitespaceBehavior)
return nil
}

func buildTree(files []*DiffFile) []*FileTreeNode {
kerwin612 marked this conversation as resolved.
Show resolved Hide resolved
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)
sortChildren(mergedNode)
roots = append(roots, mergedNode)
} else {
roots = append(roots, node)
}
}
sortChildren(&FileTreeNode{Children: roots})
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,
}
return mergeSingleChildDirs(merged)
}
for i, child := range node.Children {
node.Children[i] = mergeSingleChildDirs(child)
}
return node
}

func sortChildren(node *FileTreeNode) {
sort.Slice(node.Children, func(i, j int) bool {
if node.Children[i].IsFile == node.Children[j].IsFile {
return node.Children[i].Name < node.Children[j].Name
}
return !node.Children[i].IsFile
kerwin612 marked this conversation as resolved.
Show resolved Hide resolved
})
for _, child := range node.Children {
sortChildren(child)
}
}
10 changes: 2 additions & 8 deletions templates/repo/diff/box.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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" (printf "?skip-to=%s&file-only=true" .Diff.End)}}
<script>
if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');
</script>
Expand Down Expand Up @@ -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}}
Expand Down
12 changes: 12 additions & 0 deletions templates/repo/diff/file_tree.tmpl
Original file line number Diff line number Diff line change
@@ -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>
34 changes: 34 additions & 0 deletions templates/repo/diff/file_tree_item.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{{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-right" 16 "tw-hidden"}}
{{svg "octicon-chevron-down" 16}}
{{svg "octicon-file-directory-fill" 16 "text primary tw-hidden"}}
{{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}}
56 changes: 55 additions & 1 deletion web_src/css/repo.css
Original file line number Diff line number Diff line change
Expand Up @@ -2377,7 +2377,7 @@ tbody.commit-list {
gap: 8px;
}

#diff-file-tree {
.file-tree {
flex: 0 0 20%;
max-width: 380px;
line-height: inherit;
Expand All @@ -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;
Expand Down
52 changes: 49 additions & 3 deletions web_src/js/features/repo-diff-filetree.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,59 @@
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);
toggleElem(el.querySelectorAll('.svg.octicon-chevron-right, .svg.octicon-chevron-down, .svg.octicon-file-directory-fill, .svg.octicon-file-directory-open-fill'));
});
}
}

export function initDiffFileList() {
Expand Down
4 changes: 2 additions & 2 deletions web_src/js/features/repo-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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');
Expand Down
Loading