mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-09 16:50:28 +00:00
feat: port the path patterns of @actions/glob (#1342)
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]>
This commit is contained in:
1 parent
bb6d793cb9
commit
b88d111cc5
5 files changed
+1542
No files matched your search
@@ -0,0 +1,284 @@
|
||||
// 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]
|
||||
}
|
||||
Reference in new issue
Block a user