Skip to content

WIP: Rename/move binary files in Web UI #34350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1330,7 +1330,9 @@ editor.upload_file = Upload File
editor.edit_file = Edit File
editor.preview_changes = Preview Changes
editor.cannot_edit_lfs_files = LFS files cannot be edited in the web interface.
editor.cannot_edit_too_large_file = The file is too large to be edited.
editor.cannot_edit_non_text_files = Binary files cannot be edited in the web interface.
editor.file_not_editable_hint = But you can still rename or move it.
editor.edit_this_file = Edit File
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.
Expand Down
53 changes: 38 additions & 15 deletions routers/web/repo/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,6 @@ func editFile(ctx *context.Context, isNewFile bool) {
}

blob := entry.Blob()
if blob.Size() >= setting.UI.MaxDisplayFileSize {
ctx.NotFound(err)
return
}

buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
if err != nil {
Expand All @@ -162,26 +158,47 @@ func editFile(ctx *context.Context, isNewFile bool) {

defer dataRc.Close()

if fInfo.isLFSFile {
lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("GetTreePathLock", err)
return
}
if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
ctx.NotFound(nil)
return
}
}

ctx.Data["FileSize"] = blob.Size()

// Only some file types are editable online as text.
if !fInfo.st.IsRepresentableAsText() || fInfo.isLFSFile {
ctx.NotFound(nil)
return
}
ctx.Data["IsFileEditable"] = fInfo.st.IsRepresentableAsText() && !fInfo.isLFSFile && blob.Size() < setting.UI.MaxDisplayFileSize

d, _ := io.ReadAll(dataRc)
if fInfo.isLFSFile {
ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
} else if !fInfo.isTextFile {
ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
}

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)
if blob.Size() >= setting.UI.MaxDisplayFileSize {
ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file")
} else {
ctx.Data["FileContent"] = content
d, _ := io.ReadAll(dataRc)

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 username the new file.
treeNames = append(treeNames, fileName)

ctx.Data["IsFileEditable"] = true
}

ctx.Data["TreeNames"] = treeNames
Expand Down Expand Up @@ -282,6 +299,12 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
operation = "create"
}

var contentReader io.ReadSeeker
// form content only has data if file is representable as text, is not too large and not in lfs
if isNewFile || form.Content.Has() {
contentReader = strings.NewReader(strings.ReplaceAll(form.Content.Value(), "\r", ""))
}

if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
LastCommitID: form.LastCommit,
OldBranch: ctx.Repo.BranchName,
Expand All @@ -292,7 +315,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
Operation: operation,
FromTreePath: ctx.Repo.TreePath,
TreePath: form.TreePath,
ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
ContentReader: contentReader,
},
},
Signoff: form.Signoff,
Expand Down
2 changes: 1 addition & 1 deletion routers/web/repo/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func NewDiffPatchPost(ctx *context.Context) {
OldBranch: ctx.Repo.BranchName,
NewBranch: branchName,
Message: message,
Content: strings.ReplaceAll(form.Content, "\r", ""),
Content: strings.ReplaceAll(form.Content.Value(), "\r", ""),
Author: gitCommitter,
Committer: gitCommitter,
})
Expand Down
28 changes: 6 additions & 22 deletions routers/web/repo/view_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,6 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
}

// Assume file is not editable first.
if fInfo.isLFSFile {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
} else if !isRepresentableAsText {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
}

// read all needed attributes which will be used later
// there should be no performance different between reading 2 or 4 here
attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{
Expand Down Expand Up @@ -243,21 +236,6 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
ctx.Data["FileContent"] = fileContent
ctx.Data["LineEscapeStatus"] = statuses
}
if !fInfo.isLFSFile {
if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
ctx.Data["CanEditFile"] = false
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
} else {
ctx.Data["CanEditFile"] = true
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
}
} else if !ctx.Repo.RefFullName.IsBranch() {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
} else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
}
}

case fInfo.st.IsPDF():
ctx.Data["IsPDFFile"] = true
Expand Down Expand Up @@ -309,15 +287,21 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {

if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
ctx.Data["CanEditFile"] = false
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
ctx.Data["CanDeleteFile"] = false
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
} else {
ctx.Data["CanEditFile"] = true
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
ctx.Data["CanDeleteFile"] = true
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file")
}
} else if !ctx.Repo.RefFullName.IsBranch() {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
} else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
}
}
3 changes: 2 additions & 1 deletion services/forms/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/context"
Expand Down Expand Up @@ -689,7 +690,7 @@ func (f *NewWikiForm) Validate(req *http.Request, errs binding.Errors) binding.E
// EditRepoFileForm form for changing repository file
type EditRepoFileForm struct {
TreePath string `binding:"Required;MaxSize(500)"`
Content string
Content optional.Option[string]
CommitSummary string `binding:"MaxSize(100)"`
CommitMessage string
CommitChoice string `binding:"Required;MaxSize(50)"`
Expand Down
67 changes: 56 additions & 11 deletions services/repository/files/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type ChangeRepoFile struct {
Operation string
TreePath string
FromTreePath string
ContentReader io.ReadSeeker
ContentReader io.ReadSeeker // nil if the operation is a pure rename
SHA string
Options *RepoFileOptions
}
Expand Down Expand Up @@ -488,26 +488,62 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file
}
}

treeObjectContentReader := file.ContentReader
var treeObjectContentReader io.Reader = file.ContentReader
var oldEntry *git.TreeEntry
// If no new content is committed, use the file from the last commit as content
if file.ContentReader == nil {
lastCommit, err := t.GetLastCommit(ctx)
if err != nil {
return err
}
commit, err := t.GetCommit(lastCommit)
if err != nil {
return err
}

if oldEntry, err = commit.GetTreeEntryByPath(file.Options.fromTreePath); err != nil {
return err
}
if treeObjectContentReader, err = oldEntry.Blob().DataAsync(); err != nil {
return err
}
defer treeObjectContentReader.(io.ReadCloser).Close()
}

var lfsMetaObject *git_model.LFSMetaObject
if setting.LFS.StartServer && hasOldBranch {
// Check there is no way this can return multiple infos
attributesMap, err := attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{
Attributes: []string{attribute.Filter},
Filenames: []string{file.Options.treePath},
Filenames: []string{file.Options.treePath, file.Options.fromTreePath},
})
if err != nil {
return err
}

if attributesMap[file.Options.treePath] != nil && attributesMap[file.Options.treePath].Get(attribute.Filter).ToString().Value() == "lfs" {
// OK so we are supposed to LFS this data!
pointer, err := lfs.GeneratePointer(treeObjectContentReader)
if err != nil {
var pointer lfs.Pointer
// Get existing lfs pointer if the old path is in lfs
if oldEntry != nil && attributesMap[file.Options.fromTreePath] != nil && attributesMap[file.Options.fromTreePath].Get(attribute.Filter).ToString().Value() == "lfs" {
if pointer, err = lfs.ReadPointer(treeObjectContentReader); err != nil {
return err
}
}

if attributesMap[file.Options.treePath] != nil && attributesMap[file.Options.treePath].Get(attribute.Filter).ToString().Value() == "lfs" {
// Only generate a new lfs pointer if the old path isn't in lfs
if !pointer.IsValid() {
if pointer, err = lfs.GeneratePointer(treeObjectContentReader); err != nil {
return err
}
}

lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repoID}
treeObjectContentReader = strings.NewReader(pointer.StringContent())
} else if pointer.IsValid() { // old path was in lfs, new is not
treeObjectContentReader, err = lfs.ReadMetaObject(pointer)
if err != nil {
return err
}
}
}

Expand Down Expand Up @@ -539,11 +575,20 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file
return err
}
if !exist {
_, err := file.ContentReader.Seek(0, io.SeekStart)
if err != nil {
return err
var lfsContentReader io.Reader
if file.ContentReader != nil {
if _, err := file.ContentReader.Seek(0, io.SeekStart); err != nil {
return err
}
lfsContentReader = file.ContentReader
} else {
if lfsContentReader, err = oldEntry.Blob().DataAsync(); err != nil {
return err
}
defer lfsContentReader.(io.ReadCloser).Close()
}
if err := contentStore.Put(lfsMetaObject.Pointer, file.ContentReader); err != nil {

if err := contentStore.Put(lfsMetaObject.Pointer, lfsContentReader); err != nil {
if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil {
return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err)
}
Expand Down
51 changes: 30 additions & 21 deletions templates/repo/editor/edit.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,40 @@
<input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required>
</div>
</div>
<div class="field">
<div class="ui top attached header">
<div class="ui compact small menu small-menu-items repo-editor-menu">
<a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.new_file"}}{{else}}{{ctx.Locale.Tr "repo.editor.edit_file"}}{{end}}</a>
<a class="item" data-tab="preview" data-preview-url="{{.Repository.Link}}/markup" data-preview-context-ref="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}">{{svg "octicon-eye"}} {{ctx.Locale.Tr "preview"}}</a>
{{if not .IsNewFile}}
<a class="item" data-tab="diff" hx-params="context,content" hx-vals='{"context":"{{.BranchLink}}"}' hx-include="#edit_area" hx-swap="innerHTML" hx-target=".tab[data-tab='diff']" hx-indicator=".tab[data-tab='diff']" hx-post="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a>
{{end}}
</div>
</div>
<div class="ui bottom attached segment tw-p-0">
<div class="ui active tab tw-rounded-b" data-tab="write">
<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}"
data-previewable-extensions="{{.PreviewableExtensions}}"
data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea>
<div class="editor-loading is-loading"></div>
{{if .IsFileEditable}}
<div class="field">
<div class="ui top attached header">
<div class="ui compact small menu small-menu-items repo-editor-menu">
<a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.new_file"}}{{else}}{{ctx.Locale.Tr "repo.editor.edit_file"}}{{end}}</a>
<a class="item" data-tab="preview" data-preview-url="{{.Repository.Link}}/markup" data-preview-context-ref="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}">{{svg "octicon-eye"}} {{ctx.Locale.Tr "preview"}}</a>
{{if not .IsNewFile}}
<a class="item" data-tab="diff" hx-params="context,content" hx-vals='{"context":"{{.BranchLink}}"}' hx-include="#edit_area" hx-swap="innerHTML" hx-target=".tab[data-tab='diff']" hx-indicator=".tab[data-tab='diff']" hx-post="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a>
{{end}}
</div>
</div>
<div class="ui tab tw-px-4 tw-py-3" data-tab="preview">
{{ctx.Locale.Tr "loading"}}
<div class="ui bottom attached segment tw-p-0">
<div class="ui active tab tw-rounded-b" data-tab="write">
<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}"
data-previewable-extensions="{{.PreviewableExtensions}}"
data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea>
<div class="editor-loading is-loading"></div>
</div>
<div class="ui tab tw-px-4 tw-py-3" data-tab="preview">
{{ctx.Locale.Tr "loading"}}
</div>
<div class="ui tab" data-tab="diff">
<div class="tw-p-16"></div>
</div>
</div>
<div class="ui tab" data-tab="diff">
<div class="tw-p-16"></div>
</div>
{{else}}
<div class="field">
<div class="ui segment tw-text-center">
<h4 class="tw-font-semibold tw-mb-2">{{.NotEditableReason}}</h4>
<p>{{ctx.Locale.Tr "repo.editor.file_not_editable_hint"}}</p>
</div>
</div>
</div>
{{end}}
{{template "repo/editor/commit_form" .}}
</form>
</div>
Expand Down