Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Go sources and scripts are committed with LF; a Windows checkout must not get CRLF (gofmt, sh).
* text=auto eol=lf
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
Expand All @@ -32,6 +35,13 @@ jobs:
- name: Install mercurial and subversion (macOS)
if: runner.os == 'macOS'
run: brew install mercurial subversion
- name: Install mercurial and subversion (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
choco install -y --no-progress hg sliksvn
Add-Content $env:GITHUB_PATH 'C:\Program Files\Mercurial'
Add-Content $env:GITHUB_PATH 'C:\Program Files\SlikSvn\bin'
- name: Versions
run: go version && git --version && hg --version | head -1 && svn --version --quiet
- name: Build
Expand Down
1 change: 1 addition & 0 deletions changes/unreleased/windows-paths.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- On Windows, a repository on this machine is accepted as `git+file:///C:/path` (and `git+file://C:/path`), `file://` resources with a drive letter resolve, and the suite runs on Windows; CI gates on Windows with hg and svn installed.
10 changes: 7 additions & 3 deletions internal/install/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -297,7 +298,7 @@ func TestLockMatchesAcrossGitSuffixSpellings(t *testing.T) {
}
first, _ := os.ReadFile(filepath.Join(dir, project.LockFile))
// An older lock recorded the source in its input spelling; it is still the same location.
oldSrc := strings.ReplaceAll(string(first), `"source": "`+strings.ReplaceAll(filepath.FromSlash(widgets), `\\`, `\\\\`)+`"`, `"source": "git+file://`+filepath.ToSlash(widgets)+`"`)
oldSrc := strings.ReplaceAll(string(first), `"source": "`+strings.ReplaceAll(filepath.FromSlash(widgets), "\\", "\\\\")+`"`, `"source": "git+file://`+filepath.ToSlash(widgets)+`"`)
if oldSrc == string(first) {
t.Fatalf("lock did not contain the widgets source:\n%s", first)
}
Expand Down Expand Up @@ -549,13 +550,16 @@ func TestPublishRollsBackWhenTheLockCannotBeWritten(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("root ignores directory permissions")
}
if runtime.GOOS == "windows" {
t.Skip("a read-only directory does not refuse new files on Windows")
}
Comment on lines +553 to +555

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Windows rollback loses its test

On Windows, TestPublishRollsBackWhenTheLockCannotBeWritten skips instead of testing rollback. The Windows gate cannot catch a failed lock write leaving lib/ inconsistent with the lock.

Learn more

The rollback test simulates a lock write failure by changing directory permissions. Windows does not enforce that permission change in the same way, so the new skip removes rollback coverage precisely on the newly gated platform. Run publishes packages before writing the lock and invokes rollback if writing fails.

Example: On Windows, a regression in lock-write failure handling can leave lib/ containing version 1.1.0 while the on-disk lock still names 1.0.0; the CI test exits as skipped.

Recommended fix: Replace the permissions-based failure setup on Windows with a deterministic lock-write failure that still lets lib/ be updated, then assert the old lock and installed tree remain intact. Retain the existing POSIX setup if needed.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. #6 removes the skip: Windows ignores a read-only directory but refuses to replace a read-only file, so there the test makes .project.lock.json itself 0444; lock.Write's final os.Rename then fails after lib/ was published and the same rollback path is asserted (message "Access is denied" instead of "permission denied"). The Windows gate on #6 is the proof.

widgets, _ := fixture(t)
dir := consumer(t, project.Usage{Resource: "git+file://" + widgets + "@1.0.0"})
dir := consumer(t, project.Usage{Resource: "git+file://" + filepath.ToSlash(widgets) + "@1.0.0"})
run(t, dir, Options{})
before, _ := os.ReadFile(filepath.Join(dir, project.LockFile))
// Move to 1.1.0 (replaces the installed tree) with the project directory read-only,
// so the lock cannot be written after lib/ was updated.
write(t, dir, project.ProjectFile, `{"name":"app","version":"0.1.0","usage":[{"resource":"git+file://`+widgets+`@1.1.0"}]}`)
write(t, dir, project.ProjectFile, `{"name":"app","version":"0.1.0","usage":[{"resource":"git+file://`+filepath.ToSlash(widgets)+`@1.1.0"}]}`)
if err := os.Chmod(dir, 0o555); err != nil {
t.Fatal(err)
}
Expand Down
48 changes: 43 additions & 5 deletions internal/origin/origin.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ func Parse(resource string) (*Origin, error) {
return parseHTTP(res)
}
if rest, ok := strings.CutPrefix(res, "file://"); ok {
if i := strings.IndexByte(rest, '/'); i > 0 {
rest, ok = fileURIPath(rest)
if !ok {
return nil, fmt.Errorf("resource %q: file URIs must name a path on this machine (file:///path)", res)
}
return parseLocal(res, rest)
Expand Down Expand Up @@ -270,7 +271,8 @@ func parseGeneric(res, tail string) (*Origin, error) {
// git+https://<host>/<path>.
func parseGitAlias(res, tail string) (*Origin, error) {
if strings.HasPrefix(tail, "/") {
return nil, fmt.Errorf("resource %q: pkg:git needs a host; a repository on this machine is git+file:///%s", res, strings.TrimLeft(tail, "/"))
abs, _ := filepath.Abs(filepath.FromSlash("/" + strings.TrimLeft(tail, "/")))
return nil, fmt.Errorf("resource %q: pkg:git needs a host; a repository on this machine is git+file://%s", res, FileURIPath(abs))
}
if tail == "" || !strings.Contains(tail, "/") {
return nil, fmt.Errorf("resource %q: expected pkg:git/<host>/<path>[@<version>][#<subpath>]", res)
Expand Down Expand Up @@ -299,6 +301,12 @@ func parseVCSURL(res, raw string) (*Origin, error) {
if !transports[vcs][transport] {
return nil, fmt.Errorf("resource %q: %s+%s is not a vcs_url scheme for %s (ECMA-427 Annex B.3.5 allows %s)", res, vcs, transport, vcs, allowed(vcs))
}
if transport == "file" {
// file:///C:/path is the RFC 8089 form; file://C:/path is accepted as input.
if drive.MatchString(rest) {
rest = "/" + rest
}
Comment on lines +304 to +308

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Windows file URI contract remains undocumented

file://C:/path becomes accepted input, but the documented resource grammar lacks the Windows forms. CONTRIBUTING.md requires documentation updates when a format or contract changes.

Learn more

Example: A Windows user reading the resource grammar finds <vcs>+file:///… but no explanation of the drive-letter spelling accepted by the parser.

Recommended fix: Document the accepted Windows file:///C:/path and file://C:/path forms and their canonical identity in README.md or docs/packages.md, including plain local file:// resources.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in #6: docs/packages.md and the README now name <vcs>+file:///C:/path as the canonical Windows form, <vcs>+file://C:/path as accepted input normalised to it, the same two forms for a local .kpar or project directory, and that /C:/path is an ordinary directory on other systems.

}
u, err := url.Parse(transport + "://" + rest)
if err != nil {
return nil, fmt.Errorf("resource %q: not a valid URL: %v", res, err)
Expand All @@ -314,11 +322,15 @@ func parseVCSURL(res, raw string) (*Origin, error) {
if u.Host != "" || !strings.HasPrefix(u.Path, "/") {
return nil, fmt.Errorf("resource %q: a repository on this machine is %s+file:///absolute/path", res, vcs)
}
o.Path = filepath.ToSlash(filepath.Clean(u.Path))
o.Path = u.Path
if drive.MatchString(o.Path[1:]) {
o.Path = o.Path[1:]
}
o.Path = filepath.ToSlash(filepath.Clean(o.Path))
o.Name = strings.TrimSuffix(path.Base(o.Path), ".git")
switch vcs {
case "svn":
o.Source = "file://" + o.Path
o.Source = "file://" + FileURIPath(o.Path)
default:
o.Source = filepath.FromSlash(o.Path)
}
Expand Down Expand Up @@ -387,6 +399,32 @@ func parseHTTP(res string) (*Origin, error) {
return &Origin{Kind: kind, Source: spec, Version: ver, Resource: res}, nil
}

// drive matches the start of a Windows path, C:/ or C:\.
var drive = regexp.MustCompile(`^[A-Za-z]:[/\\]`)

// FileURIPath is the path part of a file:// URI naming abs: /C:/dir on Windows, /dir elsewhere.
func FileURIPath(abs string) string {
abs = filepath.ToSlash(abs)
if !strings.HasPrefix(abs, "/") {
abs = "/" + abs
}
return abs
}

// fileURIPath turns what follows file:// into a path on this machine; a host is refused.
func fileURIPath(rest string) (string, bool) {
if drive.MatchString(rest) {
return rest, true
}
if !strings.HasPrefix(rest, "/") {
return "", false
}
if drive.MatchString(rest[1:]) {
return rest[1:], true
Comment on lines +422 to +423

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Absolute local paths become relative

On POSIX, file:///C:/tools.kpar becomes C:/tools.kpar in fileURIPath. The archive loader resolves it under the project instead of /C:/tools.kpar, so an existing archive fails to install.

Learn more

A file URI with a first directory named C: is a valid absolute path on POSIX. The new drive check removes its leading slash without checking the host OS. resolveLocalArchive then joins the resulting relative path with the consuming project's root; parseVCSURL also removes the slash for repositories.

Example: On Linux, a real archive at /C:/tools.kpar is referenced as file:///C:/tools.kpar. Parsing yields C:/tools.kpar, so install looks for <project>/C:/tools.kpar and fails.

Recommended fix: Apply the drive normalization only on Windows in both fileURIPath and parseVCSURL; leave POSIX paths starting /C:/ absolute. Add a POSIX regression test for both local files and VCS sources.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: on POSIX a directory named C: is legal and fileURIPath dropped its leading slash. #6 makes the drive check Windows-only (isDrivePath = windowsPaths && drive.MatchString) and adds TestDriveLetterFileURIs, which exercises both machines from one test file (file:///C:/tools.kpar → /C:/tools.kpar on POSIX, file://C:/x refused there).

}
return rest, true
}

// parseLocal handles file:// URIs and plain paths: a .kpar is a local
// archive, anything else an editable project directory.
func parseLocal(res, p string) (*Origin, error) {
Expand All @@ -406,7 +444,7 @@ func parseLocal(res, p string) (*Origin, error) {
}
if ver != "" {
abs, _ := filepath.Abs(p)
return nil, fmt.Errorf("resource %q: a path with @<version> is not an identity; a repository on this machine is git+file://%s@%s (or svn+file, hg+file)", res, filepath.ToSlash(abs), ver)
return nil, fmt.Errorf("resource %q: a path with @<version> is not an identity; a repository on this machine is git+file://%s@%s (or svn+file, hg+file)", res, FileURIPath(abs), ver)
}
if sub != "" {
p = path.Join(p, sub)
Expand Down
12 changes: 8 additions & 4 deletions internal/origin/origin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package origin

import (
"path/filepath"
"strings"
"testing"
)
Expand All @@ -26,7 +27,7 @@ func TestCanonical(t *testing.T) {
{"svn://svn.example.org/pkg@1.0.0", "pkg:generic/pkg?vcs_url=svn%2Bsvn:%2F%2Fsvn.example.org%2Fpkg%401.0.0", "svn://svn.example.org/pkg"},
{"svn+https://svn.example.org/repos/pkg", "pkg:generic/pkg?vcs_url=svn%2Bhttps:%2F%2Fsvn.example.org%2Frepos%2Fpkg", "https://svn.example.org/repos/pkg"},
{"svn+ssh://svn.example.org/repos/pkg", "pkg:generic/pkg?vcs_url=svn%2Bssh:%2F%2Fsvn.example.org%2Frepos%2Fpkg", "svn+ssh://svn.example.org/repos/pkg"},
{"git+file:///srv/git/widgets@1.0.0#packages/gears", "pkg:generic/widgets?vcs_url=git%2Bfile:%2F%2F%2Fsrv%2Fgit%2Fwidgets%401.0.0%23packages%2Fgears", "/srv/git/widgets"},
{"git+file:///srv/git/widgets@1.0.0#packages/gears", "pkg:generic/widgets?vcs_url=git%2Bfile:%2F%2F%2Fsrv%2Fgit%2Fwidgets%401.0.0%23packages%2Fgears", filepath.FromSlash("/srv/git/widgets")},
{"svn+file:///srv/svn/widgets", "pkg:generic/widgets?vcs_url=svn%2Bfile:%2F%2F%2Fsrv%2Fsvn%2Fwidgets", "file:///srv/svn/widgets"},
} {
o, err := Parse(tc.in)
Expand Down Expand Up @@ -67,6 +68,9 @@ func TestArchivesAndDirectories(t *testing.T) {
}
}

// srvGitWidgets is /srv/git/widgets as a file-URI path on this machine (/D:/srv/git/widgets on Windows).
var srvGitWidgets = func() string { abs, _ := filepath.Abs(filepath.FromSlash("/srv/git/widgets")); return FileURIPath(abs) }()

func TestRefused(t *testing.T) {
for _, tc := range []struct{ in, want string }{
{"pkg:svn/svn.example.org/pkg@1.0.0", "pkg:svn is not a Package-URL type; write pkg:generic/<name>?vcs_url="},
Expand All @@ -78,9 +82,9 @@ func TestRefused(t *testing.T) {
{"https://github.com/acme/widgets.git", "only dereferenced as a project interchange file"},
{"git@github.com:acme/widgets.git@1.0.0", "write git+ssh://github.com/acme/widgets"},
{"ssh://git@github.com/acme/widgets@1.0.0", "unsupported URL scheme; a repository is written as pkg:generic"},
{"file:///srv/git/widgets@1.0.0", "a repository on this machine is git+file:///srv/git/widgets@1.0.0"},
{"/srv/git/widgets@1.0.0", "a repository on this machine is git+file:///srv/git/widgets@1.0.0"},
{"pkg:git//srv/git/widgets@1.0.0", "a repository on this machine is git+file:///srv/git/widgets@1.0.0"},
{"file:///srv/git/widgets@1.0.0", "a repository on this machine is git+file://" + srvGitWidgets + "@1.0.0"},
{"/srv/git/widgets@1.0.0", "a repository on this machine is git+file://" + srvGitWidgets + "@1.0.0"},
{"pkg:git//srv/git/widgets@1.0.0", "a repository on this machine is git+file://" + srvGitWidgets + "@1.0.0"},
{"git+https://user:pw@github.com/acme/widgets", "user name or password in the host is not supported (ECMA-427 Annex B.3.5)"},
{"hg://hg.example.org/repo", "hg+hg is not a vcs_url scheme for hg (ECMA-427 Annex B.3.5 allows hg+http, hg+https, hg+static-http, hg+ssh, hg+file)"},
{"git+ftp://host/repo", "git+ftp is not a vcs_url scheme for git"},
Expand Down
4 changes: 2 additions & 2 deletions internal/vcs/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ func (g *gitSource) Export(ctx context.Context, v Version, dst string) error {
}
defer os.RemoveAll(scratch)
clone := filepath.Join(scratch, "repo")
if _, err := run(ctx, "", "git", "-c", "advice.detachedHead=false", "clone", "--quiet", "--depth", "1", "--branch", v.Tag, "--", g.url, clone); err != nil {
if _, err := run(ctx, "", "git", "-c", "advice.detachedHead=false", "-c", "core.autocrlf=false", "clone", "--quiet", "--depth", "1", "--branch", v.Tag, "--", g.url, clone); err != nil {
if !strings.Contains(err.Error(), "shallow") {
return err
}
// A dumb HTTP remote cannot serve a shallow clone; take the full history.
os.RemoveAll(clone)
if _, err := run(ctx, "", "git", "-c", "advice.detachedHead=false", "clone", "--quiet", "--branch", v.Tag, "--", g.url, clone); err != nil {
if _, err := run(ctx, "", "git", "-c", "advice.detachedHead=false", "-c", "core.autocrlf=false", "clone", "--quiet", "--branch", v.Tag, "--", g.url, clone); err != nil {
return err
}
}
Expand Down
12 changes: 7 additions & 5 deletions internal/vcs/hg.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ func (h *hgSource) ListVersions(ctx context.Context) ([]Version, error) {
}
defer os.RemoveAll(scratch)
clone := filepath.Join(scratch, "repo")
if _, err := run(ctx, "", "hg", "clone", "--quiet", "--noupdate", "--", h.url, clone); err != nil {
// --noninteractive: hg would otherwise prompt for HTTP credentials (and on Windows
// takes the NUL stdin for a terminal); the abort then names the authorization.
if _, err := run(ctx, "", "hg", "--noninteractive", "clone", "--quiet", "--noupdate", "--", h.url, clone); err != nil {
return nil, err
}
out, err := run(ctx, clone, "hg", "tags", "--quiet")
out, err := run(ctx, clone, "hg", "--noninteractive", "tags", "--quiet")
if err != nil {
return nil, err
}
Expand All @@ -44,7 +46,7 @@ func (h *hgSource) ListVersions(ctx context.Context) ([]Version, error) {

// Revision is the 40-hex changeset node id the tag points at.
func (h *hgSource) Revision(ctx context.Context, v Version) (string, error) {
out, err := run(ctx, "", "hg", "identify", "--id", "--debug", "-r", v.Tag, "--", h.url)
out, err := run(ctx, "", "hg", "--noninteractive", "identify", "--id", "--debug", "-r", v.Tag, "--", h.url)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -76,13 +78,13 @@ func (h *hgSource) Export(ctx context.Context, v Version, dst string) error {
}
defer os.RemoveAll(scratch)
clone := filepath.Join(scratch, "repo")
if _, err := run(ctx, "", "hg", "clone", "--quiet", "--noupdate", "-r", node, "--", h.url, clone); err != nil {
if _, err := run(ctx, "", "hg", "--noninteractive", "clone", "--quiet", "--noupdate", "-r", node, "--", h.url, clone); err != nil {
return err
}
// --no-decode keeps bytes as committed. The .hg_archival.txt stamp and the
// .hgtags bookkeeping file are Mercurial's own, not the package's, and are
// excluded so the tree hash matches the same package served from git or svn.
if _, err := run(ctx, clone, "hg", "archive", "--quiet", "--type", "files", "-r", node, "--no-decode", "--exclude", ".hg_archival.txt", "--exclude", ".hgtags", dst); err != nil {
if _, err := run(ctx, clone, "hg", "--noninteractive", "archive", "--quiet", "--type", "files", "-r", node, "--no-decode", "--exclude", ".hg_archival.txt", "--exclude", ".hgtags", dst); err != nil {
return err
}
return nil
Expand Down
3 changes: 2 additions & 1 deletion internal/vcs/svn.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ func svnAuthArgsFor(repo string, lookup func(string) (netrc.Credential, bool), f
if !ok || c.Login == "" || !fromStdin {
return nil, nil
}
return []string{"--username", c.Login, "--password-from-stdin", "--no-auth-cache"}, strings.NewReader(c.Password + "\n")
// No trailing newline: svn on Windows keeps a bare LF as part of the password.
return []string{"--username", c.Login, "--password-from-stdin", "--no-auth-cache"}, strings.NewReader(c.Password)
}

// svn runs the user's svn with the netrc credentials for the repository
Expand Down
2 changes: 1 addition & 1 deletion internal/vcs/svn_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestSvnAuthArgsFor(t *testing.T) {
if strings.Join(args, " ") != "--username alice --password-from-stdin --no-auth-cache" {
t.Fatalf("args %q", args)
}
if pw, _ := io.ReadAll(stdin); string(pw) != "s3cret\n" {
if pw, _ := io.ReadAll(stdin); string(pw) != "s3cret" {
t.Fatalf("stdin %q", pw)
}
for _, a := range args {
Expand Down
11 changes: 10 additions & 1 deletion internal/vcs/vcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,21 @@ func hgFixture(t *testing.T) string {
return repo
}

// fileURL is the file:// URL of an absolute path on this machine (file:///C:/x on Windows).
func fileURL(p string) string {
p = filepath.ToSlash(p)
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
return "file://" + p
}

func svnFixture(t *testing.T) string {
t.Helper()
root := t.TempDir()
repo := filepath.Join(root, "repo")
sh(t, root, nil, "svnadmin", "create", repo)
url := "file://" + filepath.ToSlash(repo)
url := fileURL(repo)
stage := filepath.Join(root, "stage")
os.MkdirAll(filepath.Join(stage, "trunk"), 0o755)
os.MkdirAll(filepath.Join(stage, "tags"), 0o755)
Expand Down
17 changes: 11 additions & 6 deletions tests/e2e/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestAuthGitCredentialHelper(t *testing.T) {
// With a credential helper configured through the environment, as a user would.
helper := filepath.Join(root, "helper.sh")
os.WriteFile(helper, []byte("#!/bin/sh\necho username=alice\necho password=s3cret\n"), 0o755)
env := []string{"GIT_CONFIG_COUNT=1", "GIT_CONFIG_KEY_0=credential.helper", "GIT_CONFIG_VALUE_0=" + helper}
env := []string{"GIT_CONFIG_COUNT=1", "GIT_CONFIG_KEY_0=credential.helper", "GIT_CONFIG_VALUE_0=" + filepath.ToSlash(helper)}
app2 := initApp(t, root, "app-cred")
if out, err := shErr(app2, env, bin, "add", res); err != nil {
t.Fatalf("add with helper: %v\n%s", err, out)
Expand Down Expand Up @@ -120,11 +120,16 @@ func TestAuthSvnNetrc(t *testing.T) {
requireOrSkip(t, "svn", "svnadmin", "svnserve")
bin := syspiBinary(t)
root := t.TempDir()
url := svnFixture(t, root)
repo := strings.TrimPrefix(url, "file://")
conf := filepath.Join(repo, "conf")
os.WriteFile(filepath.Join(conf, "svnserve.conf"), []byte("[general]\nanon-access = none\nauth-access = read\npassword-db = passwd\nrealm = pkg\n"), 0o644)
os.WriteFile(filepath.Join(conf, "passwd"), []byte("[users]\nalice = s3cret\n"), 0o644)
svnFixture(t, root)
conf := filepath.Join(root, "widgets-svn", "conf")
for name, content := range map[string]string{
"svnserve.conf": "[general]\nanon-access = none\nauth-access = read\npassword-db = passwd\nrealm = pkg\n",
"passwd": "[users]\nalice = s3cret\n",
} {
if err := os.WriteFile(filepath.Join(conf, name), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
port := freePort(t)
pid := filepath.Join(root, "svnserve.pid")
cmd := exec.Command("svnserve", "-d", "--foreground", "--listen-host", "127.0.0.1", "--listen-port", strconv.Itoa(port), "-r", root, "--pid-file", pid)
Expand Down
17 changes: 15 additions & 2 deletions tests/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
Expand All @@ -36,6 +37,9 @@ func syspiBinary(t *testing.T) string {
return
}
binPath = filepath.Join(dir, "syspi")
if runtime.GOOS == "windows" {
binPath += ".exe"
}
cmd := exec.Command("go", "build", "-o", binPath, "../../cmd/syspi")
if out, err := cmd.CombinedOutput(); err != nil {
buildErr = &buildFailure{err, string(out)}
Expand Down Expand Up @@ -145,10 +149,19 @@ func hgFixture(t *testing.T, root string) string {
return repo
}

// fileURL is the file:// URL of an absolute path on this machine (file:///C:/x on Windows).
func fileURL(p string) string {
p = filepath.ToSlash(p)
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
return "file://" + p
}

func svnFixture(t *testing.T, root string) string {
repo := filepath.Join(root, "widgets-svn")
sh(t, root, "svnadmin", "create", repo)
url := "file://" + filepath.ToSlash(repo)
url := fileURL(repo)
stage := filepath.Join(root, "svn-stage")
os.MkdirAll(filepath.Join(stage, "tags"), 0o755)
packageTree(t, filepath.Join(stage, "trunk"), "1.0.0")
Expand Down Expand Up @@ -383,7 +396,7 @@ func TestRefusals(t *testing.T) {
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
app := filepath.Join(root, "app-"+strings.ReplaceAll(tc.name, " ", "-"))
app := filepath.Join(root, "app-"+strings.NewReplacer(" ", "-", ":", "-", "/", "-").Replace(tc.name))
os.MkdirAll(app, 0o755)
sh(t, app, bin, "init", "-name", "app")
before, _ := os.ReadFile(filepath.Join(app, ".project.json"))
Expand Down
Loading