mirror of https://github.com/go-gitea/gitea.git
Mark parent directory as viewed when all files are viewed (#33958)
Fix #25644 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>pull/34207/head^2
parent
18a673bad1
commit
2b99a58f54
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
pull_model "code.gitea.io/gitea/models/pull"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/services/gitdiff"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransformDiffTreeForWeb(t *testing.T) {
|
||||||
|
ret := transformDiffTreeForWeb(&gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{
|
||||||
|
{
|
||||||
|
Status: "changed",
|
||||||
|
HeadPath: "dir-a/dir-a-x/file-deep",
|
||||||
|
HeadMode: git.EntryModeBlob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Status: "added",
|
||||||
|
HeadPath: "file1",
|
||||||
|
HeadMode: git.EntryModeBlob,
|
||||||
|
},
|
||||||
|
}}, map[string]pull_model.ViewedState{
|
||||||
|
"dir-a/dir-a-x/file-deep": pull_model.Viewed,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, WebDiffFileTree{
|
||||||
|
TreeRoot: WebDiffFileItem{
|
||||||
|
Children: []*WebDiffFileItem{
|
||||||
|
{
|
||||||
|
EntryMode: "tree",
|
||||||
|
DisplayName: "dir-a/dir-a-x",
|
||||||
|
FullName: "dir-a/dir-a-x",
|
||||||
|
Children: []*WebDiffFileItem{
|
||||||
|
{
|
||||||
|
EntryMode: "",
|
||||||
|
DisplayName: "file-deep",
|
||||||
|
FullName: "dir-a/dir-a-x/file-deep",
|
||||||
|
NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b",
|
||||||
|
DiffStatus: "changed",
|
||||||
|
IsViewed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EntryMode: "",
|
||||||
|
DisplayName: "file1",
|
||||||
|
FullName: "file1",
|
||||||
|
NameHash: "60b27f004e454aca81b0480209cce5081ec52390",
|
||||||
|
DiffStatus: "added",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, ret)
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import {diffTreeStoreSetViewed, reactiveDiffTreeStore} from './diff-file.ts';
|
||||||
|
|
||||||
|
test('diff-tree', () => {
|
||||||
|
const store = reactiveDiffTreeStore({
|
||||||
|
'TreeRoot': {
|
||||||
|
'FullName': '',
|
||||||
|
'DisplayName': '',
|
||||||
|
'EntryMode': '',
|
||||||
|
'IsViewed': false,
|
||||||
|
'NameHash': '....',
|
||||||
|
'DiffStatus': '',
|
||||||
|
'Children': [
|
||||||
|
{
|
||||||
|
'FullName': 'dir1',
|
||||||
|
'DisplayName': 'dir1',
|
||||||
|
'EntryMode': 'tree',
|
||||||
|
'IsViewed': false,
|
||||||
|
'NameHash': '....',
|
||||||
|
'DiffStatus': '',
|
||||||
|
'Children': [
|
||||||
|
{
|
||||||
|
'FullName': 'dir1/test.txt',
|
||||||
|
'DisplayName': 'test.txt',
|
||||||
|
'DiffStatus': 'added',
|
||||||
|
'NameHash': '....',
|
||||||
|
'EntryMode': '',
|
||||||
|
'IsViewed': false,
|
||||||
|
'Children': null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FullName': 'other.txt',
|
||||||
|
'DisplayName': 'other.txt',
|
||||||
|
'NameHash': '........',
|
||||||
|
'DiffStatus': 'added',
|
||||||
|
'EntryMode': '',
|
||||||
|
'IsViewed': false,
|
||||||
|
'Children': null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
diffTreeStoreSetViewed(store, 'dir1/test.txt', true);
|
||||||
|
expect(store.fullNameMap['dir1/test.txt'].IsViewed).toBe(true);
|
||||||
|
expect(store.fullNameMap['dir1'].IsViewed).toBe(true);
|
||||||
|
});
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import {reactive} from 'vue';
|
||||||
|
import type {Reactive} from 'vue';
|
||||||
|
|
||||||
|
const {pageData} = window.config;
|
||||||
|
|
||||||
|
export type DiffStatus = '' | 'added' | 'modified' | 'deleted' | 'renamed' | 'copied' | 'typechange';
|
||||||
|
|
||||||
|
export type DiffTreeEntry = {
|
||||||
|
FullName: string,
|
||||||
|
DisplayName: string,
|
||||||
|
NameHash: string,
|
||||||
|
DiffStatus: DiffStatus,
|
||||||
|
EntryMode: string,
|
||||||
|
IsViewed: boolean,
|
||||||
|
Children: DiffTreeEntry[],
|
||||||
|
|
||||||
|
ParentEntry?: DiffTreeEntry,
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiffFileTreeData = {
|
||||||
|
TreeRoot: DiffTreeEntry,
|
||||||
|
};
|
||||||
|
|
||||||
|
type DiffFileTree = {
|
||||||
|
diffFileTree: DiffFileTreeData;
|
||||||
|
fullNameMap?: Record<string, DiffTreeEntry>
|
||||||
|
fileTreeIsVisible: boolean;
|
||||||
|
selectedItem: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let diffTreeStoreReactive: Reactive<DiffFileTree>;
|
||||||
|
export function diffTreeStore() {
|
||||||
|
if (!diffTreeStoreReactive) {
|
||||||
|
diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree);
|
||||||
|
}
|
||||||
|
return diffTreeStoreReactive;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function diffTreeStoreSetViewed(store: Reactive<DiffFileTree>, fullName: string, viewed: boolean) {
|
||||||
|
const entry = store.fullNameMap[fullName];
|
||||||
|
if (!entry) return;
|
||||||
|
entry.IsViewed = viewed;
|
||||||
|
for (let parent = entry.ParentEntry; parent; parent = parent.ParentEntry) {
|
||||||
|
parent.IsViewed = isEntryViewed(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillFullNameMap(map: Record<string, DiffTreeEntry>, entry: DiffTreeEntry) {
|
||||||
|
map[entry.FullName] = entry;
|
||||||
|
if (!entry.Children) return;
|
||||||
|
entry.IsViewed = isEntryViewed(entry);
|
||||||
|
for (const child of entry.Children) {
|
||||||
|
child.ParentEntry = entry;
|
||||||
|
fillFullNameMap(map, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reactiveDiffTreeStore(data: DiffFileTreeData): Reactive<DiffFileTree> {
|
||||||
|
const store = reactive({
|
||||||
|
diffFileTree: data,
|
||||||
|
fileTreeIsVisible: false,
|
||||||
|
selectedItem: '',
|
||||||
|
fullNameMap: {},
|
||||||
|
});
|
||||||
|
fillFullNameMap(store.fullNameMap, data.TreeRoot);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEntryViewed(entry: DiffTreeEntry): boolean {
|
||||||
|
if (entry.Children) {
|
||||||
|
let count = 0;
|
||||||
|
for (const child of entry.Children) {
|
||||||
|
if (child.IsViewed) count++;
|
||||||
|
}
|
||||||
|
return count === entry.Children.length;
|
||||||
|
}
|
||||||
|
return entry.IsViewed;
|
||||||
|
}
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import {reactive} from 'vue';
|
|
||||||
import type {Reactive} from 'vue';
|
|
||||||
|
|
||||||
const {pageData} = window.config;
|
|
||||||
|
|
||||||
let diffTreeStoreReactive: Reactive<Record<string, any>>;
|
|
||||||
export function diffTreeStore() {
|
|
||||||
if (!diffTreeStoreReactive) {
|
|
||||||
diffTreeStoreReactive = reactive({
|
|
||||||
files: pageData.DiffFiles,
|
|
||||||
fileTreeIsVisible: false,
|
|
||||||
selectedItem: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return diffTreeStoreReactive;
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
import {mergeChildIfOnlyOneDir, pathListToTree, type File} from './filetree.ts';
|
|
||||||
|
|
||||||
const emptyList: File[] = [];
|
|
||||||
const singleFile = [{Name: 'file1'}] as File[];
|
|
||||||
const singleDir = [{Name: 'dir1/file1'}] as File[];
|
|
||||||
const nestedDir = [{Name: 'dir1/dir2/file1'}] as File[];
|
|
||||||
const multiplePathsDisjoint = [{Name: 'dir1/dir2/file1'}, {Name: 'dir3/file2'}] as File[];
|
|
||||||
const multiplePathsShared = [{Name: 'dir1/dir2/dir3/file1'}, {Name: 'dir1/file2'}] as File[];
|
|
||||||
|
|
||||||
test('pathListToTree', () => {
|
|
||||||
expect(pathListToTree(emptyList)).toEqual([]);
|
|
||||||
expect(pathListToTree(singleFile)).toEqual([
|
|
||||||
{isFile: true, name: 'file1', path: 'file1', file: {Name: 'file1'}},
|
|
||||||
]);
|
|
||||||
expect(pathListToTree(singleDir)).toEqual([
|
|
||||||
{isFile: false, name: 'dir1', path: 'dir1', children: [
|
|
||||||
{isFile: true, name: 'file1', path: 'dir1/file1', file: {Name: 'dir1/file1'}},
|
|
||||||
]},
|
|
||||||
]);
|
|
||||||
expect(pathListToTree(nestedDir)).toEqual([
|
|
||||||
{isFile: false, name: 'dir1', path: 'dir1', children: [
|
|
||||||
{isFile: false, name: 'dir2', path: 'dir1/dir2', children: [
|
|
||||||
{isFile: true, name: 'file1', path: 'dir1/dir2/file1', file: {Name: 'dir1/dir2/file1'}},
|
|
||||||
]},
|
|
||||||
]},
|
|
||||||
]);
|
|
||||||
expect(pathListToTree(multiplePathsDisjoint)).toEqual([
|
|
||||||
{isFile: false, name: 'dir1', path: 'dir1', children: [
|
|
||||||
{isFile: false, name: 'dir2', path: 'dir1/dir2', children: [
|
|
||||||
{isFile: true, name: 'file1', path: 'dir1/dir2/file1', file: {Name: 'dir1/dir2/file1'}},
|
|
||||||
]},
|
|
||||||
]},
|
|
||||||
{isFile: false, name: 'dir3', path: 'dir3', children: [
|
|
||||||
{isFile: true, name: 'file2', path: 'dir3/file2', file: {Name: 'dir3/file2'}},
|
|
||||||
]},
|
|
||||||
]);
|
|
||||||
expect(pathListToTree(multiplePathsShared)).toEqual([
|
|
||||||
{isFile: false, name: 'dir1', path: 'dir1', children: [
|
|
||||||
{isFile: false, name: 'dir2', path: 'dir1/dir2', children: [
|
|
||||||
{isFile: false, name: 'dir3', path: 'dir1/dir2/dir3', children: [
|
|
||||||
{isFile: true, name: 'file1', path: 'dir1/dir2/dir3/file1', file: {Name: 'dir1/dir2/dir3/file1'}},
|
|
||||||
]},
|
|
||||||
]},
|
|
||||||
{isFile: true, name: 'file2', path: 'dir1/file2', file: {Name: 'dir1/file2'}},
|
|
||||||
]},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const mergeChildWrapper = (testCase: File[]) => {
|
|
||||||
const tree = pathListToTree(testCase);
|
|
||||||
mergeChildIfOnlyOneDir(tree);
|
|
||||||
return tree;
|
|
||||||
};
|
|
||||||
|
|
||||||
test('mergeChildIfOnlyOneDir', () => {
|
|
||||||
expect(mergeChildWrapper(emptyList)).toEqual([]);
|
|
||||||
expect(mergeChildWrapper(singleFile)).toEqual([
|
|
||||||
{isFile: true, name: 'file1', path: 'file1', file: {Name: 'file1'}},
|
|
||||||
]);
|
|
||||||
expect(mergeChildWrapper(singleDir)).toEqual([
|
|
||||||
{isFile: false, name: 'dir1', path: 'dir1', children: [
|
|
||||||
{isFile: true, name: 'file1', path: 'dir1/file1', file: {Name: 'dir1/file1'}},
|
|
||||||
]},
|
|
||||||
]);
|
|
||||||
expect(mergeChildWrapper(nestedDir)).toEqual([
|
|
||||||
{isFile: false, name: 'dir1/dir2', path: 'dir1/dir2', children: [
|
|
||||||
{isFile: true, name: 'file1', path: 'dir1/dir2/file1', file: {Name: 'dir1/dir2/file1'}},
|
|
||||||
]},
|
|
||||||
]);
|
|
||||||
expect(mergeChildWrapper(multiplePathsDisjoint)).toEqual([
|
|
||||||
{isFile: false, name: 'dir1/dir2', path: 'dir1/dir2', children: [
|
|
||||||
{isFile: true, name: 'file1', path: 'dir1/dir2/file1', file: {Name: 'dir1/dir2/file1'}},
|
|
||||||
]},
|
|
||||||
{isFile: false, name: 'dir3', path: 'dir3', children: [
|
|
||||||
{isFile: true, name: 'file2', path: 'dir3/file2', file: {Name: 'dir3/file2'}},
|
|
||||||
]},
|
|
||||||
]);
|
|
||||||
expect(mergeChildWrapper(multiplePathsShared)).toEqual([
|
|
||||||
{isFile: false, name: 'dir1', path: 'dir1', children: [
|
|
||||||
{isFile: false, name: 'dir2/dir3', path: 'dir1/dir2/dir3', children: [
|
|
||||||
{isFile: true, name: 'file1', path: 'dir1/dir2/dir3/file1', file: {Name: 'dir1/dir2/dir3/file1'}},
|
|
||||||
]},
|
|
||||||
{isFile: true, name: 'file2', path: 'dir1/file2', file: {Name: 'dir1/file2'}},
|
|
||||||
]},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
import {dirname, basename} from '../utils.ts';
|
|
||||||
|
|
||||||
export type FileStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'copied' | 'typechange';
|
|
||||||
|
|
||||||
export type File = {
|
|
||||||
Name: string;
|
|
||||||
NameHash: string;
|
|
||||||
Status: FileStatus;
|
|
||||||
IsViewed: boolean;
|
|
||||||
IsSubmodule: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type DirItem = {
|
|
||||||
isFile: false;
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
|
|
||||||
children: Item[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileItem = {
|
|
||||||
isFile: true;
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
file: File;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Item = DirItem | FileItem;
|
|
||||||
|
|
||||||
export function pathListToTree(fileEntries: File[]): Item[] {
|
|
||||||
const pathToItem = new Map<string, DirItem>();
|
|
||||||
|
|
||||||
// init root node
|
|
||||||
const root: DirItem = {name: '', path: '', isFile: false, children: []};
|
|
||||||
pathToItem.set('', root);
|
|
||||||
|
|
||||||
for (const fileEntry of fileEntries) {
|
|
||||||
const [parentPath, fileName] = [dirname(fileEntry.Name), basename(fileEntry.Name)];
|
|
||||||
|
|
||||||
let parentItem = pathToItem.get(parentPath);
|
|
||||||
if (!parentItem) {
|
|
||||||
parentItem = constructParents(pathToItem, parentPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileItem: FileItem = {name: fileName, path: fileEntry.Name, isFile: true, file: fileEntry};
|
|
||||||
|
|
||||||
parentItem.children.push(fileItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
return root.children;
|
|
||||||
}
|
|
||||||
|
|
||||||
function constructParents(pathToItem: Map<string, DirItem>, dirPath: string): DirItem {
|
|
||||||
const [dirParentPath, dirName] = [dirname(dirPath), basename(dirPath)];
|
|
||||||
|
|
||||||
let parentItem = pathToItem.get(dirParentPath);
|
|
||||||
if (!parentItem) {
|
|
||||||
// if the parent node does not exist, create it
|
|
||||||
parentItem = constructParents(pathToItem, dirParentPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirItem: DirItem = {name: dirName, path: dirPath, isFile: false, children: []};
|
|
||||||
parentItem.children.push(dirItem);
|
|
||||||
pathToItem.set(dirPath, dirItem);
|
|
||||||
|
|
||||||
return dirItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergeChildIfOnlyOneDir(nodes: Item[]): void {
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (node.isFile) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const dir = node as DirItem;
|
|
||||||
|
|
||||||
mergeChildIfOnlyOneDir(dir.children);
|
|
||||||
|
|
||||||
if (dir.children.length === 1 && dir.children[0].isFile === false) {
|
|
||||||
const child = dir.children[0];
|
|
||||||
dir.name = `${dir.name}/${child.name}`;
|
|
||||||
dir.path = child.path;
|
|
||||||
dir.children = child.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue