fix(repo): use the viewed ref's editorconfig in file view, blame and editor (#39973)

The file view, blame and web editor read `.editorconfig` from the
default branch, so branch-specific settings like tab width were ignored.
The file view's middleware ran before the ref was resolved, and blame
never loaded it.

- `GetEditorconfig` reads the current commit, falling back to the
default branch only where no ref is resolved
- `Home` and `RefBlame` load the editorconfig after ref resolution

Fixes: https://github.com/go-gitea/gitea/issues/27413
Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
Hello-Liarandsilverwind authored and GitHub committed 2026-10-25 21:41:06 +00:00
1 parent f10f7e7f91
commit 257c34e003
8 files changed
+30 -22

No files matched your search

+1 -1
View File
@@ -299,7 +299,7 @@ func GetEditorconfig(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
ec, _, err := ctx.Repo.GetEditorconfig(ctx, ctx.Repo.Commit)
ec, _, err := ctx.Repo.GetEditorconfig(ctx)
if err != nil {
ctx.APIErrorAuto(err)
return
+1
View File
@@ -94,6 +94,7 @@ func RefBlame(ctx *context.Context) {
}
renderBlame(ctx, result.Parts, commitNames)
SetEditorconfigIfExists(ctx)
ctx.HTML(http.StatusOK, tplName)
}
+1 -1
View File
@@ -169,7 +169,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
if ctx.Repo.TreePath == ".editorconfig" {
_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx, ctx.Repo.Commit)
_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx)
if editorconfigWarning != nil {
ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error())
}
+1
View File
@@ -477,6 +477,7 @@ func Home(ctx *context.Context) {
prepareFuncs := []func(*context.Context){
prepareClonePanel,
prepareHomeSidebarRepoTopics,
SetEditorconfigIfExists,
prepareToRenderDirOrFile(entry),
prepareRecentlyPushedNewBranches,
}
+2 -2
View File
@@ -1298,7 +1298,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
// end "/{username}/{reponame}/settings"
// user/org home, including rss feeds like "/{username}/{reponame}.rss"
m.Get("/{username}/{reponame}", optSignIn, webAuth.AllowBasic, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home)
m.Get("/{username}/{reponame}", optSignIn, webAuth.AllowBasic, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.Home)
m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind[*structs.MarkupOption](), misc.Markup)
@@ -1761,7 +1761,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.Home)
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.Home)
m.Get("/*", context.RepoRefByType(""), repo.Home) // "/*" route is deprecated, and kept for backward compatibility
}, repo.SetEditorconfigIfExists)
})
m.Get("/tree/*", repo.RedirectRepoTreeToSrc) // redirect "/owner/repo/tree/*" requests to "/owner/repo/src/*"
m.Get("/blob/*", repo.RedirectRepoBlobToCommit) // redirect "/owner/repo/blob/*" requests to "/owner/repo/src/commit/*"
+4 -8
View File
@@ -307,18 +307,14 @@ func (r *Repository) RefTypeNameSubURL() string {
return r.RefFullName.RefWebLinkPath()
}
// GetEditorconfig returns the .editorconfig definition if found in the
// HEAD of the default repo branch.
func (r *Repository) GetEditorconfig(ctx context.Context, optCommit ...*git.Commit) (cfg *editorconfig.Editorconfig, warning, err error) {
// GetEditorconfig returns the .editorconfig of the current commit, falling back to the default branch
func (r *Repository) GetEditorconfig(ctx context.Context) (cfg *editorconfig.Editorconfig, warning, err error) {
if r.GitRepo == nil {
return nil, nil, nil
}
var commit *git.Commit
if len(optCommit) != 0 {
commit = optCommit[0]
} else {
commit := r.Commit
if commit == nil {
commit, err = r.GitRepo.GetBranchCommit(ctx, r.Repository.DefaultBranch)
if err != nil {
return nil, nil, err
+13 -10
View File
@@ -1,16 +1,19 @@
import {env} from 'node:process';
import {expect, test} from '@playwright/test';
import {login, apiCreateRepo, randomString} from './utils.ts';
import {login, apiCreateFiles, apiCreateRepo, randomString} from './utils.ts';
test('codeeditor textarea updates correctly', async ({page, request}) => {
const repoName = `e2e-codeeditor-${randomString(8)}`;
await Promise.all([apiCreateRepo(request, {name: repoName}), login(page)]);
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/_new/main`);
await page.getByPlaceholder('Name your file…').fill('test.js');
await expect(page.locator('[data-tab="write"] .editor-loading')).toBeHidden();
const editor = page.locator('.cm-content[role="textbox"]');
await expect(editor).toBeVisible();
await editor.click();
await page.keyboard.type('const hello = "world";');
await expect(page.locator('textarea[name="content"]')).toHaveValue('const hello = "world";');
const owner = env.GITEA_TEST_E2E_USER;
await Promise.all([
(async () => {
await apiCreateRepo(request, {name: repoName});
await apiCreateFiles(request, owner, repoName, [{path: '.editorconfig', content: '[*]\nindent_size = 2\n'}], {newBranch: 'feature'});
})(),
login(page),
]);
await page.goto(`/${owner}/${repoName}/_new/feature`);
await page.locator('.cm-content[role="textbox"]').click();
await page.keyboard.press('Tab');
await expect(page.locator('textarea[name="content"]')).toHaveValue(' ');
});
+7
View File
@@ -39,6 +39,13 @@ func TestEditor(t *testing.T) {
testEditFile(t, sessionUser2, "user2", "repo1", "master", "README.md", "Hello, World (direct)\n")
testEditFileToNewBranch(t, sessionUser2, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (commit-to-new-branch)\n")
})
t.Run("EditorconfigOfBranch", func(t *testing.T) {
testCreateFile(t, sessionUser2, "user2", "repo1", "master", "editorconfig", ".editorconfig", "[*]\nindent_size = 2\n")
for _, link := range []string{"/user2/repo1/src/branch/editorconfig/README.md", "/user2/repo1/blame/branch/editorconfig/README.md"} {
resp := sessionUser2.MakeRequest(t, NewRequest(t, "GET", link), http.StatusOK)
AssertHTMLElement(t, NewHTMLParser(t, resp.Body), ".tab-size-2", true)
}
})
t.Run("PatchFile", testEditorPatchFile)
t.Run("DeleteFile", func(t *testing.T) {
viewLink := "/user2/repo1/src/branch/branch2/README.md"