mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-06 15:20:29 +00:00
Built-in actions that select files need to match what the JavaScript actions match. This ports @actions/glob 0.6.1 and the minimatch 3 subset it uses, as bundled in `actions/upload-artifact` v7. It only answers questions about paths; walking the files stays with the caller. Only malformed bracket expressions and some non-ASCII case folding on Windows can match differently. Co-authored-by: silverwind <[email protected]> Reviewed-on: https://gitea.com/gitea/runner/pulls/1342 Reviewed-by: silverwind <[email protected]> Co-authored-by: bircni <[email protected]>
285 lines
6.7 KiB
Go
285 lines
6.7 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package glob
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// flavour selects Windows or POSIX path semantics; the helpers port @actions/glob internal-path-helper.ts.
|
|
type flavour struct{ windows bool }
|
|
|
|
var (
|
|
reUNCPrefix = regexp.MustCompile(`^\\\\+[^\\]`)
|
|
reBackslashRun = regexp.MustCompile(`\\\\+`)
|
|
reSlashRun = regexp.MustCompile(`//+`)
|
|
reDriveRoot = regexp.MustCompile(`^[A-Za-z]:\\$`)
|
|
reDriveAbs = regexp.MustCompile(`^[A-Za-z]:\\`)
|
|
reDrive = regexp.MustCompile(`^[A-Za-z]:`)
|
|
reDriveOnly = regexp.MustCompile(`^[A-Za-z]:$`)
|
|
reDriveRel = regexp.MustCompile(`^[A-Za-z]:[^\\/]|^[A-Za-z]:$`)
|
|
reRelRoot = regexp.MustCompile(`^\\$|^\\[^\\]`)
|
|
reRelRootOnly = regexp.MustCompile(`^\\[^\\]`)
|
|
reDriveNoSep = regexp.MustCompile(`^[A-Za-z]:[^\\]`)
|
|
reUNCRoot = regexp.MustCompile(`^\\\\[^\\]+(\\[^\\]+)?$`)
|
|
reUNCRootSlash = regexp.MustCompile(`^\\\\[^\\]+\\[^\\]+\\$`)
|
|
)
|
|
|
|
func (w flavour) sep() string {
|
|
if w.windows {
|
|
return `\`
|
|
}
|
|
return "/"
|
|
}
|
|
|
|
func (w flavour) isSep(c byte) bool {
|
|
return c == '/' || (w.windows && c == '\\')
|
|
}
|
|
|
|
func (w flavour) normalizeSeparators(p string) string {
|
|
if w.windows {
|
|
p = strings.ReplaceAll(p, "/", `\`)
|
|
prefix := ""
|
|
if reUNCPrefix.MatchString(p) {
|
|
prefix = `\`
|
|
}
|
|
return prefix + reBackslashRun.ReplaceAllLiteralString(p, `\`)
|
|
}
|
|
return reSlashRun.ReplaceAllLiteralString(p, "/")
|
|
}
|
|
|
|
func (w flavour) safeTrimTrailingSeparator(p string) string {
|
|
if p == "" {
|
|
return ""
|
|
}
|
|
p = w.normalizeSeparators(p)
|
|
if !strings.HasSuffix(p, w.sep()) || p == w.sep() || (w.windows && reDriveRoot.MatchString(p)) {
|
|
return p
|
|
}
|
|
return p[:len(p)-1]
|
|
}
|
|
|
|
func (w flavour) hasAbsoluteRoot(p string) bool {
|
|
p = w.normalizeSeparators(p)
|
|
if w.windows {
|
|
return strings.HasPrefix(p, `\\`) || reDriveAbs.MatchString(p)
|
|
}
|
|
return strings.HasPrefix(p, "/")
|
|
}
|
|
|
|
func (w flavour) hasRoot(p string) bool {
|
|
p = w.normalizeSeparators(p)
|
|
if w.windows {
|
|
return strings.HasPrefix(p, `\`) || reDrive.MatchString(p)
|
|
}
|
|
return strings.HasPrefix(p, "/")
|
|
}
|
|
|
|
func (w flavour) dirname(p string) string {
|
|
p = w.safeTrimTrailingSeparator(p)
|
|
if w.windows && reUNCRoot.MatchString(p) {
|
|
return p
|
|
}
|
|
result := w.nodeDirname(p)
|
|
if w.windows && reUNCRootSlash.MatchString(result) {
|
|
result = w.safeTrimTrailingSeparator(result)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ensureAbsoluteRoot resolves Windows drive and root relative paths against cwd like process.cwd().
|
|
func (w flavour) ensureAbsoluteRoot(root, itemPath, cwd string) (string, error) {
|
|
if root == "" || itemPath == "" {
|
|
return "", errors.New("ensureAbsoluteRoot parameters must not be empty")
|
|
}
|
|
if w.hasAbsoluteRoot(itemPath) {
|
|
return itemPath, nil
|
|
}
|
|
if w.windows {
|
|
if reDriveRel.MatchString(itemPath) {
|
|
if !reDriveAbs.MatchString(cwd) {
|
|
return "", errors.New("expected current directory to start with an absolute drive root, actual '" + cwd + "'")
|
|
}
|
|
if strings.EqualFold(itemPath[:1], cwd[:1]) {
|
|
if len(itemPath) == 2 {
|
|
return itemPath[:1] + `:\` + cwd[3:], nil
|
|
}
|
|
if !strings.HasSuffix(cwd, `\`) {
|
|
cwd += `\`
|
|
}
|
|
return itemPath[:1] + `:\` + cwd[3:] + itemPath[2:], nil
|
|
}
|
|
return itemPath[:1] + `:\` + itemPath[2:], nil
|
|
} else if reRelRoot.MatchString(w.normalizeSeparators(itemPath)) {
|
|
if !reDriveAbs.MatchString(cwd) {
|
|
return "", errors.New("expected current directory to start with an absolute drive root, actual '" + cwd + "'")
|
|
}
|
|
return cwd[:1] + `:\` + itemPath[1:], nil
|
|
}
|
|
}
|
|
if !w.hasAbsoluteRoot(root) {
|
|
return "", errors.New("ensureAbsoluteRoot parameter 'root' must have an absolute root")
|
|
}
|
|
if !strings.HasSuffix(root, "/") && (!w.windows || !strings.HasSuffix(root, `\`)) {
|
|
root += w.sep()
|
|
}
|
|
return root + itemPath, nil
|
|
}
|
|
|
|
// splitPath ports internal-path.ts Path(string).segments.
|
|
func (w flavour) splitPath(p string) []string {
|
|
p = w.safeTrimTrailingSeparator(p)
|
|
if !w.hasRoot(p) {
|
|
return strings.Split(p, w.sep())
|
|
}
|
|
var segments []string
|
|
remaining := p
|
|
for dir := w.dirname(remaining); dir != remaining; dir = w.dirname(remaining) {
|
|
segments = append([]string{w.nodeBasename(remaining)}, segments...)
|
|
remaining = dir
|
|
}
|
|
return append([]string{remaining}, segments...)
|
|
}
|
|
|
|
// joinSegments ports internal-path.ts Path(string[]).toString().
|
|
func (w flavour) joinSegments(segments []string) string {
|
|
result := w.normalizeSeparators(segments[0])
|
|
if w.hasRoot(result) {
|
|
result = w.safeTrimTrailingSeparator(result)
|
|
}
|
|
skipSlash := strings.HasSuffix(result, w.sep()) || (w.windows && reDriveOnly.MatchString(result))
|
|
for _, s := range segments[1:] {
|
|
if skipSlash {
|
|
skipSlash = false
|
|
} else {
|
|
result += w.sep()
|
|
}
|
|
result += w.normalizeSeparators(s)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func isDeviceRoot(c byte) bool {
|
|
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
|
|
}
|
|
|
|
// nodeDirname ports Node's path.posix.dirname and path.win32.dirname.
|
|
func (w flavour) nodeDirname(p string) string {
|
|
n := len(p)
|
|
if n == 0 {
|
|
return "."
|
|
}
|
|
if !w.windows {
|
|
hasRoot := p[0] == '/'
|
|
end := -1
|
|
matchedSlash := true
|
|
for i := n - 1; i >= 1; i-- {
|
|
if p[i] == '/' {
|
|
if !matchedSlash {
|
|
end = i
|
|
break
|
|
}
|
|
} else {
|
|
matchedSlash = false
|
|
}
|
|
}
|
|
switch {
|
|
case end == -1 && hasRoot:
|
|
return "/"
|
|
case end == -1:
|
|
return "."
|
|
case hasRoot && end == 1:
|
|
return "//"
|
|
}
|
|
return p[:end]
|
|
}
|
|
if n == 1 {
|
|
if w.isSep(p[0]) {
|
|
return p
|
|
}
|
|
return "."
|
|
}
|
|
rootEnd, offset := -1, 0
|
|
if w.isSep(p[0]) {
|
|
rootEnd, offset = 1, 1
|
|
if w.isSep(p[1]) {
|
|
j := 2
|
|
last := j
|
|
for j < n && !w.isSep(p[j]) {
|
|
j++
|
|
}
|
|
if j < n && j != last {
|
|
last = j
|
|
for j < n && w.isSep(p[j]) {
|
|
j++
|
|
}
|
|
if j < n && j != last {
|
|
last = j
|
|
for j < n && !w.isSep(p[j]) {
|
|
j++
|
|
}
|
|
if j == n {
|
|
return p
|
|
}
|
|
if j != last {
|
|
rootEnd, offset = j+1, j+1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if isDeviceRoot(p[0]) && p[1] == ':' {
|
|
rootEnd = 2
|
|
if n > 2 && w.isSep(p[2]) {
|
|
rootEnd = 3
|
|
}
|
|
offset = rootEnd
|
|
}
|
|
end := -1
|
|
matchedSlash := true
|
|
for i := n - 1; i >= offset; i-- {
|
|
if w.isSep(p[i]) {
|
|
if !matchedSlash {
|
|
end = i
|
|
break
|
|
}
|
|
} else {
|
|
matchedSlash = false
|
|
}
|
|
}
|
|
if end == -1 {
|
|
if rootEnd == -1 {
|
|
return "."
|
|
}
|
|
end = rootEnd
|
|
}
|
|
return p[:end]
|
|
}
|
|
|
|
// nodeBasename ports Node's path.posix.basename and path.win32.basename without the ext argument.
|
|
func (w flavour) nodeBasename(p string) string {
|
|
start := 0
|
|
if w.windows && len(p) >= 2 && isDeviceRoot(p[0]) && p[1] == ':' {
|
|
start = 2
|
|
}
|
|
from, end := start, -1
|
|
matchedSlash := true
|
|
for i := len(p) - 1; i >= start; i-- {
|
|
if w.isSep(p[i]) {
|
|
if !matchedSlash {
|
|
from = i + 1
|
|
break
|
|
}
|
|
} else if end == -1 {
|
|
matchedSlash = false
|
|
end = i + 1
|
|
}
|
|
}
|
|
if end == -1 {
|
|
return ""
|
|
}
|
|
return p[from:end]
|
|
}
|