2
0

feat(repo): add hidden folders feature for code browser

Allow repository admins to hide specific folders from the code browser for non-admin users. Hidden folders are shown dimmed to admins but completely hidden from regular users. Includes database migration, settings UI, tree filtering logic, and frontend support for toggling visibility.
This commit is contained in:
2026-01-26 22:40:03 -05:00
parent b0b1b12fd5
commit 72d282cfaa
18 changed files with 390 additions and 10 deletions

View File

@@ -414,6 +414,7 @@ func prepareMigrationTasks() []*migration {
newMigration(337, "Add is_private to package for package visibility", v1_26.AddIsPrivateToPackage),
newMigration(338, "Add deleted_unix to action_runner table", v1_26.AddDeletedUnixToActionRunner),
newMigration(339, "Add is_limited to repository for limited visibility", v1_26.AddIsLimitedToRepository),
newMigration(340, "Create repo_hidden_folder table for hidden folders", v1_26.CreateRepoHiddenFolderTable),
}
return preparedMigrations
}

View File

@@ -0,0 +1,23 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"code.gitcaddy.com/server/v3/modules/timeutil"
"xorm.io/xorm"
)
// CreateRepoHiddenFolderTable creates the repo_hidden_folder table.
// Hidden folders are not shown in the code browser for non-admin users.
func CreateRepoHiddenFolderTable(x *xorm.Engine) error {
type RepoHiddenFolder struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
FolderPath string `xorm:"UNIQUE(s) NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}
return x.Sync(new(RepoHiddenFolder))
}

View File

@@ -0,0 +1,83 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"strings"
"code.gitcaddy.com/server/v3/models/db"
"code.gitcaddy.com/server/v3/modules/timeutil"
)
// RepoHiddenFolder represents a folder that is hidden from the code browser
// for non-admin users.
type RepoHiddenFolder struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
FolderPath string `xorm:"UNIQUE(s) NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}
func init() {
db.RegisterModel(new(RepoHiddenFolder))
}
// GetHiddenFolders returns all hidden folders for a repository.
func GetHiddenFolders(ctx context.Context, repoID int64) ([]*RepoHiddenFolder, error) {
folders := make([]*RepoHiddenFolder, 0)
return folders, db.GetEngine(ctx).Where("repo_id = ?", repoID).OrderBy("folder_path").Find(&folders)
}
// GetHiddenFolderPaths returns a set of hidden folder paths for fast lookup.
func GetHiddenFolderPaths(ctx context.Context, repoID int64) (map[string]bool, error) {
folders, err := GetHiddenFolders(ctx, repoID)
if err != nil {
return nil, err
}
paths := make(map[string]bool, len(folders))
for _, f := range folders {
paths[f.FolderPath] = true
}
return paths, nil
}
// IsHiddenFolder checks if a specific folder path is hidden.
func IsHiddenFolder(ctx context.Context, repoID int64, folderPath string) (bool, error) {
return db.GetEngine(ctx).Where("repo_id = ? AND folder_path = ?", repoID, folderPath).Exist(&RepoHiddenFolder{})
}
// AddHiddenFolder adds a folder to the hidden list.
func AddHiddenFolder(ctx context.Context, repoID int64, folderPath string) error {
_, err := db.GetEngine(ctx).Insert(&RepoHiddenFolder{
RepoID: repoID,
FolderPath: folderPath,
})
return err
}
// RemoveHiddenFolder removes a folder from the hidden list.
func RemoveHiddenFolder(ctx context.Context, repoID int64, folderPath string) error {
_, err := db.GetEngine(ctx).Where("repo_id = ? AND folder_path = ?", repoID, folderPath).Delete(&RepoHiddenFolder{})
return err
}
// IsPathUnderHiddenFolder checks if a path or any of its parent paths is hidden.
func IsPathUnderHiddenFolder(hiddenPaths map[string]bool, targetPath string) bool {
if hiddenPaths[targetPath] {
return true
}
// Check parent paths
for {
idx := strings.LastIndex(targetPath, "/")
if idx < 0 {
break
}
targetPath = targetPath[:idx]
if hiddenPaths[targetPath] {
return true
}
}
return false
}

View File

@@ -4068,6 +4068,17 @@
"repo.settings.gallery_error": "Failed to process gallery image.",
"repo.settings.gallery_size_error": "Image must be less than 5MB.",
"repo.settings.gallery_delete_confirm": "Are you sure you want to delete this image?",
"repo.settings.hidden_folders": "Hidden Folders",
"repo.settings.hidden_folders.description": "Hidden folders are not shown in the code browser for non-admin users. Admins can see them dimmed.",
"repo.settings.hidden_folders.placeholder": "e.g. docs/internal",
"repo.settings.hidden_folders.add": "Hide Folder",
"repo.settings.hidden_folders.none": "No hidden folders configured.",
"repo.settings.hidden_folders.hide": "Hide this folder",
"repo.settings.hidden_folders.unhide": "Unhide this folder",
"repo.settings.hidden_folders.added": "Folder hidden successfully.",
"repo.settings.hidden_folders.removed": "Folder unhidden successfully.",
"repo.settings.hidden_folders.not_found": "Folder path not found in repository.",
"repo.settings.hidden_folders.already_hidden": "This folder is already hidden.",
"repo.gallery": "Gallery",
"api": "API",
"admin.config.api_header_url": "API Header Link",

View File

@@ -0,0 +1,107 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
"strings"
repo_model "code.gitcaddy.com/server/v3/models/repo"
"code.gitcaddy.com/server/v3/modules/log"
"code.gitcaddy.com/server/v3/modules/templates"
"code.gitcaddy.com/server/v3/services/context"
)
const tplHiddenFolders templates.TplName = "repo/settings/hidden_folders"
// HiddenFolders renders the hidden folders settings page.
func HiddenFolders(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.hidden_folders")
ctx.Data["PageIsSettingsHiddenFolders"] = true
folders, err := repo_model.GetHiddenFolders(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetHiddenFolders", err)
return
}
ctx.Data["HiddenFolders"] = folders
ctx.HTML(http.StatusOK, tplHiddenFolders)
}
// HiddenFoldersPost adds a new hidden folder.
func HiddenFoldersPost(ctx *context.Context) {
ctx.Data["PageIsSettingsHiddenFolders"] = true
folderPath := strings.Trim(ctx.FormString("folder_path"), "/")
if folderPath == "" || strings.Contains(folderPath, "..") {
ctx.Flash.Error(ctx.Tr("repo.settings.hidden_folders.not_found"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hidden_folders")
return
}
if err := repo_model.AddHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath); err != nil {
if strings.Contains(err.Error(), "UNIQUE") || strings.Contains(err.Error(), "Duplicate") {
ctx.Flash.Warning(ctx.Tr("repo.settings.hidden_folders.already_hidden"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hidden_folders")
return
}
ctx.ServerError("AddHiddenFolder", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.hidden_folders.added"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hidden_folders")
}
// HiddenFoldersDelete removes a hidden folder.
func HiddenFoldersDelete(ctx *context.Context) {
folderPath := ctx.FormString("folder_path")
if folderPath == "" {
ctx.Flash.Error(ctx.Tr("repo.settings.hidden_folders.not_found"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hidden_folders")
return
}
if err := repo_model.RemoveHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath); err != nil {
ctx.ServerError("RemoveHiddenFolder", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.hidden_folders.removed"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hidden_folders")
}
// HiddenFoldersToggle is an AJAX endpoint to toggle a folder's hidden state.
func HiddenFoldersToggle(ctx *context.Context) {
folderPath := strings.Trim(ctx.FormString("folder_path"), "/")
if folderPath == "" {
ctx.JSON(http.StatusBadRequest, map[string]string{"error": "empty path"})
return
}
isHidden, err := repo_model.IsHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath)
if err != nil {
log.Error("IsHiddenFolder: %v", err)
ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "internal error"})
return
}
if isHidden {
if err := repo_model.RemoveHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath); err != nil {
log.Error("RemoveHiddenFolder: %v", err)
ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "internal error"})
return
}
} else {
if err := repo_model.AddHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath); err != nil {
log.Error("AddHiddenFolder: %v", err)
ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "internal error"})
return
}
}
ctx.JSON(http.StatusOK, map[string]any{
"hidden": !isHidden,
})
}

View File

@@ -10,6 +10,7 @@ import (
"strings"
pull_model "code.gitcaddy.com/server/v3/models/pull"
repo_model "code.gitcaddy.com/server/v3/models/repo"
"code.gitcaddy.com/server/v3/modules/base"
"code.gitcaddy.com/server/v3/modules/fileicon"
"code.gitcaddy.com/server/v3/modules/git"
@@ -144,7 +145,9 @@ func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTr
func TreeViewNodes(ctx *context.Context) {
renderedIconPool := fileicon.NewRenderedIconPool()
results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.RepoLink, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
hiddenPaths, _ := repo_model.GetHiddenFolderPaths(ctx, ctx.Repo.Repository.ID)
isAdmin := ctx.Repo.IsAdmin()
results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.RepoLink, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"), hiddenPaths, isAdmin)
if err != nil {
ctx.ServerError("GetTreeViewNodes", err)
return

View File

@@ -329,6 +329,28 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
}
}
// Filter hidden folders from the listing
hiddenPaths, _ := repo_model.GetHiddenFolderPaths(ctx, ctx.Repo.Repository.ID)
ctx.Data["HiddenFolderPaths"] = hiddenPaths
ctx.Data["IsRepoAdmin"] = ctx.Repo.IsAdmin()
if len(hiddenPaths) > 0 {
isAdmin := ctx.Repo.IsAdmin()
filtered := make([]git.CommitInfo, 0, len(files))
for _, f := range files {
if f.Entry.IsDir() {
fullPath := path.Join(ctx.Repo.TreePath, f.Entry.Name())
if hiddenPaths[fullPath] {
if isAdmin {
filtered = append(filtered, f)
}
continue
}
}
filtered = append(filtered, f)
}
files = filtered
}
ctx.Data["Files"] = files
prepareDirectoryFileIcons(ctx, files)
for _, f := range files {

View File

@@ -416,6 +416,15 @@ func handleRepoViewSubmodule(ctx *context.Context, commitSubmoduleFile *git.Comm
func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
return func(ctx *context.Context) {
// Block non-admin access to hidden folder contents
if ctx.Repo.TreePath != "" && !ctx.Repo.IsAdmin() {
hiddenPaths, _ := repo_model.GetHiddenFolderPaths(ctx, ctx.Repo.Repository.ID)
if len(hiddenPaths) > 0 && repo_model.IsPathUnderHiddenFolder(hiddenPaths, ctx.Repo.TreePath) {
ctx.NotFound(nil)
return
}
}
if entry.IsSubModule() {
commitSubmoduleFile, err := git.GetCommitInfoSubmoduleFile(ctx.Repo.RepoLink, ctx.Repo.TreePath, ctx.Repo.Commit, entry.ID)
if err != nil {

View File

@@ -1195,6 +1195,13 @@ func registerWebRoutes(m *web.Router) {
m.Post("/delete", repo_setting.GalleryDelete)
}, repo.MustBeNotEmpty)
m.Group("/hidden_folders", func() {
m.Get("", repo_setting.HiddenFolders)
m.Post("", repo_setting.HiddenFoldersPost)
m.Post("/delete", repo_setting.HiddenFoldersDelete)
m.Post("/toggle", repo_setting.HiddenFoldersToggle)
})
m.Combo("/public_access").Get(repo_setting.PublicAccess).Post(repo_setting.PublicAccessPost)
m.Group("/collaboration", func() {

View File

@@ -145,6 +145,7 @@ type TreeViewNode struct {
FullPath string `json:"fullPath"`
SubmoduleURL string `json:"submoduleUrl,omitempty"`
Children []*TreeViewNode `json:"children,omitempty"`
IsHidden bool `json:"isHidden,omitempty"`
}
func (node *TreeViewNode) sortLevel() int {
@@ -191,7 +192,7 @@ func sortTreeViewNodes(nodes []*TreeViewNode) {
})
}
func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) {
func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string, hiddenPaths map[string]bool, isAdmin bool) ([]*TreeViewNode, error) {
entries, err := tree.ListEntries()
if err != nil {
return nil, err
@@ -201,13 +202,22 @@ func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *filei
nodes := make([]*TreeViewNode, 0, len(entries))
for _, entry := range entries {
node := newTreeViewNodeFromEntry(ctx, repoLink, renderedIconPool, commit, treePath, entry)
// Filter hidden folders
if entry.IsDir() && len(hiddenPaths) > 0 && hiddenPaths[node.FullPath] {
if !isAdmin {
continue // skip for non-admins
}
node.IsHidden = true
}
nodes = append(nodes, node)
if entry.IsDir() && subPathDirName == entry.Name() {
subTreePath := treePath + "/" + node.EntryName
if subTreePath[0] == '/' {
subTreePath = subTreePath[1:]
}
subNodes, err := listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining)
subNodes, err := listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining, hiddenPaths, isAdmin)
if err != nil {
log.Error("listTreeNodes: %v", err)
} else {
@@ -219,10 +229,10 @@ func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *filei
return nodes, nil
}
func GetTreeViewNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) {
func GetTreeViewNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string, hiddenPaths map[string]bool, isAdmin bool) ([]*TreeViewNode, error) {
entry, err := commit.GetTreeEntryByPath(treePath)
if err != nil {
return nil, err
}
return listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), treePath, subPath)
return listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), treePath, subPath, hiddenPaths, isAdmin)
}

View File

@@ -0,0 +1,38 @@
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit")}}
<div class="repo-setting-content">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.hidden_folders"}}
</h4>
<div class="ui attached segment">
<p class="tw-text-secondary">{{ctx.Locale.Tr "repo.settings.hidden_folders.description"}}</p>
<form class="ui form" method="post" action="{{.Link}}">
{{.CsrfTokenHtml}}
<div class="ui action input tw-w-full">
<input name="folder_path" placeholder="{{ctx.Locale.Tr "repo.settings.hidden_folders.placeholder"}}" required>
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.hidden_folders.add"}}</button>
</div>
</form>
{{if .HiddenFolders}}
<div class="ui divider"></div>
<div class="ui relaxed divided list">
{{range .HiddenFolders}}
<div class="item tw-flex tw-items-center tw-justify-between">
<div class="tw-flex tw-items-center tw-gap-2">
{{svg "octicon-file-directory-fill" 16}} <code>{{.FolderPath}}</code>
</div>
<form method="post" action="{{$.Link}}/delete">
{{$.CsrfTokenHtml}}
<input type="hidden" name="folder_path" value="{{.FolderPath}}">
<button class="ui tiny red button">{{ctx.Locale.Tr "remove"}}</button>
</form>
</div>
{{end}}
</div>
{{else}}
<div class="ui divider"></div>
<p class="tw-text-center tw-text-secondary">{{ctx.Locale.Tr "repo.settings.hidden_folders.none"}}</p>
{{end}}
</div>
</div>
{{template "repo/settings/layout_footer" .}}

View File

@@ -30,6 +30,9 @@
<a class="{{if .PageIsSettingsTags}}active {{end}}item" href="{{.RepoLink}}/settings/tags">
{{ctx.Locale.Tr "repo.settings.tags"}}
</a>
<a class="{{if .PageIsSettingsHiddenFolders}}active {{end}}item" href="{{.RepoLink}}/settings/hidden_folders">
{{ctx.Locale.Tr "repo.settings.hidden_folders"}}
</a>
{{if .SignedUser.CanEditGitHook}}
<a class="{{if .PageIsSettingsGitHooks}}active {{end}}item" href="{{.RepoLink}}/settings/hooks/git">
{{ctx.Locale.Tr "repo.settings.githooks"}}

View File

@@ -1,5 +1,5 @@
{{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}}
<div id="repo-files-table" {{if .HasFilesWithoutLatestCommit}}hx-indicator="#repo-files-table .repo-file-cell.message" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
<div id="repo-files-table" data-toggle-url="{{.RepoLink}}/settings/hidden_folders/toggle" {{if .HasFilesWithoutLatestCommit}}hx-indicator="#repo-files-table .repo-file-cell.message" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
<div class="repo-file-line repo-file-last-commit">
{{template "repo/latest_commit" .}}
<div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div>
@@ -11,10 +11,16 @@
</a>
{{end}}
{{range $item := .Files}}
<div class="repo-file-item">
{{$entry := $item.Entry}}
{{$commit := $item.Commit}}
{{$submoduleFile := $item.SubmoduleFile}}
{{$entry := $item.Entry}}
{{$commit := $item.Commit}}
{{$submoduleFile := $item.SubmoduleFile}}
{{$folderFullPath := $entry.Name}}
{{if $.TreePath}}{{$folderFullPath = printf "%s/%s" $.TreePath $entry.Name}}{{end}}
{{$isHiddenFolder := false}}
{{if and $entry.IsDir $.HiddenFolderPaths}}
{{if index $.HiddenFolderPaths $folderFullPath}}{{$isHiddenFolder = true}}{{end}}
{{end}}
<div class="repo-file-item{{if $isHiddenFolder}} hidden-folder-dimmed{{end}}">
<div class="repo-file-cell name muted-links {{if not $commit}}notready{{end}}">
{{index $.FileIcons $entry.Name}}
{{if $entry.IsSubModule}}
@@ -62,6 +68,17 @@
{{svg "octicon-shield-lock" 16}}
</a>
{{end}}
{{if and $entry.IsDir (not $entry.IsSubModule) $.IsRepoAdmin}}
{{if $isHiddenFolder}}
<button class="repo-file-cell-action btn-octicon hidden-folder-toggle" data-folder-path="{{$folderFullPath}}" data-tooltip-content="{{ctx.Locale.Tr "repo.settings.hidden_folders.unhide"}}">
{{svg "octicon-eye" 16}}
</button>
{{else}}
<button class="repo-file-cell-action btn-octicon hidden-folder-toggle" data-folder-path="{{$folderFullPath}}" data-tooltip-content="{{ctx.Locale.Tr "repo.settings.hidden_folders.hide"}}">
{{svg "octicon-eye-closed" 16}}
</button>
{{end}}
{{end}}
</div>
</div>
{{end}}

View File

@@ -109,3 +109,11 @@
#repo-files-table .repo-file-item:hover .repo-file-cell-action {
display: inline-flex;
}
#repo-files-table .repo-file-item.hidden-folder-dimmed > .repo-file-cell {
opacity: 0.45;
}
#repo-files-table .repo-file-item.hidden-folder-dimmed:hover > .repo-file-cell {
opacity: 0.7;
}

View File

@@ -48,6 +48,7 @@ const onItemClick = (e: MouseEvent) => {
'type-directory': item.entryMode === 'tree',
'type-symlink': item.entryMode === 'symlink',
'type-file': item.entryMode === 'blob' || item.entryMode === 'exec',
'is-hidden-folder': item.isHidden,
}"
:title="item.entryName"
:href="store.buildTreePathWebUrl(item.fullPath)"
@@ -117,4 +118,12 @@ const onItemClick = (e: MouseEvent) => {
text-overflow: ellipsis;
min-width: 0;
}
.tree-item.is-hidden-folder {
opacity: 0.45;
}
.tree-item.is-hidden-folder:hover {
opacity: 0.7;
}
</style>

View File

@@ -12,6 +12,7 @@ export type FileTreeItem = {
fullPath: string;
submoduleUrl?: string;
children?: Array<FileTreeItem>;
isHidden?: boolean;
};
export function createViewFileTreeStore(props: {repoLink: string, treePath: string, currentRefNameSubURL: string}) {

View File

@@ -0,0 +1,26 @@
import {POST} from '../modules/fetch.ts';
export function initRepoHiddenFolderToggle() {
const table = document.querySelector<HTMLElement>('#repo-files-table');
if (!table) return;
const toggleUrl = table.getAttribute('data-toggle-url');
if (!toggleUrl) return;
for (const btn of table.querySelectorAll<HTMLButtonElement>('.hidden-folder-toggle')) {
btn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const folderPath = btn.getAttribute('data-folder-path');
if (!folderPath) return;
const data = new FormData();
data.append('folder_path', folderPath);
const resp = await POST(toggleUrl, {data});
if (resp.ok) {
window.location.reload();
}
});
}
}

View File

@@ -64,6 +64,7 @@ import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton}
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
import {callInitFunctions} from './modules/init.ts';
import {initRepoViewFileTree} from './features/repo-view-file-tree.ts';
import {initRepoHiddenFolderToggle} from './features/repo-hidden-folders.ts';
const initStartTime = performance.now();
const initPerformanceTracer = callInitFunctions([
@@ -136,6 +137,7 @@ const initPerformanceTracer = callInitFunctions([
initRepoReleaseNew,
initRepoTopicBar,
initRepoViewFileTree,
initRepoHiddenFolderToggle,
initRepoWikiForm,
initRepository,
initRepositoryActionView,