Skip to content

Mark parent directory as viewed when all files are viewed #33958

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

Merged
merged 18 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion routers/web/repo/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,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)
Expand Down
2 changes: 1 addition & 1 deletion routers/web/repo/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 7 additions & 17 deletions routers/web/repo/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -759,12 +759,9 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
// 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
// diff and if you're signed in.
shouldGetUserSpecificDiff := false
if !ctx.IsSigned || willShowSpecifiedCommit || willShowSpecifiedCommitRange {
// do nothing
} else {
shouldGetUserSpecificDiff = true
err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions, files...)
var reviewState *pull_model.ReviewState
if ctx.IsSigned && !willShowSpecifiedCommit && !willShowSpecifiedCommitRange {
reviewState, err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions)
if err != nil {
ctx.ServerError("SyncUserSpecificDiff", err)
return
Expand Down Expand Up @@ -823,18 +820,11 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
ctx.ServerError("GetDiffTree", err)
return
}

filesViewedState := make(map[string]pull_model.ViewedState)
if shouldGetUserSpecificDiff {
// 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
}
var filesViewedState map[string]pull_model.ViewedState
if reviewState != nil {
filesViewedState = reviewState.UpdatedFiles
}

ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, filesViewedState)
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, filesViewedState)
}

ctx.Data["Diff"] = diff
Expand Down
90 changes: 71 additions & 19 deletions routers/web/repo/treelist.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package repo

import (
"net/http"
"strings"

pull_model "code.gitea.io/gitea/models/pull"
"code.gitea.io/gitea/modules/base"
Expand Down Expand Up @@ -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
// 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
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}
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) {
Expand Down
60 changes: 60 additions & 0 deletions routers/web/repo/treelist_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
13 changes: 8 additions & 5 deletions services/gitdiff/gitdiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// 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)
if err != nil || review == nil || review.UpdatedFiles == nil {
return err
if err != nil {
return nil, err
}
if review == nil || len(review.UpdatedFiles) == 0 {
return review, nil
}

latestCommit := opts.AfterCommitID
Expand Down Expand Up @@ -1393,11 +1396,11 @@ outer:
err := pull_model.UpdateReviewState(ctx, review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff)
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)
return err
return nil, err
}
}

return nil
return review, err
}

// CommentAsDiff returns c.Patch as *Diff
Expand Down
15 changes: 4 additions & 11 deletions web_src/js/components/DiffFileTree.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
<script lang="ts" setup>
import DiffFileTreeItem from './DiffFileTreeItem.vue';
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 {computed, onMounted, onUnmounted} from 'vue';
import {pathListToTree, mergeChildIfOnlyOneDir} from '../utils/filetree.ts';
import {onMounted, onUnmounted} from 'vue';

const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';

const store = diffTreeStore();

const fileTree = computed(() => {
const result = pathListToTree(store.files);
mergeChildIfOnlyOneDir(result); // mutation
return result;
});

onMounted(() => {
// Default to true if unset
store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false';
Expand Down Expand Up @@ -50,7 +43,7 @@ function toggleVisibility() {

function updateVisibility(visible: boolean) {
store.fileTreeIsVisible = visible;
localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible);
localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible.toString());
updateState(store.fileTreeIsVisible);
}

Expand All @@ -69,7 +62,7 @@ function updateState(visible: boolean) {
<template>
<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 -->
<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>
</template>

Expand Down
Loading