Skip to content

Commit f02f419

Browse files
jimpariswxiaoguang
andauthored
Fix README symlink resolution in subdirectories like .github (#36775)
Fixes #36774. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
1 parent 48a3a47 commit f02f419

7 files changed

Lines changed: 133 additions & 61 deletions

File tree

modules/git/commit.go

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -86,50 +86,6 @@ func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) {
8686
return c.repo.getCommitByPathWithID(c.ID, relpath)
8787
}
8888

89-
// AddChanges marks local changes to be ready for commit.
90-
func AddChanges(ctx context.Context, repoPath string, all bool, files ...string) error {
91-
cmd := gitcmd.NewCommand().AddArguments("add")
92-
if all {
93-
cmd.AddArguments("--all")
94-
}
95-
cmd.AddDashesAndList(files...)
96-
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
97-
return err
98-
}
99-
100-
// CommitChangesOptions the options when a commit created
101-
type CommitChangesOptions struct {
102-
Committer *Signature
103-
Author *Signature
104-
Message string
105-
}
106-
107-
// CommitChanges commits local changes with given committer, author and message.
108-
// If author is nil, it will be the same as committer.
109-
func CommitChanges(ctx context.Context, repoPath string, opts CommitChangesOptions) error {
110-
cmd := gitcmd.NewCommand()
111-
if opts.Committer != nil {
112-
cmd.AddOptionValues("-c", "user.name="+opts.Committer.Name)
113-
cmd.AddOptionValues("-c", "user.email="+opts.Committer.Email)
114-
}
115-
cmd.AddArguments("commit")
116-
117-
if opts.Author == nil {
118-
opts.Author = opts.Committer
119-
}
120-
if opts.Author != nil {
121-
cmd.AddOptionFormat("--author='%s <%s>'", opts.Author.Name, opts.Author.Email)
122-
}
123-
cmd.AddOptionFormat("--message=%s", opts.Message)
124-
125-
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
126-
// No stderr but exit status 1 means nothing to commit.
127-
if gitcmd.IsErrorExitCode(err, 1) {
128-
return nil
129-
}
130-
return err
131-
}
132-
13389
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
13490
func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) {
13591
return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)

routers/web/repo/view_readme.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []*
102102
return "", nil, err
103103
}
104104

105-
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, parentDir, childEntries, false)
105+
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, path.Join(parentDir, subTreeEntry.Name()), childEntries, false)
106106
if err != nil && !git.IsErrNotExist(err) {
107107
return "", nil, err
108108
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2026 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"fmt"
8+
"path"
9+
"testing"
10+
11+
"code.gitea.io/gitea/modules/git"
12+
"code.gitea.io/gitea/modules/git/gitcmd"
13+
"code.gitea.io/gitea/services/contexttest"
14+
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func TestFindReadmeFileInEntriesWithSymlinkInSubfolder(t *testing.T) {
20+
for _, subdir := range []string{".github", ".gitea", "docs"} {
21+
t.Run(subdir, func(t *testing.T) {
22+
repoPath := t.TempDir()
23+
stdin := fmt.Sprintf(`commit refs/heads/master
24+
author Test <test@example.com> 1700000000 +0000
25+
committer Test <test@example.com> 1700000000 +0000
26+
data <<EOT
27+
initial
28+
EOT
29+
M 100644 inline target.md
30+
data <<EOT
31+
target-content
32+
EOT
33+
M 120000 inline %s/README.md
34+
data 12
35+
../target.md
36+
`, subdir)
37+
38+
var err error
39+
err = gitcmd.NewCommand("init", "--bare", ".").WithDir(repoPath).RunWithStderr(t.Context())
40+
require.NoError(t, err)
41+
err = gitcmd.NewCommand("fast-import").WithDir(repoPath).WithStdinBytes([]byte(stdin)).RunWithStderr(t.Context())
42+
require.NoError(t, err)
43+
44+
gitRepo, err := git.OpenRepository(t.Context(), repoPath)
45+
require.NoError(t, err)
46+
defer gitRepo.Close()
47+
48+
commit, err := gitRepo.GetBranchCommit("master")
49+
require.NoError(t, err)
50+
51+
entries, err := commit.ListEntries()
52+
require.NoError(t, err)
53+
54+
ctx, _ := contexttest.MockContext(t, "/")
55+
ctx.Repo.Commit = commit
56+
foundDir, foundReadme, err := findReadmeFileInEntries(ctx, "", entries, true)
57+
require.NoError(t, err)
58+
require.NotNil(t, foundReadme)
59+
60+
assert.Equal(t, subdir, foundDir)
61+
assert.Equal(t, "README.md", foundReadme.Name())
62+
assert.True(t, foundReadme.IsLink())
63+
64+
// Verify that it can follow the link
65+
res, err := git.EntryFollowLinks(commit, path.Join(foundDir, foundReadme.Name()), foundReadme)
66+
require.NoError(t, err)
67+
assert.Equal(t, "target.md", res.TargetFullPath)
68+
})
69+
}
70+
}

tests/integration/git_general_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,10 @@ func lfsCommitAndPushTest(t *testing.T, dstPath string, sizes ...int) (pushedFil
199199
_, _, err = gitcmd.NewCommand("lfs").AddArguments("track").AddDynamicArguments(prefix + "*").
200200
WithDir(dstPath).RunStdString(t.Context())
201201
assert.NoError(t, err)
202-
err = git.AddChanges(t.Context(), dstPath, false, ".gitattributes")
202+
err = gitAddChangesDeprecated(t.Context(), dstPath, false, ".gitattributes")
203203
assert.NoError(t, err)
204204

205-
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
205+
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
206206
Committer: &git.Signature{
207207
Email: "user2@example.com",
208208
Name: "User Two",
@@ -347,11 +347,11 @@ func generateCommitWithNewData(ctx context.Context, size int, repoPath, email, f
347347
_ = tmpFile.Close()
348348

349349
// Commit
350-
err = git.AddChanges(ctx, repoPath, false, filepath.Base(tmpFile.Name()))
350+
err = gitAddChangesDeprecated(ctx, repoPath, false, filepath.Base(tmpFile.Name()))
351351
if err != nil {
352352
return "", err
353353
}
354-
err = git.CommitChanges(ctx, repoPath, git.CommitChangesOptions{
354+
err = gitCommitChangesDeprecated(ctx, repoPath, gitCommitChangesOptions{
355355
Committer: &git.Signature{
356356
Email: email,
357357
Name: fullName,
@@ -837,10 +837,10 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string
837837
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
838838
require.NoError(t, err)
839839

840-
err = git.AddChanges(t.Context(), dstPath, true)
840+
err = gitAddChangesDeprecated(t.Context(), dstPath, true)
841841
assert.NoError(t, err)
842842

843-
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
843+
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
844844
Committer: &git.Signature{
845845
Email: "user2@example.com",
846846
Name: "user2",
@@ -909,10 +909,10 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string
909909
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0o666)
910910
require.NoError(t, err)
911911

912-
err = git.AddChanges(t.Context(), dstPath, true)
912+
err = gitAddChangesDeprecated(t.Context(), dstPath, true)
913913
assert.NoError(t, err)
914914

915-
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
915+
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
916916
Committer: &git.Signature{
917917
Email: "user2@example.com",
918918
Name: "user2",

tests/integration/git_helper_for_declarative_test.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,52 @@ func createSSHUrl(gitPath string, u *url.URL) *url.URL {
5858
return &u2
5959
}
6060

61+
// gitAddChangesDeprecated marks local changes to be ready for commit.
62+
// Deprecated: use "git fast-import" instead for better performance and more control over the commit creation.
63+
func gitAddChangesDeprecated(ctx context.Context, repoPath string, all bool, files ...string) error {
64+
cmd := gitcmd.NewCommand().AddArguments("add")
65+
if all {
66+
cmd.AddArguments("--all")
67+
}
68+
cmd.AddDashesAndList(files...)
69+
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
70+
return err
71+
}
72+
73+
// CommitChangesOptions the options when a commit created
74+
type gitCommitChangesOptions struct {
75+
Committer *git.Signature
76+
Author *git.Signature
77+
Message string
78+
}
79+
80+
// gitCommitChangesDeprecated commits local changes with given committer, author and message.
81+
// If author is nil, it will be the same as committer.
82+
// Deprecated: use "git fast-import" instead for better performance and more control over the commit creation.
83+
func gitCommitChangesDeprecated(ctx context.Context, repoPath string, opts gitCommitChangesOptions) error {
84+
cmd := gitcmd.NewCommand()
85+
if opts.Committer != nil {
86+
cmd.AddOptionValues("-c", "user.name="+opts.Committer.Name)
87+
cmd.AddOptionValues("-c", "user.email="+opts.Committer.Email)
88+
}
89+
cmd.AddArguments("commit")
90+
91+
if opts.Author == nil {
92+
opts.Author = opts.Committer
93+
}
94+
if opts.Author != nil {
95+
cmd.AddOptionFormat("--author='%s <%s>'", opts.Author.Name, opts.Author.Email)
96+
}
97+
cmd.AddOptionFormat("--message=%s", opts.Message)
98+
99+
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
100+
// No stderr but exit status 1 means nothing to commit.
101+
if gitcmd.IsErrorExitCode(err, 1) {
102+
return nil
103+
}
104+
return err
105+
}
106+
61107
func onGiteaRun[T testing.TB](t T, callback func(T, *url.URL)) {
62108
defer tests.PrepareTestEnv(t, 1)()
63109
s := http.Server{
@@ -128,13 +174,13 @@ func doGitInitTestRepository(dstPath string) func(*testing.T) {
128174
RunStdString(t.Context())
129175
assert.NoError(t, err)
130176
assert.NoError(t, os.WriteFile(filepath.Join(dstPath, "README.md"), []byte("# Testing Repository\n\nOriginally created in: "+dstPath), 0o644))
131-
assert.NoError(t, git.AddChanges(t.Context(), dstPath, true))
177+
assert.NoError(t, gitAddChangesDeprecated(t.Context(), dstPath, true))
132178
signature := git.Signature{
133179
Email: "test@example.com",
134180
Name: "test",
135181
When: time.Now(),
136182
}
137-
assert.NoError(t, git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
183+
assert.NoError(t, gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
138184
Committer: &signature,
139185
Author: &signature,
140186
Message: "Initial Commit",
@@ -181,12 +227,12 @@ func doGitCheckoutWriteFileCommit(opts localGitAddCommitOptions) func(*testing.T
181227
doGitCheckoutBranch(opts.LocalRepoPath, opts.CheckoutBranch)(t)
182228
localFilePath := filepath.Join(opts.LocalRepoPath, opts.TreeFilePath)
183229
require.NoError(t, os.WriteFile(localFilePath, []byte(opts.TreeFileContent), 0o644))
184-
require.NoError(t, git.AddChanges(t.Context(), opts.LocalRepoPath, true))
230+
require.NoError(t, gitAddChangesDeprecated(t.Context(), opts.LocalRepoPath, true))
185231
signature := git.Signature{
186232
Email: "test@test.test",
187233
Name: "test",
188234
}
189-
require.NoError(t, git.CommitChanges(t.Context(), opts.LocalRepoPath, git.CommitChangesOptions{
235+
require.NoError(t, gitCommitChangesDeprecated(t.Context(), opts.LocalRepoPath, gitCommitChangesOptions{
190236
Committer: &signature,
191237
Author: &signature,
192238
Message: fmt.Sprintf("update %s @ %s", opts.TreeFilePath, opts.CheckoutBranch),

tests/integration/pull_merge_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -997,10 +997,10 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing.
997997
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
998998
assert.NoError(t, err)
999999

1000-
err = git.AddChanges(t.Context(), dstPath, true)
1000+
err = gitAddChangesDeprecated(t.Context(), dstPath, true)
10011001
assert.NoError(t, err)
10021002

1003-
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
1003+
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
10041004
Committer: &git.Signature{
10051005
Email: "user2@example.com",
10061006
Name: "user2",

tests/integration/ssh_key_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ func doCheckRepositoryEmptyStatus(ctx APITestContext, isEmpty bool) func(*testin
2828
func doAddChangesToCheckout(dstPath, filename string) func(*testing.T) {
2929
return func(t *testing.T) {
3030
assert.NoError(t, os.WriteFile(filepath.Join(dstPath, filename), fmt.Appendf(nil, "# Testing Repository\n\nOriginally created in: %s at time: %v", dstPath, time.Now()), 0o644))
31-
assert.NoError(t, git.AddChanges(t.Context(), dstPath, true))
31+
assert.NoError(t, gitAddChangesDeprecated(t.Context(), dstPath, true))
3232
signature := git.Signature{
3333
Email: "test@example.com",
3434
Name: "test",
3535
When: time.Now(),
3636
}
37-
assert.NoError(t, git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
37+
assert.NoError(t, gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
3838
Committer: &signature,
3939
Author: &signature,
4040
Message: "Initial Commit",

0 commit comments

Comments
 (0)