Fix README symlink resolution in subdirectories like .github (#36775)
Fixes #36774. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -86,50 +86,6 @@ func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) {
|
|||||||
return c.repo.getCommitByPathWithID(c.ID, relpath)
|
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
|
// 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) {
|
func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) {
|
||||||
return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)
|
return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []*
|
|||||||
return "", nil, err
|
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) {
|
if err != nil && !git.IsErrNotExist(err) {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -199,10 +199,10 @@ func lfsCommitAndPushTest(t *testing.T, dstPath string, sizes ...int) (pushedFil
|
|||||||
_, _, err = gitcmd.NewCommand("lfs").AddArguments("track").AddDynamicArguments(prefix + "*").
|
_, _, err = gitcmd.NewCommand("lfs").AddArguments("track").AddDynamicArguments(prefix + "*").
|
||||||
WithDir(dstPath).RunStdString(t.Context())
|
WithDir(dstPath).RunStdString(t.Context())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = git.AddChanges(t.Context(), dstPath, false, ".gitattributes")
|
err = gitAddChangesDeprecated(t.Context(), dstPath, false, ".gitattributes")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
|
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
|
||||||
Committer: &git.Signature{
|
Committer: &git.Signature{
|
||||||
Email: "user2@example.com",
|
Email: "user2@example.com",
|
||||||
Name: "User Two",
|
Name: "User Two",
|
||||||
@@ -347,11 +347,11 @@ func generateCommitWithNewData(ctx context.Context, size int, repoPath, email, f
|
|||||||
_ = tmpFile.Close()
|
_ = tmpFile.Close()
|
||||||
|
|
||||||
// Commit
|
// Commit
|
||||||
err = git.AddChanges(ctx, repoPath, false, filepath.Base(tmpFile.Name()))
|
err = gitAddChangesDeprecated(ctx, repoPath, false, filepath.Base(tmpFile.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
err = git.CommitChanges(ctx, repoPath, git.CommitChangesOptions{
|
err = gitCommitChangesDeprecated(ctx, repoPath, gitCommitChangesOptions{
|
||||||
Committer: &git.Signature{
|
Committer: &git.Signature{
|
||||||
Email: email,
|
Email: email,
|
||||||
Name: fullName,
|
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)
|
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = git.AddChanges(t.Context(), dstPath, true)
|
err = gitAddChangesDeprecated(t.Context(), dstPath, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
|
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
|
||||||
Committer: &git.Signature{
|
Committer: &git.Signature{
|
||||||
Email: "user2@example.com",
|
Email: "user2@example.com",
|
||||||
Name: "user2",
|
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)
|
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0o666)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = git.AddChanges(t.Context(), dstPath, true)
|
err = gitAddChangesDeprecated(t.Context(), dstPath, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
|
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
|
||||||
Committer: &git.Signature{
|
Committer: &git.Signature{
|
||||||
Email: "user2@example.com",
|
Email: "user2@example.com",
|
||||||
Name: "user2",
|
Name: "user2",
|
||||||
|
|||||||
@@ -58,6 +58,52 @@ func createSSHUrl(gitPath string, u *url.URL) *url.URL {
|
|||||||
return &u2
|
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)) {
|
func onGiteaRun[T testing.TB](t T, callback func(T, *url.URL)) {
|
||||||
defer tests.PrepareTestEnv(t, 1)()
|
defer tests.PrepareTestEnv(t, 1)()
|
||||||
s := http.Server{
|
s := http.Server{
|
||||||
@@ -128,13 +174,13 @@ func doGitInitTestRepository(dstPath string) func(*testing.T) {
|
|||||||
RunStdString(t.Context())
|
RunStdString(t.Context())
|
||||||
assert.NoError(t, err)
|
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, 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{
|
signature := git.Signature{
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
Name: "test",
|
Name: "test",
|
||||||
When: time.Now(),
|
When: time.Now(),
|
||||||
}
|
}
|
||||||
assert.NoError(t, git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
|
assert.NoError(t, gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
|
||||||
Committer: &signature,
|
Committer: &signature,
|
||||||
Author: &signature,
|
Author: &signature,
|
||||||
Message: "Initial Commit",
|
Message: "Initial Commit",
|
||||||
@@ -181,12 +227,12 @@ func doGitCheckoutWriteFileCommit(opts localGitAddCommitOptions) func(*testing.T
|
|||||||
doGitCheckoutBranch(opts.LocalRepoPath, opts.CheckoutBranch)(t)
|
doGitCheckoutBranch(opts.LocalRepoPath, opts.CheckoutBranch)(t)
|
||||||
localFilePath := filepath.Join(opts.LocalRepoPath, opts.TreeFilePath)
|
localFilePath := filepath.Join(opts.LocalRepoPath, opts.TreeFilePath)
|
||||||
require.NoError(t, os.WriteFile(localFilePath, []byte(opts.TreeFileContent), 0o644))
|
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{
|
signature := git.Signature{
|
||||||
Email: "test@test.test",
|
Email: "test@test.test",
|
||||||
Name: "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,
|
Committer: &signature,
|
||||||
Author: &signature,
|
Author: &signature,
|
||||||
Message: fmt.Sprintf("update %s @ %s", opts.TreeFilePath, opts.CheckoutBranch),
|
Message: fmt.Sprintf("update %s @ %s", opts.TreeFilePath, opts.CheckoutBranch),
|
||||||
|
|||||||
@@ -997,10 +997,10 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing.
|
|||||||
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
|
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = git.AddChanges(t.Context(), dstPath, true)
|
err = gitAddChangesDeprecated(t.Context(), dstPath, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
|
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
|
||||||
Committer: &git.Signature{
|
Committer: &git.Signature{
|
||||||
Email: "user2@example.com",
|
Email: "user2@example.com",
|
||||||
Name: "user2",
|
Name: "user2",
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ func doCheckRepositoryEmptyStatus(ctx APITestContext, isEmpty bool) func(*testin
|
|||||||
func doAddChangesToCheckout(dstPath, filename string) func(*testing.T) {
|
func doAddChangesToCheckout(dstPath, filename string) func(*testing.T) {
|
||||||
return func(t *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, 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{
|
signature := git.Signature{
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
Name: "test",
|
Name: "test",
|
||||||
When: time.Now(),
|
When: time.Now(),
|
||||||
}
|
}
|
||||||
assert.NoError(t, git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
|
assert.NoError(t, gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
|
||||||
Committer: &signature,
|
Committer: &signature,
|
||||||
Author: &signature,
|
Author: &signature,
|
||||||
Message: "Initial Commit",
|
Message: "Initial Commit",
|
||||||
|
|||||||
Reference in New Issue
Block a user