diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 3fd1eacb581eb..a61f8f8b31556 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -370,7 +370,7 @@ func Diff(ctx *context.Context) { 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) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 2c36477e6a85f..13fbac981c3e8 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -639,7 +639,7 @@ func PrepareCompareDiff( return false } - ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, nil) + ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil) } headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index c72664f8e9035..bd5f34b501678 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -829,12 +829,12 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi // 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 + // If there wasn't an error, and we have a review with updated files, use that filesViewedState = review.UpdatedFiles } } - - ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, filesViewedState) + // FIXME: filesViewedState is always nil? + ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, filesViewedState) } ctx.Data["Diff"] = diff diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go index 9c5ec8f206731..01e5a4e579e19 100644 --- a/routers/web/repo/treelist.go +++ b/routers/web/repo/treelist.go @@ -5,6 +5,7 @@ package repo import ( "net/http" + "strings" pull_model "code.gitea.io/gitea/models/pull" "code.gitea.io/gitea/modules/base" @@ -57,34 +58,85 @@ func isExcludedEntry(entry *git.TreeEntry) bool { return false } -type FileDiffFile struct { - Name string +// WebDiffFileItem is used by frontend, check the field names in frontend before changing +type WebDiffFileItem struct { + FullName string + DisplayName string NameHash string - IsSubmodule bool + DiffStatus string + EntryMode string IsViewed bool - Status string + Children []*WebDiffFileItem } -// 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 -func transformDiffTreeForUI(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) []FileDiffFile { - files := make([]FileDiffFile, 0, len(diffTree.Files)) +func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) { + dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot} + addItem := func(item *WebDiffFileItem) { + var parentPath string + pos := strings.LastIndexByte(item.FullName, '/') + if pos == -1 { + item.DisplayName = item.FullName + } else { + parentPath = item.FullName[:pos] + item.DisplayName = item.FullName[pos+1:] + } + parentNode, parentExists := dirNodes[parentPath] + if !parentExists { + 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) + } for _, file := range diffTree.Files { - nameHash := git.HashFilePathForWebUI(file.HeadPath) - isSubmodule := file.HeadMode == git.EntryModeCommit - isViewed := filesViewedState[file.HeadPath] == pull_model.Viewed - - files = append(files, FileDiffFile{ - Name: file.HeadPath, - NameHash: nameHash, - IsSubmodule: isSubmodule, - IsViewed: isViewed, - Status: file.Status, - }) + item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status} + // FIXME: filesViewedState is always nil? + 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) } - return files + 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) { diff --git a/routers/web/repo/treelist_test.go b/routers/web/repo/treelist_test.go new file mode 100644 index 0000000000000..2dff64a028fe7 --- /dev/null +++ b/routers/web/repo/treelist_test.go @@ -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) +} diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue index 381a1c3ca420b..5426a672cbe1e 100644 --- a/web_src/js/components/DiffFileTree.vue +++ b/web_src/js/components/DiffFileTree.vue @@ -1,21 +1,14 @@ - - - - - {{ item.name }} - - - - - + + - {{ item.name }} + {{ item.DisplayName }} - + + + + + {{ item.DisplayName }} + + +