mirror of
https://github.com/go-gitea/gitea
synced 2025-02-10 00:27:06 +01:00
Improve implementation of diff-file-tree
This commit is contained in:
parent
145b583631
commit
3be6a1dc15
@ -448,12 +448,20 @@ func getCommitFileLineCount(commit *git.Commit, filePath string) int {
|
|||||||
return lineCount
|
return lineCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileTreeNode struct {
|
||||||
|
IsFile bool
|
||||||
|
Name string
|
||||||
|
File *DiffFile
|
||||||
|
Children []*FileTreeNode
|
||||||
|
}
|
||||||
|
|
||||||
// Diff represents a difference between two git trees.
|
// Diff represents a difference between two git trees.
|
||||||
type Diff struct {
|
type Diff struct {
|
||||||
Start, End string
|
Start, End string
|
||||||
NumFiles int
|
NumFiles int
|
||||||
TotalAddition, TotalDeletion int
|
TotalAddition, TotalDeletion int
|
||||||
Files []*DiffFile
|
Files []*DiffFile
|
||||||
|
FileTree []*FileTreeNode
|
||||||
IsIncomplete bool
|
IsIncomplete bool
|
||||||
NumViewedFiles int // user-specific
|
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 {
|
if opts.FileOnly {
|
||||||
return diff, nil
|
return diff, nil
|
||||||
}
|
}
|
||||||
@ -1384,3 +1394,65 @@ func GetWhitespaceFlag(whitespaceBehavior string) git.TrustedCmdArgs {
|
|||||||
log.Warn("unknown whitespace behavior: %q, default to 'show-all'", whitespaceBehavior)
|
log.Warn("unknown whitespace behavior: %q, default to 'show-all'", whitespaceBehavior)
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@ -61,35 +61,29 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<script id="diff-data-script" type="module">
|
<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 = {
|
const diffData = {
|
||||||
isIncomplete: {{.Diff.IsIncomplete}},
|
|
||||||
tooManyFilesMessage: "{{ctx.Locale.Tr "repo.diff.too_many_files"}}",
|
tooManyFilesMessage: "{{ctx.Locale.Tr "repo.diff.too_many_files"}}",
|
||||||
binaryFileMessage: "{{ctx.Locale.Tr "repo.diff.bin"}}",
|
binaryFileMessage: "{{ctx.Locale.Tr "repo.diff.bin"}}",
|
||||||
showMoreMessage: "{{ctx.Locale.Tr "repo.diff.show_more"}}",
|
|
||||||
statisticsMessage: "{{ctx.Locale.Tr "repo.diff.stats_desc_file"}}",
|
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
|
// for first time loading, the diffFileInfo is a plain object
|
||||||
// after the Vue component is mounted, the diffFileInfo is a reactive 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"
|
// keep in mind that this script block would be executed many times when loading more files, by "loadMoreFiles"
|
||||||
let diffFileInfo = window.config.pageData.diffFileInfo || {
|
let diffFileInfo = window.config.pageData.diffFileInfo || {
|
||||||
files:[],
|
|
||||||
fileTreeIsVisible: false,
|
fileTreeIsVisible: false,
|
||||||
fileListIsVisible: false,
|
fileListIsVisible: false,
|
||||||
isLoadingNewData: false,
|
isLoadingNewData: false,
|
||||||
selectedItem: '',
|
selectedItem: '',
|
||||||
};
|
};
|
||||||
diffFileInfo = Object.assign(diffFileInfo, diffData);
|
diffFileInfo = Object.assign(diffFileInfo, diffData);
|
||||||
diffFileInfo.files.push(...diffDataFiles);
|
|
||||||
window.config.pageData.diffFileInfo = diffFileInfo;
|
window.config.pageData.diffFileInfo = diffFileInfo;
|
||||||
</script>
|
</script>
|
||||||
<div id="diff-file-list"></div>
|
<div id="diff-file-list"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div id="diff-container">
|
<div id="diff-container">
|
||||||
{{if $showFileTree}}
|
{{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>
|
<script>
|
||||||
if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');
|
if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');
|
||||||
</script>
|
</script>
|
||||||
@ -228,7 +222,7 @@
|
|||||||
<div class="diff-file-box diff-box file-content tw-mt-2" id="diff-incomplete">
|
<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">
|
<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"}}
|
{{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>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
12
templates/repo/diff/file_tree.tmpl
Normal file
12
templates/repo/diff/file_tree.tmpl
Normal 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>
|
32
templates/repo/diff/file_tree_item.tmpl
Normal file
32
templates/repo/diff/file_tree_item.tmpl
Normal 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}}
|
@ -2377,7 +2377,7 @@ tbody.commit-list {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#diff-file-tree {
|
.file-tree {
|
||||||
flex: 0 0 20%;
|
flex: 0 0 20%;
|
||||||
max-width: 380px;
|
max-width: 380px;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
@ -2389,6 +2389,60 @@ tbody.commit-list {
|
|||||||
overflow-y: auto;
|
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 {
|
.ui.message.unicode-escape-prompt {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
@ -1,13 +1,58 @@
|
|||||||
import {createApp} from 'vue';
|
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';
|
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() {
|
export function initDiffFileTree() {
|
||||||
const el = document.querySelector('#diff-file-tree');
|
const el = document.querySelector('#diff-file-tree');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
const fileTreeView = createApp(DiffFileTree);
|
const store = diffTreeStore();
|
||||||
fileTreeView.mount(el);
|
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() {
|
export function initDiffFileList() {
|
||||||
|
@ -166,7 +166,7 @@ function onShowMoreFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadMoreFiles(url) {
|
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) {
|
if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -195,7 +195,7 @@ export async function loadMoreFiles(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initRepoDiffShowMore() {
|
function initRepoDiffShowMore() {
|
||||||
$(document).on('click', 'a#diff-show-more-files', (e) => {
|
$(document).on('click', 'a.diff-show-more-files', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const linkLoadMore = e.target.getAttribute('data-href');
|
const linkLoadMore = e.target.getAttribute('data-href');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user