Skip to content

Commit 17858b6

Browse files
committed
refactor(Repo): extract directory deletion logic into a dedicated service with testing
1 parent 442cf3b commit 17858b6

File tree

7 files changed

+184
-82
lines changed

7 files changed

+184
-82
lines changed

routers/web/repo/editor.go

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,10 @@ func DeleteFilePost(ctx *context.Context) {
394394
}
395395

396396
treePath := ctx.Repo.TreePath
397+
if treePath == "" {
398+
ctx.JSONError(ctx.Tr("repo.editor.cannot_delete_root"))
399+
return
400+
}
397401

398402
// Check if the path is a directory
399403
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
@@ -402,50 +406,18 @@ func DeleteFilePost(ctx *context.Context) {
402406
return
403407
}
404408

405-
var filesToDelete []*files_service.ChangeRepoFile
406409
var commitMessage string
407-
408410
if entry.IsDir() {
409-
// Get all files in the directory recursively
410-
tree, err := ctx.Repo.Commit.SubTree(treePath)
411-
if err != nil {
412-
ctx.ServerError("SubTree", err)
413-
return
414-
}
415-
416-
entries, err := tree.ListEntriesRecursiveFast()
417-
if err != nil {
418-
ctx.ServerError("ListEntriesRecursiveFast", err)
419-
return
420-
}
421-
422-
// Create delete operations for all files in the directory
423-
for _, e := range entries {
424-
if !e.IsDir() && !e.IsSubModule() {
425-
filesToDelete = append(filesToDelete, &files_service.ChangeRepoFile{
426-
Operation: "delete",
427-
TreePath: treePath + "/" + e.Name(),
428-
})
429-
}
430-
}
431-
432411
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete_directory", treePath))
433412
} else {
434-
// Single file deletion
435-
filesToDelete = []*files_service.ChangeRepoFile{
436-
{
437-
Operation: "delete",
438-
TreePath: treePath,
439-
},
440-
}
441413
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath))
442414
}
443415

444-
_, err = files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
416+
_, err = files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.DeleteRepoFileOptions{
445417
LastCommitID: parsed.form.LastCommit,
446418
OldBranch: parsed.OldBranchName,
447419
NewBranch: parsed.NewBranchName,
448-
Files: filesToDelete,
420+
TreePath: treePath,
449421
Message: commitMessage,
450422
Signoff: parsed.form.Signoff,
451423
Author: parsed.GitCommitter,
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package files
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
repo_model "code.gitea.io/gitea/models/repo"
8+
user_model "code.gitea.io/gitea/models/user"
9+
"code.gitea.io/gitea/modules/gitrepo"
10+
"code.gitea.io/gitea/modules/structs"
11+
)
12+
13+
type DeleteRepoFileOptions struct {
14+
LastCommitID string
15+
OldBranch string
16+
NewBranch string
17+
TreePath string
18+
Message string
19+
Signoff bool
20+
Author *IdentityOptions
21+
Committer *IdentityOptions
22+
}
23+
24+
// DeleteRepoFile deletes a file or directory in the given repository
25+
func DeleteRepoFile(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *DeleteRepoFileOptions) (*structs.FilesResponse, error) {
26+
if opts.TreePath == "" {
27+
return nil, fmt.Errorf("path cannot be empty")
28+
}
29+
30+
// If no branch name is set, assume the default branch
31+
if opts.OldBranch == "" {
32+
opts.OldBranch = repo.DefaultBranch
33+
}
34+
if opts.NewBranch == "" {
35+
opts.NewBranch = opts.OldBranch
36+
}
37+
38+
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
39+
if err != nil {
40+
return nil, err
41+
}
42+
defer closer.Close()
43+
44+
// Get commit
45+
commit, err := gitRepo.GetBranchCommit(opts.OldBranch)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
// Get entry
51+
entry, err := commit.GetTreeEntryByPath(opts.TreePath)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
var filesToDelete []*ChangeRepoFile
57+
58+
if entry.IsDir() {
59+
tree, err := commit.SubTree(opts.TreePath)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
entries, err := tree.ListEntriesRecursiveFast()
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
for _, e := range entries {
70+
if !e.IsDir() && !e.IsSubModule() {
71+
filesToDelete = append(filesToDelete, &ChangeRepoFile{
72+
Operation: "delete",
73+
TreePath: opts.TreePath + "/" + e.Name(),
74+
})
75+
}
76+
}
77+
} else {
78+
filesToDelete = append(filesToDelete, &ChangeRepoFile{
79+
Operation: "delete",
80+
TreePath: opts.TreePath,
81+
})
82+
}
83+
84+
return ChangeRepoFiles(ctx, repo, doer, &ChangeRepoFilesOptions{
85+
LastCommitID: opts.LastCommitID,
86+
OldBranch: opts.OldBranch,
87+
NewBranch: opts.NewBranch,
88+
Message: opts.Message,
89+
Files: filesToDelete,
90+
Signoff: opts.Signoff,
91+
Author: opts.Author,
92+
Committer: opts.Committer,
93+
})
94+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package files
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
repo_model "code.gitea.io/gitea/models/repo"
9+
"code.gitea.io/gitea/models/unittest"
10+
"code.gitea.io/gitea/services/contexttest"
11+
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestDeleteRepoFile(t *testing.T) {
16+
unittest.PrepareTestEnv(t)
17+
ctx, _ := contexttest.MockContext(t, "user2/repo1")
18+
contexttest.LoadRepo(t, ctx, 1)
19+
contexttest.LoadRepoCommit(t, ctx)
20+
contexttest.LoadUser(t, ctx, 2)
21+
contexttest.LoadGitRepo(t, ctx)
22+
defer ctx.Repo.GitRepo.Close()
23+
24+
// Remove hooks to avoid "gitea: no such file or directory" error
25+
repoPath := repo_model.RepoPath(ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name)
26+
assert.NoError(t, os.RemoveAll(filepath.Join(repoPath, "hooks")))
27+
28+
t.Run("DeleteRoot", func(t *testing.T) {
29+
_, err := DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, &DeleteRepoFileOptions{
30+
TreePath: "",
31+
OldBranch: "master",
32+
NewBranch: "master",
33+
})
34+
assert.Error(t, err)
35+
assert.Contains(t, err.Error(), "path cannot be empty")
36+
})
37+
38+
t.Run("DeleteFile", func(t *testing.T) {
39+
// README.md exists in repo1
40+
_, err := DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, &DeleteRepoFileOptions{
41+
TreePath: "README.md",
42+
OldBranch: "master",
43+
NewBranch: "master",
44+
Message: "Delete README.md",
45+
})
46+
assert.NoError(t, err)
47+
})
48+
}

templates/repo/editor/patch.tmpl

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,35 @@
88
{{template "repo/view_file_tree" .}}
99
</div>
1010
<div class="repo-view-content">
11-
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"
12-
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
13-
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
14-
>
15-
{{.CsrfTokenHtml}}
16-
{{template "repo/editor/common_top" .}}
17-
<div class="repo-editor-header tw-flex tw-items-center tw-gap-2">
18-
<button type="button" class="repo-view-file-tree-toggle-show ui compact basic button icon not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}"
19-
data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show"
20-
data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
21-
{{svg "octicon-sidebar-collapse"}}
22-
</button>
23-
<div class="breadcrumb">
24-
{{ctx.Locale.Tr "repo.editor.patching"}}
25-
<a class="section" href="{{$.BranchLink}}">{{.BranchName}}</a>
26-
<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
27-
<input type="hidden" name="tree_path" value="__dummy_for_EditRepoFileForm.TreePath(Required)__">
28-
<input id="file-name" type="hidden" value="diff.patch">
29-
</div>
30-
</div>
31-
<div class="field">
32-
<div class="ui compact small menu small-menu-items repo-editor-menu">
33-
<a class="active item" data-tab="write">{{svg "octicon-code" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.editor.new_patch"}}</a>
34-
</div>
35-
<div class="ui active tab segment tw-rounded tw-p-0" data-tab="write">
36-
<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-patch"
37-
data-context="{{.RepoLink}}"
38-
data-line-wrap-extensions="{{.LineWrapExtensions}}">
39-
{{.FileContent}}</textarea>
40-
<div class="editor-loading is-loading"></div>
41-
</div>
42-
</div>
43-
{{template "repo/editor/commit_form" .}}
11+
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}" data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}" data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}">
12+
{{.CsrfTokenHtml}}
13+
{{template "repo/editor/common_top" .}}
14+
<div class="repo-editor-header tw-flex tw-items-center tw-gap-2">
15+
<button type="button" class="repo-view-file-tree-toggle-show ui compact basic button icon not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}"
16+
data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show"
17+
data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
18+
{{svg "octicon-sidebar-collapse"}}
19+
</button>
20+
<div class="breadcrumb">
21+
{{ctx.Locale.Tr "repo.editor.patching"}}
22+
<a class="section" href="{{$.BranchLink}}">{{.BranchName}}</a>
23+
<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
24+
<input type="hidden" name="tree_path" value="__dummy_for_EditRepoFileForm.TreePath(Required)__">
25+
<input id="file-name" type="hidden" value="diff.patch">
26+
</div>
27+
</div>
28+
<div class="field">
29+
<div class="ui compact small menu small-menu-items repo-editor-menu">
30+
<a class="active item" data-tab="write">{{svg "octicon-code" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.editor.new_patch"}}</a>
31+
</div>
32+
<div class="ui active tab segment tw-rounded tw-p-0" data-tab="write">
33+
<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-patch"
34+
data-context="{{.RepoLink}}"
35+
data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea>
36+
<div class="editor-loading is-loading"></div>
37+
</div>
38+
</div>
39+
{{template "repo/editor/commit_form" .}}
4440
</form>
4541
</div>
4642
</div>

templates/repo/view_content.tmpl

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,7 @@
9696
<button class="ui dropdown basic compact jump button icon repo-file-actions-dropdown" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
9797
{{svg "octicon-kebab-horizontal"}}
9898
<div class="menu">
99-
<a class="item" data-clipboard-text="{{.TreePath}}">
100-
{{svg "octicon-copy" 16 "tw-mr-2"}}{{ctx.Locale.Tr "copy_path"}}
101-
</a>
102-
<a class="item" data-clipboard-text="{{AppUrl}}{{StringUtils.TrimPrefix .Repository.Link "/"}}/src/commit/{{.CommitID}}/{{PathEscapeSegments .TreePath}}">
99+
<a class="item" data-clipboard-text="{{.Repository.Link}}/src/commit/{{.CommitID}}/{{PathEscapeSegments .TreePath}}" data-clipboard-text-type="url">
103100
{{svg "octicon-link" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_copy_permalink"}}
104101
</a>
105102
{{if and (.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) (not .Repository.IsArchived) (not $isTreePathRoot)}}

web_src/css/repo/file-actions.css

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1-
/* Repository file actions dropdown and centered content */
2-
.ui.dropdown.repo-add-file > .menu {
1+
/* Repository file actions dropdown */
2+
.ui.dropdown.repo-add-file > .menu,
3+
.ui.dropdown.repo-file-actions-dropdown > .menu {
34
margin-top: 4px !important;
45
}
56

67
.ui.dropdown.repo-file-actions-dropdown > .menu {
7-
margin-top: 4px !important;
88
min-width: 200px;
99
}
1010

11-
.repo-file-actions-dropdown .menu .item {
12-
cursor: pointer;
13-
}
14-
15-
.repo-file-actions-dropdown .menu .divider {
11+
.ui.dropdown.repo-file-actions-dropdown > .menu > .divider {
1612
margin: 0.5rem 0;
1713
}
1814

web_src/js/components/ViewFileTree.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,9 @@ onMounted(async () => {
105105
if (searchInputElement) {
106106
searchInputElement.addEventListener('input', handleSearchInput);
107107
searchInputElement.addEventListener('keydown', handleKeyDown);
108+
document.addEventListener('click', handleClickOutside);
108109
}
109110
110-
document.addEventListener('click', handleClickOutside);
111-
112111
window.addEventListener('popstate', (e) => {
113112
store.selectedItem = e.state?.treePath || '';
114113
if (e.state?.url) store.loadViewContent(e.state.url);
@@ -132,8 +131,8 @@ onUnmounted(() => {
132131
if (searchInputElement) {
133132
searchInputElement.removeEventListener('input', handleSearchInput);
134133
searchInputElement.removeEventListener('keydown', handleKeyDown);
134+
document.removeEventListener('click', handleClickOutside);
135135
}
136-
document.removeEventListener('click', handleClickOutside);
137136
});
138137
139138
function handleSearchResultClick(filePath: string) {

0 commit comments

Comments
 (0)