Fix README symlink resolution in subdirectories like .github (#36775)

Fixes #36774.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Jim Paris
2026-02-28 23:33:08 -06:00
committed by GitHub
parent 48a3a47741
commit f02f419173
7 changed files with 133 additions and 61 deletions

View File

@@ -86,50 +86,6 @@ func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) {
return c.repo.getCommitByPathWithID(c.ID, relpath)
}
// AddChanges marks local changes to be ready for commit.
func AddChanges(ctx context.Context, repoPath string, all bool, files ...string) error {
cmd := gitcmd.NewCommand().AddArguments("add")
if all {
cmd.AddArguments("--all")
}
cmd.AddDashesAndList(files...)
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
return err
}
// CommitChangesOptions the options when a commit created
type CommitChangesOptions struct {
Committer *Signature
Author *Signature
Message string
}
// CommitChanges commits local changes with given committer, author and message.
// If author is nil, it will be the same as committer.
func CommitChanges(ctx context.Context, repoPath string, opts CommitChangesOptions) error {
cmd := gitcmd.NewCommand()
if opts.Committer != nil {
cmd.AddOptionValues("-c", "user.name="+opts.Committer.Name)
cmd.AddOptionValues("-c", "user.email="+opts.Committer.Email)
}
cmd.AddArguments("commit")
if opts.Author == nil {
opts.Author = opts.Committer
}
if opts.Author != nil {
cmd.AddOptionFormat("--author='%s <%s>'", opts.Author.Name, opts.Author.Email)
}
cmd.AddOptionFormat("--message=%s", opts.Message)
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
// No stderr but exit status 1 means nothing to commit.
if gitcmd.IsErrorExitCode(err, 1) {
return nil
}
return err
}
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) {
return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)

View File

@@ -102,7 +102,7 @@ func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []*
return "", nil, err
}
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, parentDir, childEntries, false)
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, path.Join(parentDir, subTreeEntry.Name()), childEntries, false)
if err != nil && !git.IsErrNotExist(err) {
return "", nil, err
}

View File

@@ -0,0 +1,70 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"fmt"
"path"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFindReadmeFileInEntriesWithSymlinkInSubfolder(t *testing.T) {
for _, subdir := range []string{".github", ".gitea", "docs"} {
t.Run(subdir, func(t *testing.T) {
repoPath := t.TempDir()
stdin := fmt.Sprintf(`commit refs/heads/master
author Test <test@example.com> 1700000000 +0000
committer Test <test@example.com> 1700000000 +0000
data <<EOT
initial
EOT
M 100644 inline target.md
data <<EOT
target-content
EOT
M 120000 inline %s/README.md
data 12
../target.md
`, subdir)
var err error
err = gitcmd.NewCommand("init", "--bare", ".").WithDir(repoPath).RunWithStderr(t.Context())
require.NoError(t, err)
err = gitcmd.NewCommand("fast-import").WithDir(repoPath).WithStdinBytes([]byte(stdin)).RunWithStderr(t.Context())
require.NoError(t, err)
gitRepo, err := git.OpenRepository(t.Context(), repoPath)
require.NoError(t, err)
defer gitRepo.Close()
commit, err := gitRepo.GetBranchCommit("master")
require.NoError(t, err)
entries, err := commit.ListEntries()
require.NoError(t, err)
ctx, _ := contexttest.MockContext(t, "/")
ctx.Repo.Commit = commit
foundDir, foundReadme, err := findReadmeFileInEntries(ctx, "", entries, true)
require.NoError(t, err)
require.NotNil(t, foundReadme)
assert.Equal(t, subdir, foundDir)
assert.Equal(t, "README.md", foundReadme.Name())
assert.True(t, foundReadme.IsLink())
// Verify that it can follow the link
res, err := git.EntryFollowLinks(commit, path.Join(foundDir, foundReadme.Name()), foundReadme)
require.NoError(t, err)
assert.Equal(t, "target.md", res.TargetFullPath)
})
}
}

View File

@@ -199,10 +199,10 @@ func lfsCommitAndPushTest(t *testing.T, dstPath string, sizes ...int) (pushedFil
_, _, err = gitcmd.NewCommand("lfs").AddArguments("track").AddDynamicArguments(prefix + "*").
WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err)
err = git.AddChanges(t.Context(), dstPath, false, ".gitattributes")
err = gitAddChangesDeprecated(t.Context(), dstPath, false, ".gitattributes")
assert.NoError(t, err)
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
Committer: &git.Signature{
Email: "user2@example.com",
Name: "User Two",
@@ -347,11 +347,11 @@ func generateCommitWithNewData(ctx context.Context, size int, repoPath, email, f
_ = tmpFile.Close()
// Commit
err = git.AddChanges(ctx, repoPath, false, filepath.Base(tmpFile.Name()))
err = gitAddChangesDeprecated(ctx, repoPath, false, filepath.Base(tmpFile.Name()))
if err != nil {
return "", err
}
err = git.CommitChanges(ctx, repoPath, git.CommitChangesOptions{
err = gitCommitChangesDeprecated(ctx, repoPath, gitCommitChangesOptions{
Committer: &git.Signature{
Email: email,
Name: fullName,
@@ -837,10 +837,10 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
require.NoError(t, err)
err = git.AddChanges(t.Context(), dstPath, true)
err = gitAddChangesDeprecated(t.Context(), dstPath, true)
assert.NoError(t, err)
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
Committer: &git.Signature{
Email: "user2@example.com",
Name: "user2",
@@ -909,10 +909,10 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0o666)
require.NoError(t, err)
err = git.AddChanges(t.Context(), dstPath, true)
err = gitAddChangesDeprecated(t.Context(), dstPath, true)
assert.NoError(t, err)
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
Committer: &git.Signature{
Email: "user2@example.com",
Name: "user2",

View File

@@ -58,6 +58,52 @@ func createSSHUrl(gitPath string, u *url.URL) *url.URL {
return &u2
}
// gitAddChangesDeprecated marks local changes to be ready for commit.
// Deprecated: use "git fast-import" instead for better performance and more control over the commit creation.
func gitAddChangesDeprecated(ctx context.Context, repoPath string, all bool, files ...string) error {
cmd := gitcmd.NewCommand().AddArguments("add")
if all {
cmd.AddArguments("--all")
}
cmd.AddDashesAndList(files...)
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
return err
}
// CommitChangesOptions the options when a commit created
type gitCommitChangesOptions struct {
Committer *git.Signature
Author *git.Signature
Message string
}
// gitCommitChangesDeprecated commits local changes with given committer, author and message.
// If author is nil, it will be the same as committer.
// Deprecated: use "git fast-import" instead for better performance and more control over the commit creation.
func gitCommitChangesDeprecated(ctx context.Context, repoPath string, opts gitCommitChangesOptions) error {
cmd := gitcmd.NewCommand()
if opts.Committer != nil {
cmd.AddOptionValues("-c", "user.name="+opts.Committer.Name)
cmd.AddOptionValues("-c", "user.email="+opts.Committer.Email)
}
cmd.AddArguments("commit")
if opts.Author == nil {
opts.Author = opts.Committer
}
if opts.Author != nil {
cmd.AddOptionFormat("--author='%s <%s>'", opts.Author.Name, opts.Author.Email)
}
cmd.AddOptionFormat("--message=%s", opts.Message)
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
// No stderr but exit status 1 means nothing to commit.
if gitcmd.IsErrorExitCode(err, 1) {
return nil
}
return err
}
func onGiteaRun[T testing.TB](t T, callback func(T, *url.URL)) {
defer tests.PrepareTestEnv(t, 1)()
s := http.Server{
@@ -128,13 +174,13 @@ func doGitInitTestRepository(dstPath string) func(*testing.T) {
RunStdString(t.Context())
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(dstPath, "README.md"), []byte("# Testing Repository\n\nOriginally created in: "+dstPath), 0o644))
assert.NoError(t, git.AddChanges(t.Context(), dstPath, true))
assert.NoError(t, gitAddChangesDeprecated(t.Context(), dstPath, true))
signature := git.Signature{
Email: "test@example.com",
Name: "test",
When: time.Now(),
}
assert.NoError(t, git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
assert.NoError(t, gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: "Initial Commit",
@@ -181,12 +227,12 @@ func doGitCheckoutWriteFileCommit(opts localGitAddCommitOptions) func(*testing.T
doGitCheckoutBranch(opts.LocalRepoPath, opts.CheckoutBranch)(t)
localFilePath := filepath.Join(opts.LocalRepoPath, opts.TreeFilePath)
require.NoError(t, os.WriteFile(localFilePath, []byte(opts.TreeFileContent), 0o644))
require.NoError(t, git.AddChanges(t.Context(), opts.LocalRepoPath, true))
require.NoError(t, gitAddChangesDeprecated(t.Context(), opts.LocalRepoPath, true))
signature := git.Signature{
Email: "test@test.test",
Name: "test",
}
require.NoError(t, git.CommitChanges(t.Context(), opts.LocalRepoPath, git.CommitChangesOptions{
require.NoError(t, gitCommitChangesDeprecated(t.Context(), opts.LocalRepoPath, gitCommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: fmt.Sprintf("update %s @ %s", opts.TreeFilePath, opts.CheckoutBranch),

View File

@@ -997,10 +997,10 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing.
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
assert.NoError(t, err)
err = git.AddChanges(t.Context(), dstPath, true)
err = gitAddChangesDeprecated(t.Context(), dstPath, true)
assert.NoError(t, err)
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
Committer: &git.Signature{
Email: "user2@example.com",
Name: "user2",

View File

@@ -28,13 +28,13 @@ func doCheckRepositoryEmptyStatus(ctx APITestContext, isEmpty bool) func(*testin
func doAddChangesToCheckout(dstPath, filename string) func(*testing.T) {
return func(t *testing.T) {
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))
assert.NoError(t, git.AddChanges(t.Context(), dstPath, true))
assert.NoError(t, gitAddChangesDeprecated(t.Context(), dstPath, true))
signature := git.Signature{
Email: "test@example.com",
Name: "test",
When: time.Now(),
}
assert.NoError(t, git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
assert.NoError(t, gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: "Initial Commit",