diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index c113aa890..ab724e923 100755 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -740,3 +740,9 @@ type CreateCourseForm struct { func (f *CreateCourseForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { return validate(errs, ctx.Data, f, ctx.Locale) } + +// RenameRepoFileForm form for renaming repository file +type RenameRepoFileForm struct { + TreePath string `binding:"Required;MaxSize(500)"` + LastCommit string +} diff --git a/modules/repofiles/temp_repo.go b/modules/repofiles/temp_repo.go index 89f9b0b20..66f7e3487 100644 --- a/modules/repofiles/temp_repo.go +++ b/modules/repofiles/temp_repo.go @@ -109,6 +109,34 @@ func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, erro return filelist, nil } +// LsFilesStage list all files with stage format in index for the given paths +// if the given path is directory ,then return all files under it +// if the given path is file ,then return the file +func (t *TemporaryUploadRepository) LsFilesStage(paths ...string) ([]string, error) { + stdOut := new(bytes.Buffer) + stdErr := new(bytes.Buffer) + + cmdArgs := []string{"ls-files", "-z", "-s", "--"} + for _, arg := range paths { + if arg != "" { + cmdArgs = append(cmdArgs, arg) + } + } + + if err := git.NewCommand(cmdArgs...).RunInDirPipeline(t.basePath, stdOut, stdErr); err != nil { + log.Error("Unable to run git ls-files for temporary repo: %s (%s) Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), t.basePath, err, stdOut.String(), stdErr.String()) + err = fmt.Errorf("Unable to run git ls-files for temporary repo of: %s Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String()) + return nil, err + } + + filelist := make([]string, 0) + for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) { + filelist = append(filelist, string(line)) + } + + return filelist, nil +} + // RemoveFilesFromIndex removes the given files from the index func (t *TemporaryUploadRepository) RemoveFilesFromIndex(filenames ...string) error { stdOut := new(bytes.Buffer) diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index d65f61c84..d7751d50e 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -756,3 +756,210 @@ func createCommitRepoActions(repo *models.Repository, gitRepo *git.Repository, o } return actions, nil } + +// RenameRepoFileOptions +type RenameRepoFileOptions struct { + LastCommitID string + BranchName string + TreePath string + FromTreePath string + Message string + Author *IdentityOptions + Committer *IdentityOptions +} + +// RenameRepoFile rename file in the given repository +func RenameRepoFile(repo *models.Repository, doer *models.User, opts *RenameRepoFileOptions) error { + + // Branch must exist for this operation + if _, err := repo_module.GetBranch(repo, opts.BranchName); err != nil { + return err + } + + //make sure user can commit to the given branch + if err := checkBranchProtection(doer, repo, opts.BranchName, opts.TreePath); err != nil { + return err + } + + // Check that the path given in opts.treePath is valid (not a git path) + treePath := CleanUploadFileName(opts.TreePath) + if treePath == "" { + return models.ErrFilenameInvalid{ + Path: opts.TreePath, + } + } + // If there is a fromTreePath (we are copying it), also clean it up + fromTreePath := CleanUploadFileName(opts.FromTreePath) + if fromTreePath == "" && opts.FromTreePath != "" { + return models.ErrFilenameInvalid{ + Path: opts.FromTreePath, + } + } + + author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) + + t, err := NewTemporaryUploadRepository(repo) + if err != nil { + log.Error("%v", err) + } + defer t.Close() + if err := t.Clone(opts.BranchName); err != nil { + return err + } + if err := t.SetDefaultIndex(); err != nil { + return err + } + + // Get the commit of the original branch + commit, err := t.GetBranchCommit(opts.BranchName) + if err != nil { + return err // Couldn't get a commit for the branch + } + + lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID) + if err != nil { + return fmt.Errorf("DeleteRepoFile: Invalid last commit ID: %v", err) + } + opts.LastCommitID = lastCommitID.String() + + if opts.LastCommitID == "" { + // When updating a file, a lastCommitID needs to be given to make sure other commits + // haven't been made. We throw an error if one wasn't provided. + return models.ErrSHAOrCommitIDNotProvided{} + } + + //if fromTreePath not exist,return error + _, err = commit.GetTreeEntryByPath(fromTreePath) + if err != nil { + return err + } + + // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw + // an error. + if commit.ID.String() != opts.LastCommitID { + if changed, err := commit.FileChangedSinceCommit(fromTreePath, opts.LastCommitID); err != nil { + return err + } else if changed { + return models.ErrCommitIDDoesNotMatch{ + GivenCommitID: opts.LastCommitID, + CurrentCommitID: opts.LastCommitID, + } + } + } + + //if treePath has been exist,return error + _, err = commit.GetTreeEntryByPath(treePath) + if err == nil || !git.IsErrNotExist(err) { + // Means the file has been exist in new path + return models.ErrFilePathInvalid{ + Message: fmt.Sprintf("a file exists where you’re trying to create a subdirectory [path: %s]", treePath), + Path: treePath, + Name: treePath, + Type: git.EntryModeBlob, + } + } + + //move and add files to index + if err = moveAndAddFiles(fromTreePath, treePath, t); err != nil { + return err + } + + // Now write the tree + treeHash, err := t.WriteTree() + if err != nil { + return err + } + + // Now commit the tree + message := strings.TrimSpace(opts.Message) + commitHash, err := t.CommitTree(author, committer, treeHash, message) + if err != nil { + return err + } + + // Then push this tree to NewBranch + if err := t.Push(doer, commitHash, opts.BranchName); err != nil { + log.Error("%T %v", err, err) + return err + } + + return nil +} + +func checkBranchProtection(doer *models.User, repo *models.Repository, branchName, treePath string) error { + //make sure user can commit to the given branch + protectedBranch, err := repo.GetBranchProtection(branchName) + if err != nil { + return err + } + if protectedBranch != nil { + if !protectedBranch.CanUserPush(doer.ID) { + return models.ErrUserCannotCommit{ + UserName: doer.LowerName, + } + } + if protectedBranch.RequireSignedCommits { + _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), branchName) + if err != nil { + if !models.IsErrWontSign(err) { + return err + } + return models.ErrUserCannotCommit{ + UserName: doer.LowerName, + } + } + } + patterns := protectedBranch.GetProtectedFilePatterns() + for _, pat := range patterns { + if pat.Match(strings.ToLower(treePath)) { + return models.ErrFilePathProtected{ + Path: treePath, + } + } + } + } + return nil +} + +func moveAndAddFiles(oldTreePath, newTreePath string, t *TemporaryUploadRepository) error { + array, err := t.LsFilesStage(oldTreePath) + if err != nil { + return err + } + if len(array) == 0 { + return git.ErrNotExist{RelPath: oldTreePath} + } + stdOut := new(bytes.Buffer) + stdErr := new(bytes.Buffer) + stdIn := new(bytes.Buffer) + //write all files in stage format to the stdin, + //for each file,remove old tree path and add new tree path + //see the update-index help document at https://git-scm.com/docs/git-update-index + //especially see the content of "USING --INDEX-INFO" + for _, v := range array { + if v == "" { + continue + } + //example for v(mode SHA-1 stage file) + //100755 d294c88235ac05d3dece028d8a65590f28ec46ac 0 custom/conf/app.ini + v = strings.ReplaceAll(v, "0\t", "") + tmpArray := strings.Split(v, " ") + oldPath := tmpArray[2] + newPath := newTreePath + strings.TrimPrefix(oldPath, oldTreePath) + // mode 0 means remove file + stdIn.WriteString("0 0000000000000000000000000000000000000000\t") + stdIn.WriteString(oldPath) + stdIn.WriteByte('\000') + stdIn.WriteString(tmpArray[0] + " ") + stdIn.WriteString(tmpArray[1] + "\t") + stdIn.WriteString(newPath) + stdIn.WriteByte('\000') + } + + if err := git.NewCommand("update-index", "--replace", "-z", "--index-info").RunInDirFullPipeline(t.basePath, stdOut, stdErr, stdIn); err != nil { + log.Error("Unable to update-index for temporary repo: %s (%s) Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), t.basePath, err, stdOut.String(), stdErr.String()) + return fmt.Errorf("Unable to update-index for temporary repo: %s Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String()) + } + + return nil +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 09d9278fb..741c778a4 100755 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -263,7 +263,7 @@ search_issue=Issue search_pr=Pull Request search_user=User search_org=Organization -search_finded=Find +search_finded=Find search_related=related search_maybe=maybe search_ge= @@ -276,7 +276,7 @@ use_plt__fuction = To use the AI collaboration functions provided by this platfo provide_resoure = Computing resources of CPU/GPU/NPU are provided freely for various types of AI tasks. activity = Activity no_events = There are no events related -or_t = or +or_t = or [explore] repos = Repositories @@ -1301,6 +1301,8 @@ editor.require_signed_commit = Branch requires a signed commit editor.repo_too_large = Repository can not exceed %d MB editor.repo_file_invalid = Upload files are invalid editor.upload_file_too_much = Can not upload more than %d files at a time +editor.rename = rename "%s" to %s" +editor.file_changed_while_renaming=The version of the file or folder to be renamed has changed. Please refresh the page and try again commits.desc = Browse source code change history. @@ -1365,7 +1367,7 @@ issues.add_milestone_at = `added this to the %s milestone %s` issues.change_milestone_at = `modified the milestone from %s to %s %s` issues.remove_milestone_at = `removed this from the %s milestone %s` -issues.add_branch_at=`added this to the %s branch %s` +issues.add_branch_at=`added this to the %s branch %s` issues.add_tag_at =`added this to the %s tag %s` issues.change_branch_tag_at= `modified the branch/tag from %s to %s %s` issues.remove_branch_at=`removed this from the %s branch %s` @@ -3017,4 +3019,4 @@ SNN4IMAGENET = SNN4IMAGENET BRAINSCORE = BRAINSCORE TRAIN = TRAIN INFERENCE = INFERENCE -BENCHMARK = BENCHMARK \ No newline at end of file +BENCHMARK = BENCHMARK diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 6eedeaecd..db933425e 100755 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -271,7 +271,7 @@ search_maybe=约为 search_ge=个 wecome_AI_plt=欢迎来到启智AI协作平台! -explore_AI = 探索更好的AI,来这里发现更有意思的 +explore_AI = 探索更好的AI,来这里发现更有意思的 datasets = 数据集 repositories = 项目 use_plt__fuction = 使用本平台提供的AI协作功能,如:托管代码、共享数据、调试算法或训练模型,请先 @@ -279,7 +279,7 @@ provide_resoure = 平台目前免费提供CPU、GPU、NPU的算力资源,可 create_pro = 创建项目 activity = 活动 no_events = 还没有与您相关的活动 -or_t = 或 +or_t = 或 [explore] @@ -1313,6 +1313,8 @@ editor.require_signed_commit=分支需要签名提交 editor.repo_too_large = 代码仓总大小不能超过%dMB editor.repo_file_invalid = 提交的文件非法 editor.upload_file_too_much = 不能同时提交超过%d个文件 +editor.rename = 重命名"%s"为"%s" +editor.file_changed_while_renaming=待重命名的文件或文件夹版本已发生变化,请您刷新页面后重试 commits.desc=浏览代码修改历史 commits.commits=次代码提交 diff --git a/routers/repo/editor.go b/routers/repo/editor.go index 8e13735df..b389759f5 100644 --- a/routers/repo/editor.go +++ b/routers/repo/editor.go @@ -5,10 +5,12 @@ package repo import ( + "code.gitea.io/gitea/routers/response" repo_service "code.gitea.io/gitea/services/repository" "encoding/json" "fmt" "io/ioutil" + "net/http" "path" "path/filepath" "strings" @@ -795,3 +797,102 @@ func GetClosestParentWithFiles(treePath string, commit *git.Commit) string { } return treePath } + +// RenameFilePost response for editing file +func RenameFilePost(ctx *context.Context, form auth.RenameRepoFileForm) { + renameFilePost(ctx, form) +} + +func renameFilePost(ctx *context.Context, form auth.RenameRepoFileForm) { + if form.TreePath == "" || form.LastCommit == "" { + ctx.JSON(http.StatusOK, response.ServerError("param error")) + return + } + if form.TreePath == ctx.Repo.TreePath { + ctx.JSON(http.StatusOK, response.Success()) + return + } + + canCommit := renderCommitRights(ctx) + branchName := ctx.Repo.BranchName + if ctx.HasError() { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Flash.ErrorMsg)) + return + } + + // Cannot commit to a an existing branch if user doesn't have rights + if branchName == ctx.Repo.BranchName && !canCommit { + ctx.Data["Err_NewBranchName"] = true + ctx.Data["commit_choice"] = frmCommitChoiceNewBranch + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName))) + return + } + + message := ctx.Tr("repo.editor.rename", ctx.Repo.TreePath, form.TreePath) + + if err := repofiles.RenameRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.RenameRepoFileOptions{ + LastCommitID: form.LastCommit, + BranchName: branchName, + FromTreePath: ctx.Repo.TreePath, + TreePath: form.TreePath, + Message: message, + }); err != nil { + // This is where we handle all the errors thrown by repofiles.CreateOrUpdateRepoFile + if git.IsErrNotExist(err) { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath))) + } else if models.IsErrLFSFileLocked(err) { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Tr("repo.editor.upload_file_is_locked", err.(models.ErrLFSFileLocked).Path, err.(models.ErrLFSFileLocked).UserName)))) + } else if models.IsErrFilenameInvalid(err) { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath))) + } else if models.IsErrFilePathInvalid(err) { + if fileErr, ok := err.(models.ErrFilePathInvalid); ok { + switch fileErr.Type { + case git.EntryModeSymlink: + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path))) + case git.EntryModeTree: + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path))) + case git.EntryModeBlob: + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path))) + default: + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + } + } else { + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + } + } else if models.IsErrRepoFileAlreadyExists(err) { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.file_already_exists", form.TreePath))) + } else if git.IsErrBranchNotExist(err) { + // For when a user adds/updates a file to a branch that no longer exists + if branchErr, ok := err.(git.ErrBranchNotExist); ok { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name))) + } else { + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + } + } else if models.IsErrBranchAlreadyExists(err) { + // For when a user specifies a new branch that already exists + ctx.Data["Err_NewBranchName"] = true + if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName))) + } else { + ctx.JSON(http.StatusOK, response.ServerError(err.Error())) + ctx.Error(500, err.Error()) + } + } else if models.IsErrCommitIDDoesNotMatch(err) { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.file_changed_while_renaming"))) + } else if git.IsErrPushOutOfDate(err) { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.file_changed_while_renaming"))) + } else if git.IsErrPushRejected(err) { + errPushRej := err.(*git.ErrPushRejected) + if len(errPushRej.Message) == 0 { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.push_rejected_no_message"))) + } else { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.push_rejected", utils.SanitizeFlashErrorString(errPushRej.Message)))) + } + } else { + ctx.JSON(http.StatusOK, response.ServerError(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, utils.SanitizeFlashErrorString(err.Error())))) + } + return + } + ctx.JSON(http.StatusOK, response.Success()) + +} diff --git a/routers/repo/view.go b/routers/repo/view.go index b28e21aa1..6880d5261 100755 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -608,6 +608,11 @@ func getContributorInfo(contributorInfos []*ContributorInfo, email string) *Cont // Home render repository home page func Home(ctx *context.Context) { + if ctx.Repo.CanEnableEditor() { + ctx.Data["CanEditFile"] = true + } else { + ctx.Data["CanEditFile"] = false + } if len(ctx.Repo.Units) > 0 { //get repo contributors info contributors, err := git.GetContributors(ctx.Repo.Repository.RepoPath(), ctx.Repo.BranchName) diff --git a/routers/response/response.go b/routers/response/response.go new file mode 100644 index 000000000..edd3b9cca --- /dev/null +++ b/routers/response/response.go @@ -0,0 +1,32 @@ +package response + +const ( + RESPONSE_CODE_SUCCESS = 0 + RESPONSE_MSG_SUCCESS = "ok" + RESPONSE_CODE_ERROR_DEFAULT = 99 +) + +type AiforgeResponse struct { + Code int + Msg string + Data interface{} +} + +func Success() *AiforgeResponse { + return &AiforgeResponse{Code: RESPONSE_CODE_SUCCESS, Msg: RESPONSE_MSG_SUCCESS} +} + +func Error(code int, msg string) *AiforgeResponse { + return &AiforgeResponse{Code: code, Msg: msg} +} + +func ServerError(msg string) *AiforgeResponse { + return &AiforgeResponse{Code: RESPONSE_CODE_ERROR_DEFAULT, Msg: msg} +} + +func SuccessWithData(data interface{}) *AiforgeResponse { + return &AiforgeResponse{Code: RESPONSE_CODE_ERROR_DEFAULT, Msg: RESPONSE_MSG_SUCCESS, Data: data} +} +func ErrorWithData(code int, msg string, data interface{}) *AiforgeResponse { + return &AiforgeResponse{Code: code, Msg: msg, Data: data} +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 1e1a862ff..ccfb1df6c 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -935,6 +935,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Combo("/_upload/*", repo.MustBeAbleToUpload). Get(repo.UploadFile). Post(bindIgnErr(auth.UploadRepoFileForm{}), repo.UploadFilePost) + m.Post("/_rename/*", bindIgnErr(auth.RenameRepoFileForm{}), repo.RenameFilePost) }, context.RepoRefByType(context.RepoRefBranch), repo.MustBeEditable) m.Group("", func() { m.Post("/upload-file", repo.UploadFileToServer) diff --git a/templates/repo/cloudbrain/models/dir_list.tmpl b/templates/repo/cloudbrain/models/dir_list.tmpl index f7aaee009..4c87d3d83 100755 --- a/templates/repo/cloudbrain/models/dir_list.tmpl +++ b/templates/repo/cloudbrain/models/dir_list.tmpl @@ -20,8 +20,9 @@ {{.ModTime}} + {{end}} -{{end}} +{{end}} \ No newline at end of file diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index d253e4752..7c44aa147 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -1,102 +1,114 @@ {{template "base/head" .}}
{{template "repo/header" .}}
+ {{template "base/alert" .}} {{if and .Permission.IsAdmin (not .Repository.IsArchived)}} - - {{end}} -
- {{.i18n.Tr "repo.topic.count_prompt"}} - {{.i18n.Tr "repo.topic.format_prompt"}} -
+ {{end}} +
+ {{.i18n.Tr "repo.topic.count_prompt"}} + {{.i18n.Tr "repo.topic.format_prompt"}} +
{{if .RepoSearchEnabled}} - {{if .Repository.IsArchived}} -
- {{.i18n.Tr "repo.archive.title"}} -
+
+ {{.i18n.Tr "repo.archive.title"}} +
{{end}} {{template "repo/sub_menu" .}}