diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b5b90b31a53cb..6712250924361 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1354,8 +1354,11 @@ editor.this_file_locked = File is locked editor.must_be_on_a_branch = You must be on a branch to make or propose changes to this file. editor.fork_before_edit = You must fork this repository to make or propose changes to this file. editor.delete_this_file = Delete File +editor.delete_this_directory = Delete Directory editor.must_have_write_access = You must have write access to make or propose changes to this file. editor.file_delete_success = File "%s" has been deleted. +editor.directory_delete_success = Directory "%s" has been deleted. +editor.delete_directory = Delete directory '%s' editor.name_your_file = Name your file⦠editor.filename_help = Add a directory by typing its name followed by a slash ('/'). Remove a directory by typing backspace at the beginning of the input field. editor.or = or diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index ec34d54d2248b..27a0827a10f20 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -610,10 +610,6 @@ func handleChangeRepoFilesError(ctx *context.APIContext, err error) { ctx.APIError(http.StatusUnprocessableEntity, err) return } - if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { - ctx.APIError(http.StatusNotFound, err) - return - } if errors.Is(err, util.ErrNotExist) { ctx.APIError(http.StatusNotFound, err) return diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index e304633f95238..0eebff6aa88b8 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -10,7 +10,6 @@ import ( "net/url" "path" "strconv" - "strings" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -42,8 +41,8 @@ type blameRow struct { // RefBlame render blame page func RefBlame(ctx *context.Context) { - ctx.Data["PageIsViewCode"] = true ctx.Data["IsBlame"] = true + prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL()) // Get current entry user currently looking at. if ctx.Repo.TreePath == "" { @@ -56,17 +55,6 @@ func RefBlame(ctx *context.Context) { return } - treeNames := strings.Split(ctx.Repo.TreePath, "/") - var paths []string - for i := range treeNames { - paths = append(paths, strings.Join(treeNames[:i+1], "/")) - } - - ctx.Data["Paths"] = paths - ctx.Data["TreeNames"] = treeNames - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) - blob := entry.Blob() fileSize := blob.Size() ctx.Data["FileSize"] = fileSize diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 8c630cb35f0a6..983249a6d247e 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -41,7 +41,12 @@ const ( editorCommitChoiceNewBranch string = "commit-to-new-branch" ) -func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions { +func prepareEditorPage(ctx *context.Context, editorAction string) *context.CommitFormOptions { + prepareHomeTreeSideBarSwitch(ctx) + return prepareEditorPageFormOptions(ctx, editorAction) +} + +func prepareEditorPageFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions { cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath) if cleanedTreePath != ctx.Repo.TreePath { redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath)) @@ -283,7 +288,7 @@ func EditFile(ctx *context.Context) { // on the "New File" page, we should add an empty path field to make end users could input a new name prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath)) - prepareEditorCommitFormOptions(ctx, editorAction) + prepareEditorPage(ctx, editorAction) if ctx.Written() { return } @@ -376,15 +381,16 @@ func EditFilePost(ctx *context.Context) { // DeleteFile render delete file page func DeleteFile(ctx *context.Context) { - prepareEditorCommitFormOptions(ctx, "_delete") + prepareEditorPage(ctx, "_delete") if ctx.Written() { return } ctx.Data["PageIsDelete"] = true + prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath) ctx.HTML(http.StatusOK, tplDeleteFile) } -// DeleteFilePost response for deleting file +// DeleteFilePost response for deleting file or directory func DeleteFilePost(ctx *context.Context) { parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx) if ctx.Written() { @@ -392,17 +398,37 @@ func DeleteFilePost(ctx *context.Context) { } treePath := ctx.Repo.TreePath - _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + if treePath == "" { + ctx.JSONError("cannot delete root directory") // it should not happen unless someone is trying to be malicious + return + } + + // Check if the path is a directory + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) + if err != nil { + ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err) + return + } + + var commitMessage string + if entry.IsDir() { + commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete_directory", treePath)) + } else { + commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)) + } + + _, err = files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: parsed.form.LastCommit, OldBranch: parsed.OldBranchName, NewBranch: parsed.NewBranchName, Files: []*files_service.ChangeRepoFile{ { - Operation: "delete", - TreePath: treePath, + Operation: "delete", + TreePath: treePath, + DeleteRecursively: true, }, }, - Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)), + Message: commitMessage, Signoff: parsed.form.Signoff, Author: parsed.GitCommitter, Committer: parsed.GitCommitter, @@ -412,7 +438,11 @@ func DeleteFilePost(ctx *context.Context) { return } - ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) + if entry.IsDir() { + ctx.Flash.Success(ctx.Tr("repo.editor.directory_delete_success", treePath)) + } else { + ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) + } redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath) redirectForCommitChoice(ctx, parsed, redirectTreePath) } @@ -420,7 +450,7 @@ func DeleteFilePost(ctx *context.Context) { func UploadFile(ctx *context.Context) { ctx.Data["PageIsUpload"] = true prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath) - opts := prepareEditorCommitFormOptions(ctx, "_upload") + opts := prepareEditorPage(ctx, "_upload") if ctx.Written() { return } diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go index aad7b4129c795..357c6f3a21aa2 100644 --- a/routers/web/repo/editor_apply_patch.go +++ b/routers/web/repo/editor_apply_patch.go @@ -14,7 +14,7 @@ import ( ) func NewDiffPatch(ctx *context.Context) { - prepareEditorCommitFormOptions(ctx, "_diffpatch") + prepareEditorPage(ctx, "_diffpatch") if ctx.Written() { return } diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go index 099814a9fa9bb..32e3c58e8748e 100644 --- a/routers/web/repo/editor_cherry_pick.go +++ b/routers/web/repo/editor_cherry_pick.go @@ -16,7 +16,7 @@ import ( ) func CherryPick(ctx *context.Context) { - prepareEditorCommitFormOptions(ctx, "_cherrypick") + prepareEditorPage(ctx, "_cherrypick") if ctx.Written() { return } diff --git a/routers/web/repo/find.go b/routers/web/repo/find.go deleted file mode 100644 index 3a3a7610e772b..0000000000000 --- a/routers/web/repo/find.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "net/http" - - "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/services/context" -) - -const ( - tplFindFiles templates.TplName = "repo/find/files" -) - -// FindFiles render the page to find repository files -func FindFiles(ctx *context.Context) { - path := ctx.PathParam("*") - ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + util.PathEscapeSegments(path) - ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + util.PathEscapeSegments(path) - ctx.HTML(http.StatusOK, tplFindFiles) -} diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 09ac33cff43f0..8e85cc327858f 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -245,27 +245,17 @@ func LastCommit(ctx *context.Context) { return } + // The "/lastcommit/" endpoint is used to render the embedded HTML content for the directory file listing with latest commit info + // It needs to construct correct links to the file items, but the route only accepts a commit ID, not a full ref name (branch or tag). + // So we need to get the ref name from the query parameter "refSubUrl". + // TODO: LAST-COMMIT-ASYNC-LOADING: it needs more tests to cover this + refSubURL := path.Clean(ctx.FormString("refSubUrl")) + prepareRepoViewContent(ctx, util.IfZero(refSubURL, ctx.Repo.RefTypeNameSubURL())) renderDirectoryFiles(ctx, 0) if ctx.Written() { return } - var treeNames []string - paths := make([]string, 0, 5) - if len(ctx.Repo.TreePath) > 0 { - treeNames = strings.Split(ctx.Repo.TreePath, "/") - for i := range treeNames { - paths = append(paths, strings.Join(treeNames[:i+1], "/")) - } - - ctx.Data["HasParentPath"] = true - if len(paths)-2 >= 0 { - ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] - } - } - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["BranchLink"] = branchLink - ctx.HTML(http.StatusOK, tplRepoViewList) } @@ -289,7 +279,9 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri return nil } - ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + // TODO: LAST-COMMIT-ASYNC-LOADING: search this keyword to see more details + lastCommitLoaderURL := ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + ctx.Data["LastCommitLoaderURL"] = lastCommitLoaderURL + "?refSubUrl=" + url.QueryEscape(ctx.Repo.RefTypeNameSubURL()) // Get current entry user currently looking at. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) @@ -322,6 +314,21 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri ctx.ServerError("GetCommitsInfo", err) return nil } + + { + if timeout != 0 && !setting.IsProd && !setting.IsInTesting { + log.Debug("first call to get directory file commit info") + clearFilesCommitInfo := func() { + log.Warn("clear directory file commit info to force async loading on frontend") + for i := range files { + files[i].Commit = nil + } + } + _ = clearFilesCommitInfo + // clearFilesCommitInfo() // TODO: LAST-COMMIT-ASYNC-LOADING: debug the frontend async latest commit info loading, uncomment this line, and it needs more tests + } + } + ctx.Data["Files"] = files prepareDirectoryFileIcons(ctx, files) for _, f := range files { @@ -334,16 +341,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri if !loadLatestCommitData(ctx, latestCommit) { return nil } - - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - treeLink := branchLink - - if len(ctx.Repo.TreePath) > 0 { - treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) - } - - ctx.Data["TreeLink"] = treeLink - return allEntries } diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 17043055e5ff9..00d30bedef5ed 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -362,6 +362,32 @@ func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) b return false } +func prepareRepoViewContent(ctx *context.Context, refTypeNameSubURL string) { + // for: home, file list, file view, blame + ctx.Data["PageIsViewCode"] = true + ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show Upload File button or menu item + + // prepare the tree path navigation + var treeNames, paths []string + branchLink := ctx.Repo.RepoLink + "/src/" + refTypeNameSubURL + treeLink := branchLink + if ctx.Repo.TreePath != "" { + treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + treeNames = strings.Split(ctx.Repo.TreePath, "/") + for i := range treeNames { + paths = append(paths, strings.Join(treeNames[:i+1], "/")) + } + ctx.Data["HasParentPath"] = true + if len(paths)-2 >= 0 { + ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] + } + } + ctx.Data["Paths"] = paths + ctx.Data["TreeLink"] = treeLink + ctx.Data["TreeNames"] = treeNames + ctx.Data["BranchLink"] = branchLink +} + // Home render repository home page func Home(ctx *context.Context) { if handleRepoHomeFeed(ctx) { @@ -383,8 +409,7 @@ func Home(ctx *context.Context) { title += ": " + ctx.Repo.Repository.Description } ctx.Data["Title"] = title - ctx.Data["PageIsViewCode"] = true - ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show New File / Upload File buttons + prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL()) if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { // empty or broken repositories need to be handled differently @@ -405,26 +430,6 @@ func Home(ctx *context.Context) { return } - // prepare the tree path - var treeNames, paths []string - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - treeLink := branchLink - if ctx.Repo.TreePath != "" { - treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) - treeNames = strings.Split(ctx.Repo.TreePath, "/") - for i := range treeNames { - paths = append(paths, strings.Join(treeNames[:i+1], "/")) - } - ctx.Data["HasParentPath"] = true - if len(paths)-2 >= 0 { - ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] - } - } - ctx.Data["Paths"] = paths - ctx.Data["TreeLink"] = treeLink - ctx.Data["TreeNames"] = treeNames - ctx.Data["BranchLink"] = branchLink - // some UI components are only shown when the tree path is root isTreePathRoot := ctx.Repo.TreePath == "" @@ -455,7 +460,7 @@ func Home(ctx *context.Context) { if isViewHomeOnlyContent(ctx) { ctx.HTML(http.StatusOK, tplRepoViewContent) - } else if len(treeNames) != 0 { + } else if ctx.Repo.TreePath != "" { ctx.HTML(http.StatusOK, tplRepoView) } else { ctx.HTML(http.StatusOK, tplRepoHome) diff --git a/routers/web/web.go b/routers/web/web.go index dd1b391c68983..89a570dce0773 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1184,7 +1184,6 @@ func registerWebRoutes(m *web.Router) { m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) m.Group("/{username}/{reponame}", func() { - m.Get("/find/*", repo.FindFiles) m.Group("/tree-list", func() { m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeList) m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList) diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index feb4811bb02ac..731f23855d28a 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -135,6 +135,14 @@ func (t *TemporaryUploadRepository) LsFiles(ctx context.Context, filenames ...st return fileList, nil } +func (t *TemporaryUploadRepository) RemoveRecursivelyFromIndex(ctx context.Context, path string) error { + _, _, err := gitcmd.NewCommand("rm", "--cached", "-r"). + AddDynamicArguments(path). + WithDir(t.basePath). + RunStdBytes(ctx) + return err +} + // RemoveFilesFromIndex removes the given files from the index func (t *TemporaryUploadRepository) RemoveFilesFromIndex(ctx context.Context, filenames ...string) error { objFmt, err := t.gitRepo.GetObjectFormat() diff --git a/services/repository/files/update.go b/services/repository/files/update.go index b07055d57aaa6..4830f711fc284 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -46,7 +46,10 @@ type ChangeRepoFile struct { FromTreePath string ContentReader io.ReadSeeker SHA string - Options *RepoFileOptions + + DeleteRecursively bool // when deleting, work as `git rm -r ...` + + Options *RepoFileOptions // FIXME: need to refactor, internal usage only } // ChangeRepoFilesOptions holds the repository files update options @@ -69,26 +72,6 @@ type RepoFileOptions struct { executable bool } -// ErrRepoFileDoesNotExist represents a "RepoFileDoesNotExist" kind of error. -type ErrRepoFileDoesNotExist struct { - Path string - Name string -} - -// IsErrRepoFileDoesNotExist checks if an error is a ErrRepoDoesNotExist. -func IsErrRepoFileDoesNotExist(err error) bool { - _, ok := err.(ErrRepoFileDoesNotExist) - return ok -} - -func (err ErrRepoFileDoesNotExist) Error() string { - return fmt.Sprintf("repository file does not exist [path: %s]", err.Path) -} - -func (err ErrRepoFileDoesNotExist) Unwrap() error { - return util.ErrNotExist -} - type LazyReadSeeker interface { io.ReadSeeker io.Closer @@ -217,24 +200,6 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } } - for _, file := range opts.Files { - if file.Operation == "delete" { - // Get the files in the index - filesInIndex, err := t.LsFiles(ctx, file.TreePath) - if err != nil { - return nil, fmt.Errorf("DeleteRepoFile: %w", err) - } - - // Find the file we want to delete in the index - inFilelist := slices.Contains(filesInIndex, file.TreePath) - if !inFilelist { - return nil, ErrRepoFileDoesNotExist{ - Path: file.TreePath, - } - } - } - } - if hasOldBranch { // Get the commit of the original branch commit, err := t.GetBranchCommit(opts.OldBranch) @@ -272,8 +237,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use addedLfsPointers = append(addedLfsPointers, *addedLfsPointer) } case "delete": - if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil { - return nil, err + if file.DeleteRecursively { + if err = t.RemoveRecursivelyFromIndex(ctx, file.TreePath); err != nil { + return nil, err + } + } else { + if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil { + return nil, err + } } default: return nil, fmt.Errorf("invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath) diff --git a/templates/repo/editor/delete.tmpl b/templates/repo/editor/delete.tmpl index bf6143f1cb91a..70769326a7c28 100644 --- a/templates/repo/editor/delete.tmpl +++ b/templates/repo/editor/delete.tmpl @@ -1,13 +1,30 @@ {{template "base/head" .}}