diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index bb1722039e19d..dacb3fff256c3 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -448,12 +448,20 @@ func getCommitFileLineCount(commit *git.Commit, filePath string) int { return lineCount } +type FileTreeNode struct { + IsFile bool + Name string + File *DiffFile + Children []*FileTreeNode +} + // Diff represents a difference between two git trees. type Diff struct { Start, End string NumFiles int TotalAddition, TotalDeletion int Files []*DiffFile + FileTree []*FileTreeNode IsIncomplete bool 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 { return diff, nil } @@ -1384,3 +1394,78 @@ func GetWhitespaceFlag(whitespaceBehavior string) git.TrustedCmdArgs { log.Warn("unknown whitespace behavior: %q, default to 'show-all'", whitespaceBehavior) 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) + sortChildren(mergedNode) + roots = append(roots, mergedNode) + } else { + roots = append(roots, node) + } + } + sortChildren(&FileTreeNode{Children: roots}) + 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, + } + return mergeSingleChildDirs(merged) + } + for i, child := range node.Children { + node.Children[i] = mergeSingleChildDirs(child) + } + return node +} + +func sortChildren(node *FileTreeNode) { + sort.Slice(node.Children, func(i, j int) bool { + if node.Children[i].IsFile == node.Children[j].IsFile { + return node.Children[i].Name < node.Children[j].Name + } + return !node.Children[i].IsFile + }) + for _, child := range node.Children { + sortChildren(child) + } +} diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index adcac355a7bb4..d9af23f6a9e02 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -668,3 +668,191 @@ func TestNoCrashes(t *testing.T) { ParsePatch(db.DefaultContext, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(testcase.gitdiff), "") } } + +func TestBuildTree(t *testing.T) { + tests := []struct { + name string + input []*DiffFile + expected []*FileTreeNode + }{ + { + name: "Test case 1", + input: []*DiffFile{ + {Name: "test1", NameHash: "b444ac06613fc8d63795be9ad0beaf55011936ac"}, + {Name: "2/2", NameHash: "23b3d60a69a4b1bacb1a3b9ced0a8ac609efbf61"}, + }, + expected: []*FileTreeNode{ + { + Name: "2", + IsFile: false, + Children: []*FileTreeNode{ + { + Name: "2", + IsFile: true, + File: &DiffFile{ + Name: "2/2", + NameHash: "23b3d60a69a4b1bacb1a3b9ced0a8ac609efbf61", + }, + }, + }, + }, + { + Name: "test1", + IsFile: true, + File: &DiffFile{ + Name: "test1", + NameHash: "b444ac06613fc8d63795be9ad0beaf55011936ac", + }, + }, + }, + }, + { + name: "Test case 2", + input: []*DiffFile{ + {Name: "a/b/d", NameHash: "hash2"}, + {Name: "a/e", NameHash: "hash3"}, + {Name: "a/b/c", NameHash: "hash1"}, + {Name: "f", NameHash: "hash4"}, + }, + expected: []*FileTreeNode{ + { + Name: "a", + IsFile: false, + Children: []*FileTreeNode{ + { + Name: "b", + IsFile: false, + Children: []*FileTreeNode{ + { + Name: "c", + IsFile: true, + File: &DiffFile{ + Name: "a/b/c", + NameHash: "hash1", + }, + }, + { + Name: "d", + IsFile: true, + File: &DiffFile{ + Name: "a/b/d", + NameHash: "hash2", + }, + }, + }, + }, + { + Name: "e", + IsFile: true, + File: &DiffFile{ + Name: "a/e", + NameHash: "hash3", + }, + }, + }, + }, + { + Name: "f", + IsFile: true, + File: &DiffFile{ + Name: "f", + NameHash: "hash4", + }, + }, + }, + }, + { + name: "Test case 3", + input: []*DiffFile{ + {Name: "dir2/file4", NameHash: "hash4"}, + {Name: "dir1/file1", NameHash: "hash1"}, + {Name: "dir2/file3", NameHash: "hash3"}, + {Name: "dir1/file2", NameHash: "hash2"}, + {Name: "file5", NameHash: "hash5"}, + }, + expected: []*FileTreeNode{ + { + Name: "dir1", + IsFile: false, + Children: []*FileTreeNode{ + { + Name: "file1", + IsFile: true, + File: &DiffFile{ + Name: "dir1/file1", + NameHash: "hash1", + }, + }, + { + Name: "file2", + IsFile: true, + File: &DiffFile{ + Name: "dir1/file2", + NameHash: "hash2", + }, + }, + }, + }, + { + Name: "dir2", + IsFile: false, + Children: []*FileTreeNode{ + { + Name: "file3", + IsFile: true, + File: &DiffFile{ + Name: "dir2/file3", + NameHash: "hash3", + }, + }, + { + Name: "file4", + IsFile: true, + File: &DiffFile{ + Name: "dir2/file4", + NameHash: "hash4", + }, + }, + }, + }, + { + Name: "file5", + IsFile: true, + File: &DiffFile{ + Name: "file5", + NameHash: "hash5", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := buildTree(tt.input) + if !compareFileTreeNodes(result, tt.expected) { + t.Errorf("Expected %v, but got %v", tt.expected, result) + } + }) + } +} + +func compareFileTreeNodes(a, b []*FileTreeNode) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i].Name != b[i].Name || a[i].IsFile != b[i].IsFile { + return false + } + if a[i].IsFile && b[i].IsFile { + if a[i].File.Name != b[i].File.Name || a[i].File.NameHash != b[i].File.NameHash { + return false + } + } + if !compareFileTreeNodes(a[i].Children, b[i].Children) { + return false + } + } + return true +} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 0f1458bfbfd22..0be1a09ff2b5d 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -61,35 +61,29 @@ {{end}}
{{end}}