Add ability to download subpath archive (#36371)

closes: https://github.com/go-gitea/gitea/issues/4478

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
TheFox0x7
2026-01-16 10:31:12 +01:00
committed by GitHub
parent 67e75f30a8
commit 69c5921d71
18 changed files with 230 additions and 134 deletions

View File

@@ -426,9 +426,9 @@ type RunStdError interface {
}
type runStdError struct {
err error
stderr string
errMsg string
err error // usually the low-level error like `*exec.ExitError`
stderr string // git command's stderr output
errMsg string // the cached error message for Error() method
}
func (r *runStdError) Error() string {
@@ -448,6 +448,22 @@ func (r *runStdError) Stderr() string {
return r.stderr
}
func ErrorAsStderr(err error) (string, bool) {
var runErr RunStdError
if errors.As(err, &runErr) {
return runErr.Stderr(), true
}
return "", false
}
func StderrHasPrefix(err error, prefix string) bool {
stderr, ok := ErrorAsStderr(err)
if !ok {
return false
}
return strings.HasPrefix(stderr, prefix)
}
func IsErrorExitCode(err error, code int) bool {
var exitError *exec.ExitError
if errors.As(err, &exitError) {

View File

@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/tempdir"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
@@ -99,3 +100,14 @@ func TestCommandString(t *testing.T) {
cmd = NewCommand("url: https://a:b@c/", "/root/dir-a/dir-b")
assert.Equal(t, cmd.prog+` "url: https://sanitized-credential@c/" .../dir-a/dir-b`, cmd.LogString())
}
func TestRunStdError(t *testing.T) {
e := &runStdError{stderr: "some error"}
var err RunStdError = e
var asErr RunStdError
require.ErrorAs(t, err, &asErr)
require.Equal(t, "some error", asErr.Stderr())
require.ErrorAs(t, fmt.Errorf("wrapped %w", err), &asErr)
}

View File

@@ -3,12 +3,18 @@
package gitcmd
import "fmt"
import (
"fmt"
// ConcatenateError concatenats an error with stderr string
"code.gitea.io/gitea/modules/util"
)
// ConcatenateError concatenates an error with stderr string
// FIXME: use RunStdError instead
func ConcatenateError(err error, stderr string) error {
if len(stderr) == 0 {
return err
}
return fmt.Errorf("%w - %s", err, stderr)
errMsg := fmt.Sprintf("%s - %s", err.Error(), stderr)
return util.ErrorWrap(&runStdError{err: err, stderr: stderr, errMsg: errMsg}, "%s", errMsg)
}

View File

@@ -8,7 +8,9 @@ import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"slices"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
@@ -16,7 +18,7 @@ import (
)
// CreateArchive create archive content to the target path
func CreateArchive(ctx context.Context, repo Repository, format string, target io.Writer, usePrefix bool, commitID string) error {
func CreateArchive(ctx context.Context, repo Repository, format string, target io.Writer, usePrefix bool, commitID string, paths []string) error {
if format == "unknown" {
return fmt.Errorf("unknown format: %v", format)
}
@@ -28,6 +30,13 @@ func CreateArchive(ctx context.Context, repo Repository, format string, target i
cmd.AddOptionFormat("--format=%s", format)
cmd.AddDynamicArguments(commitID)
paths = slices.Clone(paths)
for i := range paths {
// although "git archive" already ensures the paths won't go outside the repo, we still clean them here for safety
paths[i] = path.Clean(paths[i])
}
cmd.AddDynamicArguments(paths...)
var stderr strings.Builder
if err := RunCmd(ctx, repo, cmd.WithStdout(target).WithStderr(&stderr)); err != nil {
return gitcmd.ConcatenateError(err, stderr.String())

View File

@@ -4,6 +4,9 @@
package test
import (
"archive/tar"
"compress/gzip"
"io"
"net/http"
"net/http/httptest"
"os"
@@ -71,3 +74,31 @@ func SetupGiteaRoot() string {
_ = os.Setenv("GITEA_ROOT", giteaRoot)
return giteaRoot
}
func ReadAllTarGzContent(r io.Reader) (map[string]string, error) {
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
content := make(map[string]string)
tr := tar.NewReader(gzr)
for {
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
buf, err := io.ReadAll(tr)
if err != nil {
return nil, err
}
content[hd.Name] = string(buf)
}
return content, nil
}