refactor(ax): dedupe sync repo parsing
Some checks failed
Security Scan / security (push) Failing after 15s
Test / test (push) Failing after 1m37s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 14:20:14 +00:00
parent a14feec8ab
commit 48d1eb22b0
4 changed files with 75 additions and 52 deletions

View file

@ -8,10 +8,10 @@ import (
os "dappco.re/go/core/scm/internal/ax/osx"
strings "dappco.re/go/core/scm/internal/ax/stringsx"
exec "golang.org/x/sys/execabs"
"net/url"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/scm/agentci"
"dappco.re/go/core/scm/cmd/internal/syncutil"
fg "dappco.re/go/core/scm/forge"
forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
@ -99,7 +99,7 @@ func buildSyncRepoList(client *fg.Client, args []string, basePath string) ([]syn
if len(args) > 0 {
for _, arg := range args {
name, err := syncRepoNameFromArg(arg)
name, err := syncutil.ParseRepoName(arg)
if err != nil {
return nil, coreerr.E("forge.buildSyncRepoList", "invalid repo argument", err)
}
@ -347,27 +347,3 @@ func syncCreateMainFromUpstream(client *fg.Client, org, repo string) error {
return nil
}
func syncRepoNameFromArg(arg string) (string, error) {
decoded, err := url.PathUnescape(arg)
if err != nil {
return "", coreerr.E("forge.syncRepoNameFromArg", "decode repo argument", err)
}
parts := strings.Split(decoded, "/")
switch len(parts) {
case 1:
return agentci.ValidatePathElement(parts[0])
case 2:
if _, err := agentci.ValidatePathElement(parts[0]); err != nil {
return "", coreerr.E("forge.syncRepoNameFromArg", "invalid repo owner", err)
}
name, err := agentci.ValidatePathElement(parts[1])
if err != nil {
return "", coreerr.E("forge.syncRepoNameFromArg", "invalid repo name", err)
}
return name, nil
default:
return "", coreerr.E("forge.syncRepoNameFromArg", "repo argument must be repo or owner/repo", nil)
}
}

View file

@ -8,10 +8,10 @@ import (
os "dappco.re/go/core/scm/internal/ax/osx"
strings "dappco.re/go/core/scm/internal/ax/stringsx"
exec "golang.org/x/sys/execabs"
"net/url"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/scm/agentci"
"dappco.re/go/core/scm/cmd/internal/syncutil"
gt "dappco.re/go/core/scm/gitea"
"code.gitea.io/sdk/gitea"
@ -100,7 +100,7 @@ func buildRepoList(client *gt.Client, args []string, basePath string) ([]repoEnt
if len(args) > 0 {
// Specific repos from args
for _, arg := range args {
name, err := repoNameFromArg(arg)
name, err := syncutil.ParseRepoName(arg)
if err != nil {
return nil, coreerr.E("gitea.buildRepoList", "invalid repo argument", err)
}
@ -365,27 +365,3 @@ func createMainFromUpstream(client *gt.Client, org, repo string) error {
}
func strPtr(s string) *string { return &s }
func repoNameFromArg(arg string) (string, error) {
decoded, err := url.PathUnescape(arg)
if err != nil {
return "", coreerr.E("gitea.repoNameFromArg", "decode repo argument", err)
}
parts := strings.Split(decoded, "/")
switch len(parts) {
case 1:
return agentci.ValidatePathElement(parts[0])
case 2:
if _, err := agentci.ValidatePathElement(parts[0]); err != nil {
return "", coreerr.E("gitea.repoNameFromArg", "invalid repo owner", err)
}
name, err := agentci.ValidatePathElement(parts[1])
if err != nil {
return "", coreerr.E("gitea.repoNameFromArg", "invalid repo name", err)
}
return name, nil
default:
return "", coreerr.E("gitea.repoNameFromArg", "repo argument must be repo or owner/repo", nil)
}
}

View file

@ -0,0 +1,37 @@
// SPDX-License-Identifier: EUPL-1.2
package syncutil
import (
"net/url"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/scm/agentci"
strings "dappco.re/go/core/scm/internal/ax/stringsx"
)
// ParseRepoName normalises a sync argument into a validated repo name.
// Usage: ParseRepoName(...)
func ParseRepoName(arg string) (string, error) {
decoded, err := url.PathUnescape(arg)
if err != nil {
return "", coreerr.E("syncutil.ParseRepoName", "decode repo argument", err)
}
parts := strings.Split(decoded, "/")
switch len(parts) {
case 1:
return agentci.ValidatePathElement(parts[0])
case 2:
if _, err := agentci.ValidatePathElement(parts[0]); err != nil {
return "", coreerr.E("syncutil.ParseRepoName", "invalid repo owner", err)
}
name, err := agentci.ValidatePathElement(parts[1])
if err != nil {
return "", coreerr.E("syncutil.ParseRepoName", "invalid repo name", err)
}
return name, nil
default:
return "", coreerr.E("syncutil.ParseRepoName", "repo argument must be repo or owner/repo", nil)
}
}

View file

@ -0,0 +1,34 @@
// SPDX-License-Identifier: EUPL-1.2
package syncutil
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseRepoName_Good(t *testing.T) {
name, err := ParseRepoName("core")
require.NoError(t, err)
assert.Equal(t, "core", name)
}
func TestParseRepoName_Good_OwnerRepo(t *testing.T) {
name, err := ParseRepoName("host-uk/core")
require.NoError(t, err)
assert.Equal(t, "core", name)
}
func TestParseRepoName_Bad_PathTraversal(t *testing.T) {
_, err := ParseRepoName("../escape")
require.Error(t, err)
assert.Contains(t, err.Error(), "syncutil.ParseRepoName")
}
func TestParseRepoName_Bad_PathTraversalEncoded(t *testing.T) {
_, err := ParseRepoName("host-uk%2F..%2Fescape")
require.Error(t, err)
assert.Contains(t, err.Error(), "syncutil.ParseRepoName")
}