75ce1e2ac1
- Add a dropdown to the web interface for changing files to select which Email should be used for the commit. It only shows (and verifies) that a activated mail can be used, while this isn't necessary, it's better to have this already in place. - Added integration testing. - Resolves https://codeberg.org/forgejo/forgejo/issues/281 (cherry picked from commit 564e701f407c0e110f3c7a4102bf7ed7902b815f) (cherry picked from commit de8f2e03cc7d274049dd6a849b3d226968782644) (cherry picked from commit 0182cff12ed4b68bd49ebc2b9951d9a29f7a36ca) (cherry picked from commit 9c74254d4606febd702315c670db4fb6b14040a1) (cherry picked from commit 2f0b68f821ae53dd12b496cc660353d5bf7cd143) (cherry picked from commit 079b995d49ba7a625035fe9ec53741f6b0112007) (cherry picked from commit 6952ea6ee3de8157d056c4381de7529de6eaef7b) (cherry picked from commit 6c7d5a5d140152be80ec38a979a2a7b704ce653a) (cherry picked from commit 49c39f0ed5a011b26f2e33f35811bb31fab3cf64) (cherry picked from commit a8f9727388192c6c22b2f8cbbae15a96203ec3b6)
943 lines
34 KiB
Go
943 lines
34 KiB
Go
// Copyright 2016 The Gogs Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models"
|
|
git_model "code.gitea.io/gitea/models/git"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/models/unit"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/base"
|
|
"code.gitea.io/gitea/modules/charset"
|
|
"code.gitea.io/gitea/modules/context"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/json"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/markup"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/typesniffer"
|
|
"code.gitea.io/gitea/modules/upload"
|
|
"code.gitea.io/gitea/modules/util"
|
|
"code.gitea.io/gitea/modules/web"
|
|
"code.gitea.io/gitea/routers/utils"
|
|
"code.gitea.io/gitea/services/forms"
|
|
files_service "code.gitea.io/gitea/services/repository/files"
|
|
)
|
|
|
|
const (
|
|
tplEditFile base.TplName = "repo/editor/edit"
|
|
tplEditDiffPreview base.TplName = "repo/editor/diff_preview"
|
|
tplDeleteFile base.TplName = "repo/editor/delete"
|
|
tplUploadFile base.TplName = "repo/editor/upload"
|
|
|
|
frmCommitChoiceDirect string = "direct"
|
|
frmCommitChoiceNewBranch string = "commit-to-new-branch"
|
|
)
|
|
|
|
func canCreateBasePullRequest(ctx *context.Context) bool {
|
|
baseRepo := ctx.Repo.Repository.BaseRepo
|
|
return baseRepo != nil && baseRepo.UnitEnabled(ctx, unit.TypePullRequests)
|
|
}
|
|
|
|
func renderCommitRights(ctx *context.Context) bool {
|
|
canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer)
|
|
if err != nil {
|
|
log.Error("CanCommitToBranch: %v", err)
|
|
}
|
|
ctx.Data["CanCommitToBranch"] = canCommitToBranch
|
|
ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx)
|
|
|
|
return canCommitToBranch.CanCommitToBranch
|
|
}
|
|
|
|
// redirectForCommitChoice redirects after committing the edit to a branch
|
|
func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, treePath string) {
|
|
if commitChoice == frmCommitChoiceNewBranch {
|
|
// Redirect to a pull request when possible
|
|
redirectToPullRequest := false
|
|
repo := ctx.Repo.Repository
|
|
baseBranch := ctx.Repo.BranchName
|
|
headBranch := newBranchName
|
|
if repo.UnitEnabled(ctx, unit.TypePullRequests) {
|
|
redirectToPullRequest = true
|
|
} else if canCreateBasePullRequest(ctx) {
|
|
redirectToPullRequest = true
|
|
baseBranch = repo.BaseRepo.DefaultBranch
|
|
headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
|
|
repo = repo.BaseRepo
|
|
}
|
|
|
|
if redirectToPullRequest {
|
|
ctx.Redirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
|
|
return
|
|
}
|
|
}
|
|
|
|
// Redirect to viewing file or folder
|
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(newBranchName) + "/" + util.PathEscapeSegments(treePath))
|
|
}
|
|
|
|
// getParentTreeFields returns list of parent tree names and corresponding tree paths
|
|
// based on given tree path.
|
|
func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
|
|
if len(treePath) == 0 {
|
|
return treeNames, treePaths
|
|
}
|
|
|
|
treeNames = strings.Split(treePath, "/")
|
|
treePaths = make([]string, len(treeNames))
|
|
for i := range treeNames {
|
|
treePaths[i] = strings.Join(treeNames[:i+1], "/")
|
|
}
|
|
return treeNames, treePaths
|
|
}
|
|
|
|
// getSelectableEmailAddresses returns which emails can be used by the user as
|
|
// email for a Git commiter.
|
|
func getSelectableEmailAddresses(ctx *context.Context) ([]*user_model.ActivatedEmailAddress, error) {
|
|
// Retrieve emails that the user could use for commiter identity.
|
|
commitEmails, err := user_model.GetActivatedEmailAddresses(ctx, ctx.Doer.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetActivatedEmailAddresses: %w", err)
|
|
}
|
|
|
|
// Allow for the placeholder mail to be used. Use -1 as ID to identify
|
|
// this entry to be the placerholder mail of the user.
|
|
placeholderMail := &user_model.ActivatedEmailAddress{ID: -1, Email: ctx.Doer.GetPlaceholderEmail()}
|
|
if ctx.Doer.KeepEmailPrivate {
|
|
commitEmails = append([]*user_model.ActivatedEmailAddress{placeholderMail}, commitEmails...)
|
|
} else {
|
|
commitEmails = append(commitEmails, placeholderMail)
|
|
}
|
|
|
|
return commitEmails, nil
|
|
}
|
|
|
|
func editFile(ctx *context.Context, isNewFile bool) {
|
|
ctx.Data["PageIsEdit"] = true
|
|
ctx.Data["IsNewFile"] = isNewFile
|
|
canCommit := renderCommitRights(ctx)
|
|
|
|
treePath := cleanUploadFileName(ctx.Repo.TreePath)
|
|
if treePath != ctx.Repo.TreePath {
|
|
if isNewFile {
|
|
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
|
|
} else {
|
|
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check if the filename (and additional path) is specified in the querystring
|
|
// (filename is a misnomer, but kept for compatibility with GitHub)
|
|
filePath, fileName := path.Split(ctx.Req.URL.Query().Get("filename"))
|
|
filePath = strings.Trim(filePath, "/")
|
|
treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath))
|
|
|
|
if !isNewFile {
|
|
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
|
if err != nil {
|
|
HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
|
|
return
|
|
}
|
|
|
|
// No way to edit a directory online.
|
|
if entry.IsDir() {
|
|
ctx.NotFound("entry.IsDir", nil)
|
|
return
|
|
}
|
|
|
|
blob := entry.Blob()
|
|
if blob.Size() >= setting.UI.MaxDisplayFileSize {
|
|
ctx.NotFound("blob.Size", err)
|
|
return
|
|
}
|
|
|
|
dataRc, err := blob.DataAsync()
|
|
if err != nil {
|
|
ctx.NotFound("blob.Data", err)
|
|
return
|
|
}
|
|
|
|
defer dataRc.Close()
|
|
|
|
ctx.Data["FileSize"] = blob.Size()
|
|
ctx.Data["FileName"] = blob.Name()
|
|
|
|
buf := make([]byte, 1024)
|
|
n, _ := util.ReadAtMost(dataRc, buf)
|
|
buf = buf[:n]
|
|
|
|
// Only some file types are editable online as text.
|
|
if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
|
|
ctx.NotFound("typesniffer.IsRepresentableAsText", nil)
|
|
return
|
|
}
|
|
|
|
d, _ := io.ReadAll(dataRc)
|
|
if err := dataRc.Close(); err != nil {
|
|
log.Error("Error whilst closing blob data: %v", err)
|
|
}
|
|
|
|
buf = append(buf, d...)
|
|
if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
|
|
log.Error("ToUTF8: %v", err)
|
|
ctx.Data["FileContent"] = string(buf)
|
|
} else {
|
|
ctx.Data["FileContent"] = content
|
|
}
|
|
} else {
|
|
// Append filename from query, or empty string to allow user name the new file.
|
|
treeNames = append(treeNames, fileName)
|
|
}
|
|
|
|
commitEmails, err := getSelectableEmailAddresses(ctx)
|
|
if err != nil {
|
|
ctx.ServerError("getSelectableEmailAddresses", err)
|
|
return
|
|
}
|
|
|
|
ctx.Data["TreeNames"] = treeNames
|
|
ctx.Data["TreePaths"] = treePaths
|
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
|
ctx.Data["commit_summary"] = ""
|
|
ctx.Data["commit_message"] = ""
|
|
if canCommit {
|
|
ctx.Data["commit_choice"] = frmCommitChoiceDirect
|
|
} else {
|
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
|
}
|
|
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
|
|
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
|
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
|
|
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
|
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
|
|
ctx.Data["CommitMails"] = commitEmails
|
|
ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail()
|
|
|
|
ctx.HTML(http.StatusOK, tplEditFile)
|
|
}
|
|
|
|
// GetEditorConfig returns a editorconfig JSON string for given treePath or "null"
|
|
func GetEditorConfig(ctx *context.Context, treePath string) string {
|
|
ec, _, err := ctx.Repo.GetEditorconfig()
|
|
if err == nil {
|
|
def, err := ec.GetDefinitionForFilename(treePath)
|
|
if err == nil {
|
|
jsonStr, _ := json.Marshal(def)
|
|
return string(jsonStr)
|
|
}
|
|
}
|
|
return "null"
|
|
}
|
|
|
|
// EditFile render edit file page
|
|
func EditFile(ctx *context.Context) {
|
|
editFile(ctx, false)
|
|
}
|
|
|
|
// NewFile render create file page
|
|
func NewFile(ctx *context.Context) {
|
|
editFile(ctx, true)
|
|
}
|
|
|
|
func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
|
|
canCommit := renderCommitRights(ctx)
|
|
treeNames, treePaths := getParentTreeFields(form.TreePath)
|
|
branchName := ctx.Repo.BranchName
|
|
if form.CommitChoice == frmCommitChoiceNewBranch {
|
|
branchName = form.NewBranchName
|
|
}
|
|
|
|
commitEmails, err := getSelectableEmailAddresses(ctx)
|
|
if err != nil {
|
|
ctx.ServerError("getSelectableEmailAddresses", err)
|
|
return
|
|
}
|
|
|
|
ctx.Data["PageIsEdit"] = true
|
|
ctx.Data["PageHasPosted"] = true
|
|
ctx.Data["IsNewFile"] = isNewFile
|
|
ctx.Data["TreePath"] = form.TreePath
|
|
ctx.Data["TreeNames"] = treeNames
|
|
ctx.Data["TreePaths"] = treePaths
|
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
|
|
ctx.Data["FileContent"] = form.Content
|
|
ctx.Data["commit_summary"] = form.CommitSummary
|
|
ctx.Data["commit_message"] = form.CommitMessage
|
|
ctx.Data["commit_choice"] = form.CommitChoice
|
|
ctx.Data["new_branch_name"] = form.NewBranchName
|
|
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
|
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
|
|
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
|
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)
|
|
ctx.Data["CommitMails"] = commitEmails
|
|
ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail()
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(http.StatusOK, tplEditFile)
|
|
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.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
|
|
return
|
|
}
|
|
|
|
// CommitSummary is optional in the web form, if empty, give it a default message based on add or update
|
|
// `message` will be both the summary and message combined
|
|
message := strings.TrimSpace(form.CommitSummary)
|
|
if len(message) == 0 {
|
|
if isNewFile {
|
|
message = ctx.Tr("repo.editor.add", form.TreePath)
|
|
} else {
|
|
message = ctx.Tr("repo.editor.update", form.TreePath)
|
|
}
|
|
}
|
|
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
|
|
if len(form.CommitMessage) > 0 {
|
|
message += "\n\n" + form.CommitMessage
|
|
}
|
|
|
|
operation := "update"
|
|
if isNewFile {
|
|
operation = "create"
|
|
}
|
|
|
|
gitIdentity := &files_service.IdentityOptions{
|
|
Name: ctx.Doer.Name,
|
|
}
|
|
|
|
// -1 is defined as placeholder email.
|
|
if form.CommitMailID == -1 {
|
|
gitIdentity.Email = ctx.Doer.GetPlaceholderEmail()
|
|
} else {
|
|
// Check if the given email is activated.
|
|
email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, form.CommitMailID)
|
|
if err != nil {
|
|
ctx.ServerError("GetEmailAddressByID", err)
|
|
return
|
|
}
|
|
|
|
if email == nil || !email.IsActivated {
|
|
ctx.Data["Err_CommitMailID"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_mail"), tplEditFile, &form)
|
|
return
|
|
}
|
|
|
|
gitIdentity.Email = email.Email
|
|
}
|
|
|
|
if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
|
LastCommitID: form.LastCommit,
|
|
OldBranch: ctx.Repo.BranchName,
|
|
NewBranch: branchName,
|
|
Message: message,
|
|
Files: []*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: operation,
|
|
FromTreePath: ctx.Repo.TreePath,
|
|
TreePath: form.TreePath,
|
|
ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
|
|
},
|
|
},
|
|
Signoff: form.Signoff,
|
|
Author: gitIdentity,
|
|
Committer: gitIdentity,
|
|
}); err != nil {
|
|
// This is where we handle all the errors thrown by files_service.ChangeRepoFiles
|
|
if git.IsErrNotExist(err) {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
|
|
} else if git_model.IsErrLFSFileLocked(err) {
|
|
ctx.Data["Err_TreePath"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form)
|
|
} else if models.IsErrFilenameInvalid(err) {
|
|
ctx.Data["Err_TreePath"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
|
|
} else if models.IsErrFilePathInvalid(err) {
|
|
ctx.Data["Err_TreePath"] = true
|
|
if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
|
|
switch fileErr.Type {
|
|
case git.EntryModeSymlink:
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
|
|
case git.EntryModeTree:
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
|
|
case git.EntryModeBlob:
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
|
|
default:
|
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
|
}
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
|
}
|
|
} else if models.IsErrRepoFileAlreadyExists(err) {
|
|
ctx.Data["Err_TreePath"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
|
|
} 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.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
|
}
|
|
} else if git_model.IsErrBranchAlreadyExists(err) {
|
|
// For when a user specifies a new branch that already exists
|
|
ctx.Data["Err_NewBranchName"] = true
|
|
if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
|
}
|
|
} else if models.IsErrCommitIDDoesNotMatch(err) {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplEditFile, &form)
|
|
} else if git.IsErrPushOutOfDate(err) {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form)
|
|
} else if git.IsErrPushRejected(err) {
|
|
errPushRej := err.(*git.ErrPushRejected)
|
|
if len(errPushRej.Message) == 0 {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
|
|
} else {
|
|
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
|
|
"Message": ctx.Tr("repo.editor.push_rejected"),
|
|
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
|
|
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("editFilePost.HTMLString", err)
|
|
return
|
|
}
|
|
ctx.RenderWithErr(flashError, tplEditFile, &form)
|
|
}
|
|
} else {
|
|
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
|
|
"Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
|
|
"Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
|
|
"Details": utils.SanitizeFlashErrorString(err.Error()),
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("editFilePost.HTMLString", err)
|
|
return
|
|
}
|
|
ctx.RenderWithErr(flashError, tplEditFile, &form)
|
|
}
|
|
}
|
|
|
|
if ctx.Repo.Repository.IsEmpty {
|
|
if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
|
|
_ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
|
|
}
|
|
}
|
|
|
|
redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
|
|
}
|
|
|
|
// EditFilePost response for editing file
|
|
func EditFilePost(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.EditRepoFileForm)
|
|
editFilePost(ctx, *form, false)
|
|
}
|
|
|
|
// NewFilePost response for creating file
|
|
func NewFilePost(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.EditRepoFileForm)
|
|
editFilePost(ctx, *form, true)
|
|
}
|
|
|
|
// DiffPreviewPost render preview diff page
|
|
func DiffPreviewPost(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.EditPreviewDiffForm)
|
|
treePath := cleanUploadFileName(ctx.Repo.TreePath)
|
|
if len(treePath) == 0 {
|
|
ctx.Error(http.StatusInternalServerError, "file name to diff is invalid")
|
|
return
|
|
}
|
|
|
|
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error())
|
|
return
|
|
} else if entry.IsDir() {
|
|
ctx.Error(http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
|
|
diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "GetDiffPreview: "+err.Error())
|
|
return
|
|
}
|
|
|
|
if diff.NumFiles == 0 {
|
|
ctx.PlainText(http.StatusOK, ctx.Tr("repo.editor.no_changes_to_show"))
|
|
return
|
|
}
|
|
ctx.Data["File"] = diff.Files[0]
|
|
|
|
ctx.HTML(http.StatusOK, tplEditDiffPreview)
|
|
}
|
|
|
|
// DeleteFile render delete file page
|
|
func DeleteFile(ctx *context.Context) {
|
|
ctx.Data["PageIsDelete"] = true
|
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
|
treePath := cleanUploadFileName(ctx.Repo.TreePath)
|
|
|
|
if treePath != ctx.Repo.TreePath {
|
|
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
|
|
return
|
|
}
|
|
|
|
ctx.Data["TreePath"] = treePath
|
|
canCommit := renderCommitRights(ctx)
|
|
|
|
ctx.Data["commit_summary"] = ""
|
|
ctx.Data["commit_message"] = ""
|
|
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
|
if canCommit {
|
|
ctx.Data["commit_choice"] = frmCommitChoiceDirect
|
|
} else {
|
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
|
}
|
|
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
|
|
|
|
ctx.HTML(http.StatusOK, tplDeleteFile)
|
|
}
|
|
|
|
// DeleteFilePost response for deleting file
|
|
func DeleteFilePost(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.DeleteRepoFileForm)
|
|
canCommit := renderCommitRights(ctx)
|
|
branchName := ctx.Repo.BranchName
|
|
if form.CommitChoice == frmCommitChoiceNewBranch {
|
|
branchName = form.NewBranchName
|
|
}
|
|
|
|
ctx.Data["PageIsDelete"] = true
|
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
|
ctx.Data["TreePath"] = ctx.Repo.TreePath
|
|
ctx.Data["commit_summary"] = form.CommitSummary
|
|
ctx.Data["commit_message"] = form.CommitMessage
|
|
ctx.Data["commit_choice"] = form.CommitChoice
|
|
ctx.Data["new_branch_name"] = form.NewBranchName
|
|
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(http.StatusOK, tplDeleteFile)
|
|
return
|
|
}
|
|
|
|
if branchName == ctx.Repo.BranchName && !canCommit {
|
|
ctx.Data["Err_NewBranchName"] = true
|
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
|
|
return
|
|
}
|
|
|
|
message := strings.TrimSpace(form.CommitSummary)
|
|
if len(message) == 0 {
|
|
message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
|
|
}
|
|
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
|
|
if len(form.CommitMessage) > 0 {
|
|
message += "\n\n" + form.CommitMessage
|
|
}
|
|
|
|
if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
|
LastCommitID: form.LastCommit,
|
|
OldBranch: ctx.Repo.BranchName,
|
|
NewBranch: branchName,
|
|
Files: []*files_service.ChangeRepoFile{
|
|
{
|
|
Operation: "delete",
|
|
TreePath: ctx.Repo.TreePath,
|
|
},
|
|
},
|
|
Message: message,
|
|
Signoff: form.Signoff,
|
|
}); err != nil {
|
|
// This is where we handle all the errors thrown by repofiles.DeleteRepoFile
|
|
if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form)
|
|
} else if models.IsErrFilenameInvalid(err) {
|
|
ctx.Data["Err_TreePath"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form)
|
|
} else if models.IsErrFilePathInvalid(err) {
|
|
ctx.Data["Err_TreePath"] = true
|
|
if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
|
|
switch fileErr.Type {
|
|
case git.EntryModeSymlink:
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form)
|
|
case git.EntryModeTree:
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form)
|
|
case git.EntryModeBlob:
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form)
|
|
default:
|
|
ctx.ServerError("DeleteRepoFile", err)
|
|
}
|
|
} else {
|
|
ctx.ServerError("DeleteRepoFile", err)
|
|
}
|
|
} else if git.IsErrBranchNotExist(err) {
|
|
// For when a user deletes a file to a branch that no longer exists
|
|
if branchErr, ok := err.(git.ErrBranchNotExist); ok {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
|
}
|
|
} else if git_model.IsErrBranchAlreadyExists(err) {
|
|
// For when a user specifies a new branch that already exists
|
|
if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
|
}
|
|
} else if models.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form)
|
|
} else if git.IsErrPushRejected(err) {
|
|
errPushRej := err.(*git.ErrPushRejected)
|
|
if len(errPushRej.Message) == 0 {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
|
|
} else {
|
|
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
|
|
"Message": ctx.Tr("repo.editor.push_rejected"),
|
|
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
|
|
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("DeleteFilePost.HTMLString", err)
|
|
return
|
|
}
|
|
ctx.RenderWithErr(flashError, tplDeleteFile, &form)
|
|
}
|
|
} else {
|
|
ctx.ServerError("DeleteRepoFile", err)
|
|
}
|
|
}
|
|
|
|
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
|
|
treePath := path.Dir(ctx.Repo.TreePath)
|
|
if treePath == "." {
|
|
treePath = "" // the file deleted was in the root, so we return the user to the root directory
|
|
}
|
|
if len(treePath) > 0 {
|
|
// Need to get the latest commit since it changed
|
|
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
|
|
if err == nil && commit != nil {
|
|
// We have the comment, now find what directory we can return the user to
|
|
// (must have entries)
|
|
treePath = GetClosestParentWithFiles(treePath, commit)
|
|
} else {
|
|
treePath = "" // otherwise return them to the root of the repo
|
|
}
|
|
}
|
|
|
|
redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath)
|
|
}
|
|
|
|
// UploadFile render upload file page
|
|
func UploadFile(ctx *context.Context) {
|
|
ctx.Data["PageIsUpload"] = true
|
|
upload.AddUploadContext(ctx, "repo")
|
|
canCommit := renderCommitRights(ctx)
|
|
treePath := cleanUploadFileName(ctx.Repo.TreePath)
|
|
if treePath != ctx.Repo.TreePath {
|
|
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
|
|
return
|
|
}
|
|
ctx.Repo.TreePath = treePath
|
|
|
|
treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
|
|
if len(treeNames) == 0 {
|
|
// We must at least have one element for user to input.
|
|
treeNames = []string{""}
|
|
}
|
|
|
|
ctx.Data["TreeNames"] = treeNames
|
|
ctx.Data["TreePaths"] = treePaths
|
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
|
ctx.Data["commit_summary"] = ""
|
|
ctx.Data["commit_message"] = ""
|
|
if canCommit {
|
|
ctx.Data["commit_choice"] = frmCommitChoiceDirect
|
|
} else {
|
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
|
}
|
|
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
|
|
|
|
ctx.HTML(http.StatusOK, tplUploadFile)
|
|
}
|
|
|
|
// UploadFilePost response for uploading file
|
|
func UploadFilePost(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.UploadRepoFileForm)
|
|
ctx.Data["PageIsUpload"] = true
|
|
upload.AddUploadContext(ctx, "repo")
|
|
canCommit := renderCommitRights(ctx)
|
|
|
|
oldBranchName := ctx.Repo.BranchName
|
|
branchName := oldBranchName
|
|
|
|
if form.CommitChoice == frmCommitChoiceNewBranch {
|
|
branchName = form.NewBranchName
|
|
}
|
|
|
|
form.TreePath = cleanUploadFileName(form.TreePath)
|
|
|
|
treeNames, treePaths := getParentTreeFields(form.TreePath)
|
|
if len(treeNames) == 0 {
|
|
// We must at least have one element for user to input.
|
|
treeNames = []string{""}
|
|
}
|
|
|
|
ctx.Data["TreePath"] = form.TreePath
|
|
ctx.Data["TreeNames"] = treeNames
|
|
ctx.Data["TreePaths"] = treePaths
|
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)
|
|
ctx.Data["commit_summary"] = form.CommitSummary
|
|
ctx.Data["commit_message"] = form.CommitMessage
|
|
ctx.Data["commit_choice"] = form.CommitChoice
|
|
ctx.Data["new_branch_name"] = branchName
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(http.StatusOK, tplUploadFile)
|
|
return
|
|
}
|
|
|
|
if oldBranchName != branchName {
|
|
if _, err := ctx.Repo.GitRepo.GetBranch(branchName); err == nil {
|
|
ctx.Data["Err_NewBranchName"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form)
|
|
return
|
|
}
|
|
} else if !canCommit {
|
|
ctx.Data["Err_NewBranchName"] = true
|
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form)
|
|
return
|
|
}
|
|
|
|
if !ctx.Repo.Repository.IsEmpty {
|
|
var newTreePath string
|
|
for _, part := range treeNames {
|
|
newTreePath = path.Join(newTreePath, part)
|
|
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
|
|
if err != nil {
|
|
if git.IsErrNotExist(err) {
|
|
break // Means there is no item with that name, so we're good
|
|
}
|
|
ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
|
|
return
|
|
}
|
|
|
|
// User can only upload files to a directory, the directory name shouldn't be an existing file.
|
|
if !entry.IsDir() {
|
|
ctx.Data["Err_TreePath"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
message := strings.TrimSpace(form.CommitSummary)
|
|
if len(message) == 0 {
|
|
dir := form.TreePath
|
|
if dir == "" {
|
|
dir = "/"
|
|
}
|
|
message = ctx.Tr("repo.editor.upload_files_to_dir", dir)
|
|
}
|
|
|
|
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
|
|
if len(form.CommitMessage) > 0 {
|
|
message += "\n\n" + form.CommitMessage
|
|
}
|
|
|
|
if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
|
|
LastCommitID: ctx.Repo.CommitID,
|
|
OldBranch: oldBranchName,
|
|
NewBranch: branchName,
|
|
TreePath: form.TreePath,
|
|
Message: message,
|
|
Files: form.Files,
|
|
Signoff: form.Signoff,
|
|
}); err != nil {
|
|
if git_model.IsErrLFSFileLocked(err) {
|
|
ctx.Data["Err_TreePath"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form)
|
|
} else if models.IsErrFilenameInvalid(err) {
|
|
ctx.Data["Err_TreePath"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form)
|
|
} else if models.IsErrFilePathInvalid(err) {
|
|
ctx.Data["Err_TreePath"] = true
|
|
fileErr := err.(models.ErrFilePathInvalid)
|
|
switch fileErr.Type {
|
|
case git.EntryModeSymlink:
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form)
|
|
case git.EntryModeTree:
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form)
|
|
case git.EntryModeBlob:
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form)
|
|
default:
|
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
|
}
|
|
} else if models.IsErrRepoFileAlreadyExists(err) {
|
|
ctx.Data["Err_TreePath"] = true
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form)
|
|
} else if git.IsErrBranchNotExist(err) {
|
|
branchErr := err.(git.ErrBranchNotExist)
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form)
|
|
} else if git_model.IsErrBranchAlreadyExists(err) {
|
|
// For when a user specifies a new branch that already exists
|
|
ctx.Data["Err_NewBranchName"] = true
|
|
branchErr := err.(git_model.ErrBranchAlreadyExists)
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form)
|
|
} else if git.IsErrPushOutOfDate(err) {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form)
|
|
} else if git.IsErrPushRejected(err) {
|
|
errPushRej := err.(*git.ErrPushRejected)
|
|
if len(errPushRej.Message) == 0 {
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
|
|
} else {
|
|
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
|
|
"Message": ctx.Tr("repo.editor.push_rejected"),
|
|
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
|
|
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("UploadFilePost.HTMLString", err)
|
|
return
|
|
}
|
|
ctx.RenderWithErr(flashError, tplUploadFile, &form)
|
|
}
|
|
} else {
|
|
// os.ErrNotExist - upload file missing in the intervening time?!
|
|
log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err)
|
|
ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form)
|
|
}
|
|
return
|
|
}
|
|
|
|
if ctx.Repo.Repository.IsEmpty {
|
|
if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
|
|
_ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
|
|
}
|
|
}
|
|
|
|
redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
|
|
}
|
|
|
|
func cleanUploadFileName(name string) string {
|
|
// Rebase the filename
|
|
name = util.PathJoinRel(name)
|
|
// Git disallows any filenames to have a .git directory in them.
|
|
for _, part := range strings.Split(name, "/") {
|
|
if strings.ToLower(part) == ".git" {
|
|
return ""
|
|
}
|
|
}
|
|
return name
|
|
}
|
|
|
|
// UploadFileToServer upload file to server file dir not git
|
|
func UploadFileToServer(ctx *context.Context) {
|
|
file, header, err := ctx.Req.FormFile("file")
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
buf := make([]byte, 1024)
|
|
n, _ := util.ReadAtMost(file, buf)
|
|
if n > 0 {
|
|
buf = buf[:n]
|
|
}
|
|
|
|
err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
|
|
if err != nil {
|
|
ctx.Error(http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
name := cleanUploadFileName(header.Filename)
|
|
if len(name) == 0 {
|
|
ctx.Error(http.StatusInternalServerError, "Upload file name is invalid")
|
|
return
|
|
}
|
|
|
|
upload, err := repo_model.NewUpload(ctx, name, buf, file)
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err))
|
|
return
|
|
}
|
|
|
|
log.Trace("New file uploaded: %s", upload.UUID)
|
|
ctx.JSON(http.StatusOK, map[string]string{
|
|
"uuid": upload.UUID,
|
|
})
|
|
}
|
|
|
|
// RemoveUploadFileFromServer remove file from server file dir
|
|
func RemoveUploadFileFromServer(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.RemoveUploadFileForm)
|
|
if len(form.File) == 0 {
|
|
ctx.Status(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil {
|
|
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err))
|
|
return
|
|
}
|
|
|
|
log.Trace("Upload file removed: %s", form.File)
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// GetUniquePatchBranchName Gets a unique branch name for a new patch branch
|
|
// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
|
|
// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
|
|
// type in the branch name themselves (will be an empty field)
|
|
func GetUniquePatchBranchName(ctx *context.Context) string {
|
|
prefix := ctx.Doer.LowerName + "-patch-"
|
|
for i := 1; i <= 1000; i++ {
|
|
branchName := fmt.Sprintf("%s%d", prefix, i)
|
|
if _, err := ctx.Repo.GitRepo.GetBranch(branchName); err != nil {
|
|
if git.IsErrBranchNotExist(err) {
|
|
return branchName
|
|
}
|
|
log.Error("GetUniquePatchBranchName: %v", err)
|
|
return ""
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is
|
|
// deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a
|
|
// SubTree or it has no entries, we go up one dir and see if we can return the user to that listing.
|
|
func GetClosestParentWithFiles(treePath string, commit *git.Commit) string {
|
|
if len(treePath) == 0 || treePath == "." {
|
|
return ""
|
|
}
|
|
// see if the tree has entries
|
|
if tree, err := commit.SubTree(treePath); err != nil {
|
|
// failed to get tree, going up a dir
|
|
return GetClosestParentWithFiles(path.Dir(treePath), commit)
|
|
} else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
|
|
// no files in this dir, going up a dir
|
|
return GetClosestParentWithFiles(path.Dir(treePath), commit)
|
|
}
|
|
return treePath
|
|
}
|