mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +00:00 
			
		
		
		
	Mark parent directory as viewed when all files are viewed (#33958)
Fix #25644 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		@@ -369,7 +369,7 @@ func Diff(ctx *context.Context) {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, nil)
 | 
							ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
 | 
						statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -639,7 +639,7 @@ func PrepareCompareDiff(
 | 
				
			|||||||
			return false
 | 
								return false
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, nil)
 | 
							ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)
 | 
						headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -759,12 +759,9 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
				
			|||||||
	// have to load only the diff and not get the viewed information
 | 
						// have to load only the diff and not get the viewed information
 | 
				
			||||||
	// as the viewed information is designed to be loaded only on latest PR
 | 
						// as the viewed information is designed to be loaded only on latest PR
 | 
				
			||||||
	// diff and if you're signed in.
 | 
						// diff and if you're signed in.
 | 
				
			||||||
	shouldGetUserSpecificDiff := false
 | 
						var reviewState *pull_model.ReviewState
 | 
				
			||||||
	if !ctx.IsSigned || willShowSpecifiedCommit || willShowSpecifiedCommitRange {
 | 
						if ctx.IsSigned && !willShowSpecifiedCommit && !willShowSpecifiedCommitRange {
 | 
				
			||||||
		// do nothing
 | 
							reviewState, err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions)
 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		shouldGetUserSpecificDiff = true
 | 
					 | 
				
			||||||
		err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions, files...)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			ctx.ServerError("SyncUserSpecificDiff", err)
 | 
								ctx.ServerError("SyncUserSpecificDiff", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@@ -823,18 +820,11 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
				
			|||||||
			ctx.ServerError("GetDiffTree", err)
 | 
								ctx.ServerError("GetDiffTree", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							var filesViewedState map[string]pull_model.ViewedState
 | 
				
			||||||
		filesViewedState := make(map[string]pull_model.ViewedState)
 | 
							if reviewState != nil {
 | 
				
			||||||
		if shouldGetUserSpecificDiff {
 | 
								filesViewedState = reviewState.UpdatedFiles
 | 
				
			||||||
			// This sort of sucks because we already fetch this when getting the diff
 | 
					 | 
				
			||||||
			review, err := pull_model.GetNewestReviewState(ctx, ctx.Doer.ID, issue.ID)
 | 
					 | 
				
			||||||
			if err == nil && review != nil && review.UpdatedFiles != nil {
 | 
					 | 
				
			||||||
				// If there wasn't an error and we have a review with updated files, use that
 | 
					 | 
				
			||||||
				filesViewedState = review.UpdatedFiles
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, filesViewedState)
 | 
				
			||||||
		ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, filesViewedState)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.Data["Diff"] = diff
 | 
						ctx.Data["Diff"] = diff
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ package repo
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pull_model "code.gitea.io/gitea/models/pull"
 | 
						pull_model "code.gitea.io/gitea/models/pull"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/base"
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
@@ -57,34 +58,85 @@ func isExcludedEntry(entry *git.TreeEntry) bool {
 | 
				
			|||||||
	return false
 | 
						return false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type FileDiffFile struct {
 | 
					// WebDiffFileItem is used by frontend, check the field names in frontend before changing
 | 
				
			||||||
	Name        string
 | 
					type WebDiffFileItem struct {
 | 
				
			||||||
 | 
						FullName    string
 | 
				
			||||||
 | 
						DisplayName string
 | 
				
			||||||
	NameHash    string
 | 
						NameHash    string
 | 
				
			||||||
	IsSubmodule bool
 | 
						DiffStatus  string
 | 
				
			||||||
 | 
						EntryMode   string
 | 
				
			||||||
	IsViewed    bool
 | 
						IsViewed    bool
 | 
				
			||||||
	Status      string
 | 
						Children    []*WebDiffFileItem
 | 
				
			||||||
 | 
						// TODO: add icon support in the future
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// transformDiffTreeForUI transforms a DiffTree into a slice of FileDiffFile for UI rendering
 | 
					// WebDiffFileTree is used by frontend, check the field names in frontend before changing
 | 
				
			||||||
 | 
					type WebDiffFileTree struct {
 | 
				
			||||||
 | 
						TreeRoot WebDiffFileItem
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// transformDiffTreeForWeb transforms a gitdiff.DiffTree into a WebDiffFileTree for Web UI rendering
 | 
				
			||||||
// it also takes a map of file names to their viewed state, which is used to mark files as viewed
 | 
					// it also takes a map of file names to their viewed state, which is used to mark files as viewed
 | 
				
			||||||
func transformDiffTreeForUI(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) []FileDiffFile {
 | 
					func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) {
 | 
				
			||||||
	files := make([]FileDiffFile, 0, len(diffTree.Files))
 | 
						dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot}
 | 
				
			||||||
 | 
						addItem := func(item *WebDiffFileItem) {
 | 
				
			||||||
	for _, file := range diffTree.Files {
 | 
							var parentPath string
 | 
				
			||||||
		nameHash := git.HashFilePathForWebUI(file.HeadPath)
 | 
							pos := strings.LastIndexByte(item.FullName, '/')
 | 
				
			||||||
		isSubmodule := file.HeadMode == git.EntryModeCommit
 | 
							if pos == -1 {
 | 
				
			||||||
		isViewed := filesViewedState[file.HeadPath] == pull_model.Viewed
 | 
								item.DisplayName = item.FullName
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
		files = append(files, FileDiffFile{
 | 
								parentPath = item.FullName[:pos]
 | 
				
			||||||
			Name:        file.HeadPath,
 | 
								item.DisplayName = item.FullName[pos+1:]
 | 
				
			||||||
			NameHash:    nameHash,
 | 
							}
 | 
				
			||||||
			IsSubmodule: isSubmodule,
 | 
							parentNode, parentExists := dirNodes[parentPath]
 | 
				
			||||||
			IsViewed:    isViewed,
 | 
							if !parentExists {
 | 
				
			||||||
			Status:      file.Status,
 | 
								parentNode = &dft.TreeRoot
 | 
				
			||||||
		})
 | 
								fields := strings.Split(parentPath, "/")
 | 
				
			||||||
 | 
								for idx, field := range fields {
 | 
				
			||||||
 | 
									nodePath := strings.Join(fields[:idx+1], "/")
 | 
				
			||||||
 | 
									node, ok := dirNodes[nodePath]
 | 
				
			||||||
 | 
									if !ok {
 | 
				
			||||||
 | 
										node = &WebDiffFileItem{EntryMode: "tree", DisplayName: field, FullName: nodePath}
 | 
				
			||||||
 | 
										dirNodes[nodePath] = node
 | 
				
			||||||
 | 
										parentNode.Children = append(parentNode.Children, node)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									parentNode = node
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							parentNode.Children = append(parentNode.Children, item)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return files
 | 
						for _, file := range diffTree.Files {
 | 
				
			||||||
 | 
							item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
 | 
				
			||||||
 | 
							item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
 | 
				
			||||||
 | 
							item.NameHash = git.HashFilePathForWebUI(item.FullName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							switch file.HeadMode {
 | 
				
			||||||
 | 
							case git.EntryModeTree:
 | 
				
			||||||
 | 
								item.EntryMode = "tree"
 | 
				
			||||||
 | 
							case git.EntryModeCommit:
 | 
				
			||||||
 | 
								item.EntryMode = "commit" // submodule
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								// default to empty, and will be treated as "blob" file because there is no "symlink" support yet
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							addItem(item)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var mergeSingleDir func(node *WebDiffFileItem)
 | 
				
			||||||
 | 
						mergeSingleDir = func(node *WebDiffFileItem) {
 | 
				
			||||||
 | 
							if len(node.Children) == 1 {
 | 
				
			||||||
 | 
								if child := node.Children[0]; child.EntryMode == "tree" {
 | 
				
			||||||
 | 
									node.FullName = child.FullName
 | 
				
			||||||
 | 
									node.DisplayName = node.DisplayName + "/" + child.DisplayName
 | 
				
			||||||
 | 
									node.Children = child.Children
 | 
				
			||||||
 | 
									mergeSingleDir(node)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, node := range dft.TreeRoot.Children {
 | 
				
			||||||
 | 
							mergeSingleDir(node)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return dft
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TreeViewNodes(ctx *context.Context) {
 | 
					func TreeViewNodes(ctx *context.Context) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										60
									
								
								routers/web/repo/treelist_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								routers/web/repo/treelist_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1337,10 +1337,13 @@ func GetDiffShortStat(gitRepo *git.Repository, beforeCommitID, afterCommitID str
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// SyncUserSpecificDiff inserts user-specific data such as which files the user has already viewed on the given diff
 | 
					// SyncUserSpecificDiff inserts user-specific data such as which files the user has already viewed on the given diff
 | 
				
			||||||
// Additionally, the database is updated asynchronously if files have changed since the last review
 | 
					// Additionally, the database is updated asynchronously if files have changed since the last review
 | 
				
			||||||
func SyncUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.PullRequest, gitRepo *git.Repository, diff *Diff, opts *DiffOptions, files ...string) error {
 | 
					func SyncUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.PullRequest, gitRepo *git.Repository, diff *Diff, opts *DiffOptions) (*pull_model.ReviewState, error) {
 | 
				
			||||||
	review, err := pull_model.GetNewestReviewState(ctx, userID, pull.ID)
 | 
						review, err := pull_model.GetNewestReviewState(ctx, userID, pull.ID)
 | 
				
			||||||
	if err != nil || review == nil || review.UpdatedFiles == nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if review == nil || len(review.UpdatedFiles) == 0 {
 | 
				
			||||||
 | 
							return review, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	latestCommit := opts.AfterCommitID
 | 
						latestCommit := opts.AfterCommitID
 | 
				
			||||||
@@ -1393,11 +1396,11 @@ outer:
 | 
				
			|||||||
		err := pull_model.UpdateReviewState(ctx, review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff)
 | 
							err := pull_model.UpdateReviewState(ctx, review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Warn("Could not update review for user %d, pull %d, commit %s and the changed files %v: %v", review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff, err)
 | 
								log.Warn("Could not update review for user %d, pull %d, commit %s and the changed files %v: %v", review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff, err)
 | 
				
			||||||
			return err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return review, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CommentAsDiff returns c.Patch as *Diff
 | 
					// CommentAsDiff returns c.Patch as *Diff
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +1,14 @@
 | 
				
			|||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import DiffFileTreeItem from './DiffFileTreeItem.vue';
 | 
					import DiffFileTreeItem from './DiffFileTreeItem.vue';
 | 
				
			||||||
import {toggleElem} from '../utils/dom.ts';
 | 
					import {toggleElem} from '../utils/dom.ts';
 | 
				
			||||||
import {diffTreeStore} from '../modules/stores.ts';
 | 
					import {diffTreeStore} from '../modules/diff-file.ts';
 | 
				
			||||||
import {setFileFolding} from '../features/file-fold.ts';
 | 
					import {setFileFolding} from '../features/file-fold.ts';
 | 
				
			||||||
import {computed, onMounted, onUnmounted} from 'vue';
 | 
					import {onMounted, onUnmounted} from 'vue';
 | 
				
			||||||
import {pathListToTree, mergeChildIfOnlyOneDir} from '../utils/filetree.ts';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';
 | 
					const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const store = diffTreeStore();
 | 
					const store = diffTreeStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fileTree = computed(() => {
 | 
					 | 
				
			||||||
  const result = pathListToTree(store.files);
 | 
					 | 
				
			||||||
  mergeChildIfOnlyOneDir(result); // mutation
 | 
					 | 
				
			||||||
  return result;
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
  // Default to true if unset
 | 
					  // Default to true if unset
 | 
				
			||||||
  store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false';
 | 
					  store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false';
 | 
				
			||||||
@@ -50,7 +43,7 @@ function toggleVisibility() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function updateVisibility(visible: boolean) {
 | 
					function updateVisibility(visible: boolean) {
 | 
				
			||||||
  store.fileTreeIsVisible = visible;
 | 
					  store.fileTreeIsVisible = visible;
 | 
				
			||||||
  localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible);
 | 
					  localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible.toString());
 | 
				
			||||||
  updateState(store.fileTreeIsVisible);
 | 
					  updateState(store.fileTreeIsVisible);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -69,7 +62,7 @@ function updateState(visible: boolean) {
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div v-if="store.fileTreeIsVisible" class="diff-file-tree-items">
 | 
					  <div v-if="store.fileTreeIsVisible" class="diff-file-tree-items">
 | 
				
			||||||
    <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
 | 
					    <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
 | 
				
			||||||
    <DiffFileTreeItem v-for="item in fileTree" :key="item.name" :item="item"/>
 | 
					    <DiffFileTreeItem v-for="item in store.diffFileTree.TreeRoot.Children" :key="item.FullName" :item="item"/>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,18 @@
 | 
				
			|||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import {SvgIcon, type SvgName} from '../svg.ts';
 | 
					import {SvgIcon, type SvgName} from '../svg.ts';
 | 
				
			||||||
import {diffTreeStore} from '../modules/stores.ts';
 | 
					 | 
				
			||||||
import {ref} from 'vue';
 | 
					import {ref} from 'vue';
 | 
				
			||||||
import type {Item, File, FileStatus} from '../utils/filetree.ts';
 | 
					import {type DiffStatus, type DiffTreeEntry, diffTreeStore} from '../modules/diff-file.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
  item: Item,
 | 
					  item: DiffTreeEntry,
 | 
				
			||||||
}>();
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const store = diffTreeStore();
 | 
					const store = diffTreeStore();
 | 
				
			||||||
const collapsed = ref(false);
 | 
					const collapsed = ref(props.item.IsViewed);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getIconForDiffStatus(pType: FileStatus) {
 | 
					function getIconForDiffStatus(pType: DiffStatus) {
 | 
				
			||||||
  const diffTypes: Record<FileStatus, { name: SvgName, classes: Array<string> }> = {
 | 
					  const diffTypes: Record<DiffStatus, { name: SvgName, classes: Array<string> }> = {
 | 
				
			||||||
 | 
					    '': {name: 'octicon-blocked', classes: ['text', 'red']}, // unknown case
 | 
				
			||||||
    'added': {name: 'octicon-diff-added', classes: ['text', 'green']},
 | 
					    'added': {name: 'octicon-diff-added', classes: ['text', 'green']},
 | 
				
			||||||
    'modified': {name: 'octicon-diff-modified', classes: ['text', 'yellow']},
 | 
					    'modified': {name: 'octicon-diff-modified', classes: ['text', 'yellow']},
 | 
				
			||||||
    'deleted': {name: 'octicon-diff-removed', classes: ['text', 'red']},
 | 
					    'deleted': {name: 'octicon-diff-removed', classes: ['text', 'red']},
 | 
				
			||||||
@@ -20,11 +20,11 @@ function getIconForDiffStatus(pType: FileStatus) {
 | 
				
			|||||||
    'copied': {name: 'octicon-diff-renamed', classes: ['text', 'green']},
 | 
					    'copied': {name: 'octicon-diff-renamed', classes: ['text', 'green']},
 | 
				
			||||||
    'typechange': {name: 'octicon-diff-modified', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok
 | 
					    'typechange': {name: 'octicon-diff-modified', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  return diffTypes[pType];
 | 
					  return diffTypes[pType] ?? diffTypes[''];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function fileIcon(file: File) {
 | 
					function entryIcon(entry: DiffTreeEntry) {
 | 
				
			||||||
  if (file.IsSubmodule) {
 | 
					  if (entry.EntryMode === 'commit') {
 | 
				
			||||||
    return 'octicon-file-submodule';
 | 
					    return 'octicon-file-submodule';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return 'octicon-file';
 | 
					  return 'octicon-file';
 | 
				
			||||||
@@ -32,37 +32,36 @@ function fileIcon(file: File) {
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <!--title instead of tooltip above as the tooltip needs too much work with the current methods, i.e. not being loaded or staying open for "too long"-->
 | 
					  <template v-if="item.EntryMode === 'tree'">
 | 
				
			||||||
  <a
 | 
					    <div class="item-directory" :class="{ 'viewed': item.IsViewed }" :title="item.DisplayName" @click.stop="collapsed = !collapsed">
 | 
				
			||||||
    v-if="item.isFile" class="item-file"
 | 
					 | 
				
			||||||
    :class="{ 'selected': store.selectedItem === '#diff-' + item.file.NameHash, 'viewed': item.file.IsViewed }"
 | 
					 | 
				
			||||||
    :title="item.name" :href="'#diff-' + item.file.NameHash"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <!-- file -->
 | 
					 | 
				
			||||||
    <SvgIcon :name="fileIcon(item.file)"/>
 | 
					 | 
				
			||||||
    <span class="gt-ellipsis tw-flex-1">{{ item.name }}</span>
 | 
					 | 
				
			||||||
    <SvgIcon
 | 
					 | 
				
			||||||
      :name="getIconForDiffStatus(item.file.Status).name"
 | 
					 | 
				
			||||||
      :class="getIconForDiffStatus(item.file.Status).classes"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  </a>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <template v-else-if="item.isFile === false">
 | 
					 | 
				
			||||||
    <div class="item-directory" :title="item.name" @click.stop="collapsed = !collapsed">
 | 
					 | 
				
			||||||
      <!-- directory -->
 | 
					      <!-- directory -->
 | 
				
			||||||
      <SvgIcon :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"/>
 | 
					      <SvgIcon :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"/>
 | 
				
			||||||
      <SvgIcon
 | 
					      <SvgIcon
 | 
				
			||||||
        class="text primary"
 | 
					        class="text primary"
 | 
				
			||||||
        :name="collapsed ? 'octicon-file-directory-fill' : 'octicon-file-directory-open-fill'"
 | 
					        :name="collapsed ? 'octicon-file-directory-fill' : 'octicon-file-directory-open-fill'"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <span class="gt-ellipsis">{{ item.name }}</span>
 | 
					      <span class="gt-ellipsis">{{ item.DisplayName }}</span>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div v-show="!collapsed" class="sub-items">
 | 
					    <div v-show="!collapsed" class="sub-items">
 | 
				
			||||||
      <DiffFileTreeItem v-for="childItem in item.children" :key="childItem.name" :item="childItem"/>
 | 
					      <DiffFileTreeItem v-for="childItem in item.Children" :key="childItem.DisplayName" :item="childItem"/>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </template>
 | 
					  </template>
 | 
				
			||||||
 | 
					  <a
 | 
				
			||||||
 | 
					    v-else
 | 
				
			||||||
 | 
					    class="item-file" :class="{ 'selected': store.selectedItem === '#diff-' + item.NameHash, 'viewed': item.IsViewed }"
 | 
				
			||||||
 | 
					    :title="item.DisplayName" :href="'#diff-' + item.NameHash"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <!-- file -->
 | 
				
			||||||
 | 
					    <SvgIcon :name="entryIcon(item)"/>
 | 
				
			||||||
 | 
					    <span class="gt-ellipsis tw-flex-1">{{ item.DisplayName }}</span>
 | 
				
			||||||
 | 
					    <SvgIcon
 | 
				
			||||||
 | 
					      :name="getIconForDiffStatus(item.DiffStatus).name"
 | 
				
			||||||
 | 
					      :class="getIconForDiffStatus(item.DiffStatus).classes"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </a>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
a,
 | 
					a,
 | 
				
			||||||
a:hover {
 | 
					a:hover {
 | 
				
			||||||
@@ -88,7 +87,8 @@ a:hover {
 | 
				
			|||||||
  border-radius: 4px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.item-file.viewed {
 | 
					.item-file.viewed,
 | 
				
			||||||
 | 
					.item-directory.viewed {
 | 
				
			||||||
  color: var(--color-text-light-3);
 | 
					  color: var(--color-text-light-3);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ import {svg} from '../svg.ts';
 | 
				
			|||||||
// The fold arrow is the icon displayed on the upper left of the file box, especially intended for components having the 'fold-file' class.
 | 
					// The fold arrow is the icon displayed on the upper left of the file box, especially intended for components having the 'fold-file' class.
 | 
				
			||||||
// The file content box is the box that should be hidden or shown, especially intended for components having the 'file-content' class.
 | 
					// The file content box is the box that should be hidden or shown, especially intended for components having the 'file-content' class.
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
export function setFileFolding(fileContentBox: HTMLElement, foldArrow: HTMLElement, newFold: boolean) {
 | 
					export function setFileFolding(fileContentBox: Element, foldArrow: HTMLElement, newFold: boolean) {
 | 
				
			||||||
  foldArrow.innerHTML = svg(`octicon-chevron-${newFold ? 'right' : 'down'}`, 18);
 | 
					  foldArrow.innerHTML = svg(`octicon-chevron-${newFold ? 'right' : 'down'}`, 18);
 | 
				
			||||||
  fileContentBox.setAttribute('data-folded', String(newFold));
 | 
					  fileContentBox.setAttribute('data-folded', String(newFold));
 | 
				
			||||||
  if (newFold && fileContentBox.getBoundingClientRect().top < 0) {
 | 
					  if (newFold && fileContentBox.getBoundingClientRect().top < 0) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import {diffTreeStore} from '../modules/stores.ts';
 | 
					import {diffTreeStore, diffTreeStoreSetViewed} from '../modules/diff-file.ts';
 | 
				
			||||||
import {setFileFolding} from './file-fold.ts';
 | 
					import {setFileFolding} from './file-fold.ts';
 | 
				
			||||||
import {POST} from '../modules/fetch.ts';
 | 
					import {POST} from '../modules/fetch.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -58,11 +58,8 @@ export function initViewedCheckboxListenerFor() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      const fileName = checkbox.getAttribute('name');
 | 
					      const fileName = checkbox.getAttribute('name');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // check if the file is in our difftreestore and if we find it -> change the IsViewed status
 | 
					      // check if the file is in our diffTreeStore and if we find it -> change the IsViewed status
 | 
				
			||||||
      const fileInPageData = diffTreeStore().files.find((x: Record<string, any>) => x.Name === fileName);
 | 
					      diffTreeStoreSetViewed(diffTreeStore(), fileName, this.checked);
 | 
				
			||||||
      if (fileInPageData) {
 | 
					 | 
				
			||||||
        fileInPageData.IsViewed = this.checked;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Unfortunately, actual forms cause too many problems, hence another approach is needed
 | 
					      // Unfortunately, actual forms cause too many problems, hence another approach is needed
 | 
				
			||||||
      const files: Record<string, boolean> = {};
 | 
					      const files: Record<string, boolean> = {};
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										47
									
								
								web_src/js/modules/diff-file.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								web_src/js/modules/diff-file.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										78
									
								
								web_src/js/modules/diff-file.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								web_src/js/modules/diff-file.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user