Files
runner/act/filecollector/file_collector.go
T
silverwind 20c8b4abc4 refactor: replace go-git with the git CLI for gitignore filtering (#1309)
Replace go-git in `exec --use-gitignore` filtering with `git ls-files`. The git CLI is already required for action clones since https://gitea.com/gitea/runner/pulls/1277, so this drops `go-git` and 14 other modules, and filtering now follows git's own ignore rules.

Also stop deleting a remote action's `.gitignore` before copying it. Tracked files are never filtered, so the deletion only made copied actions differ from GitHub's action tarballs.

Reviewed-on: https://gitea.com/gitea/runner/pulls/1309
Reviewed-by: bircni <[email protected]>
Co-authored-by: silverwind <[email protected]>
2026-10-12 18:13:41 +00:00

173 lines
4.2 KiB
Go

// Copyright 2024 The Gitea Authors. All rights reserved.
// Copyright 2024 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package filecollector
import (
"archive/tar"
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/common/git"
"golang.org/x/text/unicode/norm"
)
type Handler interface {
WriteFile(path string, fi fs.FileInfo, linkName string, f io.Reader) error
}
type TarCollector struct {
TarWriter *tar.Writer
UID int
GID int
DstDir string
}
func (tc TarCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string, f io.Reader) error {
// create a new dir/file header
header, err := tar.FileInfoHeader(fi, linkName)
if err != nil {
return err
}
// update the name to correctly reflect the desired destination when untaring
header.Name = path.Join(tc.DstDir, fpath)
header.Mode = int64(fi.Mode())
header.ModTime = fi.ModTime()
header.Uid = tc.UID
header.Gid = tc.GID
// write the header
if err := tc.TarWriter.WriteHeader(header); err != nil {
return err
}
// this is a symlink no reader provided
if f == nil {
return nil
}
// copy file data into tar writer
if _, err := io.Copy(tc.TarWriter, f); err != nil {
return err
}
return nil
}
type CopyCollector struct {
DstDir string
}
func (cc *CopyCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string, f io.Reader) error {
fdestpath := filepath.Join(cc.DstDir, fpath)
if err := os.MkdirAll(filepath.Dir(fdestpath), 0o777); err != nil {
return err
}
// Remove any existing destination so we can overwrite read-only files
// (e.g. git pack files at mode 0444 trip EACCES on macOS and "Access is
// denied" on Windows when reopened with O_WRONLY) and so os.Symlink does
// not fail with EEXIST. os.Remove clears the Windows read-only attribute
// internally; on Unix unlink only needs write permission on the parent.
_ = os.Remove(fdestpath)
if f == nil {
return os.Symlink(linkName, fdestpath)
}
df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, fi.Mode())
if err != nil {
return err
}
defer df.Close()
if _, err := io.Copy(df, f); err != nil {
return err
}
return nil
}
type FileCollector struct {
SrcPath string
SrcPrefix string
SkipGitDir bool
UseGitIgnore bool
Handler Handler
}
// foldPath compares git's spelling of a path with the filesystem's, which may differ in case or Unicode normalization.
func foldPath(name string) string {
return strings.ToLower(norm.NFC.String(name))
}
func (fc *FileCollector) gitFiles(ctx context.Context) map[string]bool {
if !fc.UseGitIgnore {
return nil
}
files, err := git.ListFiles(ctx, fc.SrcPath)
if err != nil {
common.Logger(ctx).Debugf("Copying %s without applying .gitignore: %v", fc.SrcPath, err)
return nil
}
keep := map[string]bool{".": true}
for _, file := range files {
for file = foldPath(file); !keep[file]; file = path.Dir(file) {
keep[file] = true
}
}
return keep
}
func (fc *FileCollector) CollectFiles(ctx context.Context) filepath.WalkFunc {
keep := fc.gitFiles(ctx)
return func(file string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if ctx.Err() != nil {
return errors.New("copy cancelled")
}
if fc.SkipGitDir && fi.Name() == ".git" {
if fi.IsDir() {
return filepath.SkipDir
}
return nil
}
if keep != nil {
rel, _ := filepath.Rel(fc.SrcPath, file)
if rel = filepath.ToSlash(rel); !keep[foldPath(rel)] && !strings.Contains("/"+rel+"/", "/.git/") {
if fi.IsDir() {
return filepath.SkipDir
}
return nil
}
}
path := filepath.ToSlash(strings.TrimPrefix(file, fc.SrcPrefix))
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
linkName, err := os.Readlink(file)
if err != nil {
return fmt.Errorf("unable to readlink '%s': %w", file, err)
}
return fc.Handler.WriteFile(path, fi, linkName, nil)
} else if !fi.Mode().IsRegular() {
return nil
}
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
defer context.AfterFunc(ctx, func() { _ = f.Close() })()
return fc.Handler.WriteFile(path, fi, "", f)
}
}