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
55 changes: 41 additions & 14 deletions internal/swift/swift.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package swift

import (
"encoding/json"
"github.com/git-pkgs/manifests/internal/core"
"net/url"
"regexp"
"strings"

"github.com/git-pkgs/manifests/internal/core"
)

func init() {
Expand Down Expand Up @@ -56,7 +58,7 @@ func (p *packageSwiftParser) Parse(filename string, content []byte) (*core.Resul
version = match[versionGroup]
}

name := extractSwiftPackageName(url)
name := swiftSourceCoordinate(url)
if name == "" || seen[name] {
continue
}
Expand All @@ -74,16 +76,38 @@ func (p *packageSwiftParser) Parse(filename string, content []byte) (*core.Resul
return &core.Result{Name: selfName, Dependencies: deps}, nil
}

// extractSwiftPackageName extracts the package name from a git URL.
func extractSwiftPackageName(url string) string {
// Remove .git suffix
url = strings.TrimSuffix(url, ".git")
func swiftSourceCoordinate(rawURL string) string {
if strings.Contains(rawURL, "://") {
parsed, err := url.Parse(rawURL)
if err != nil || parsed.Hostname() == "" {
return ""
}

host := strings.ToLower(parsed.Hostname())
if parsed.Port() != "" {
host += ":" + parsed.Port()
}
return joinSwiftSourceCoordinate(host, parsed.Path)
}

// Get last path component
if idx := strings.LastIndex(url, "/"); idx >= 0 {
return url[idx+1:]
separator := strings.IndexByte(rawURL, ':')
if separator <= 0 {
return ""
}
return url
host := rawURL[:separator]
if at := strings.LastIndexByte(host, '@'); at >= 0 {
host = host[at+1:]
}
return joinSwiftSourceCoordinate(strings.ToLower(host), rawURL[separator+1:])
}

func joinSwiftSourceCoordinate(host, path string) string {
path = strings.Trim(strings.TrimSpace(path), "/")
path = strings.TrimSuffix(path, ".git")
if host == "" || path == "" {
return ""
}
return host + "/" + path
}

// packageResolvedParser parses Package.resolved files.
Expand Down Expand Up @@ -112,6 +136,7 @@ type packageResolvedV2 struct {

type packageResolvedPinV2 struct {
Identity string `json:"identity"`
Kind string `json:"kind"`
Location string `json:"location"`
State struct {
Version string `json:"version"`
Expand Down Expand Up @@ -147,9 +172,9 @@ func parsePackageResolvedV1(filename string, content []byte) ([]core.Dependency,

var deps []core.Dependency
for _, pin := range resolved.Object.Pins {
name := pin.Package
name := swiftSourceCoordinate(pin.RepositoryURL)
if name == "" {
name = extractSwiftPackageName(pin.RepositoryURL)
name = pin.Package
}

deps = append(deps, core.Dependency{
Expand All @@ -172,8 +197,10 @@ func parsePackageResolvedV2(filename string, content []byte) ([]core.Dependency,
var deps []core.Dependency
for _, pin := range resolved.Pins {
name := pin.Identity
if name == "" {
name = extractSwiftPackageName(pin.Location)
if pin.Kind == "remoteSourceControl" {
if coordinate := swiftSourceCoordinate(pin.Location); coordinate != "" {
name = coordinate
}
}

deps = append(deps, core.Dependency{
Expand Down
37 changes: 25 additions & 12 deletions internal/swift/swift_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ func TestPackageSwift(t *testing.T) {
depMap[d.Name] = d
}

// All 3 packages (extracted from git URLs)
expected := []string{
"vapor",
"Tasks",
"Environment",
"github.com/qutheory/vapor",
"github.com/czechboy0/Tasks",
"github.com/czechboy0/Environment",
}

for _, name := range expected {
Expand Down Expand Up @@ -63,9 +62,8 @@ func TestPackageResolved(t *testing.T) {
depMap[d.Name] = d
}

// Check Yams
if dep, ok := depMap["Yams"]; !ok {
t.Error("expected Yams dependency")
if dep, ok := depMap["github.com/jpsim/Yams"]; !ok {
t.Error("expected github.com/jpsim/Yams dependency")
} else if dep.Version != "5.0.1" {
t.Errorf("Yams version = %q, want %q", dep.Version, "5.0.1")
}
Expand All @@ -83,19 +81,19 @@ func TestPackageResolvedV2(t *testing.T) {
t.Fatalf("Parse failed: %v", err)
}

if len(res.Dependencies) != 2 {
t.Fatalf("expected 2 dependencies, got %d", len(res.Dependencies))
if len(res.Dependencies) != 3 {
t.Fatalf("expected 3 dependencies, got %d", len(res.Dependencies))
}

depMap := make(map[string]core.Dependency)
for _, d := range res.Dependencies {
depMap[d.Name] = d
}

// All 2 packages with versions
expected := map[string]string{
"cryptoswift": "1.6.0",
"swift-docc-plugin": "1.0.0",
"github.com/krzyzanowskim/CryptoSwift": "1.6.0",
"github.com/apple/swift-docc-plugin": "1.0.0",
"apple.swift-argument-parser": "1.2.3",
}

for name, wantVer := range expected {
Expand All @@ -109,3 +107,18 @@ func TestPackageResolvedV2(t *testing.T) {
}
}
}

func TestSwiftSourceCoordinate(t *testing.T) {
tests := map[string]string{
"https://github.com/apple/swift-argument-parser.git": "github.com/apple/swift-argument-parser",
"ssh://git@github.com/apple/swift-nio.git": "github.com/apple/swift-nio",
"git@github.com:apple/swift-log.git": "github.com/apple/swift-log",
"../local-package": "",
}

for rawURL, want := range tests {
if got := swiftSourceCoordinate(rawURL); got != want {
t.Errorf("swiftSourceCoordinate(%q) = %q, want %q", rawURL, got, want)
}
}
}
29 changes: 29 additions & 0 deletions manifests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,35 @@ func TestPURL(t *testing.T) {
t.Error("express dependency not found")
}

func TestSwiftSourcePURLs(t *testing.T) {
content, err := os.ReadFile("testdata/swift/Package.resolved.2")
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
result, err := Parse("Package.resolved", content)
if err != nil {
t.Fatalf("Parse: %v", err)
}
want := map[string]string{
"github.com/krzyzanowskim/CryptoSwift": "pkg:swift/github.com/krzyzanowskim/CryptoSwift@1.6.0",
"github.com/apple/swift-docc-plugin": "pkg:swift/github.com/apple/swift-docc-plugin@1.0.0",
"apple.swift-argument-parser": "",
}
if len(result.Dependencies) != len(want) {
t.Fatalf("Dependencies has %d entries, want %d: %+v", len(result.Dependencies), len(want), result.Dependencies)
}
for _, dependency := range result.Dependencies {
wantPURL, ok := want[dependency.Name]
if !ok {
t.Errorf("unexpected dependency: %+v", dependency)
continue
}
if dependency.PURL != wantPURL {
t.Errorf("%s PURL = %q, want %q", dependency.Name, dependency.PURL, wantPURL)
}
}
}

func TestParsePEP508ParenthesizedRequirements(t *testing.T) {
content, err := os.ReadFile("testdata/pypi/pep508-parenthesized/pyproject.toml")
if err != nil {
Expand Down
10 changes: 9 additions & 1 deletion testdata/swift/Package.resolved.2

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.