A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
1package readme
2
3import (
4 "fmt"
5 "net/url"
6 "strings"
7)
8
9// Platform represents a supported Git hosting platform
10type Platform string
11
12const (
13 PlatformGitHub Platform = "github"
14 PlatformGitLab Platform = "gitlab"
15 PlatformTangled Platform = "tangled"
16)
17
18// ParseSourceURL extracts platform, user, and repo from a source repository URL.
19// Returns ok=false if the URL is not a recognized pattern.
20func ParseSourceURL(sourceURL string) (platform Platform, user, repo string, ok bool) {
21 if sourceURL == "" {
22 return "", "", "", false
23 }
24
25 parsed, err := url.Parse(sourceURL)
26 if err != nil {
27 return "", "", "", false
28 }
29
30 // Normalize: remove trailing slash and .git suffix
31 path := strings.TrimSuffix(parsed.Path, "/")
32 path = strings.TrimSuffix(path, ".git")
33 path = strings.TrimPrefix(path, "/")
34
35 if path == "" {
36 return "", "", "", false
37 }
38
39 host := strings.ToLower(parsed.Host)
40
41 switch {
42 case host == "github.com":
43 // GitHub: github.com/{user}/{repo}
44 parts := strings.SplitN(path, "/", 3)
45 if len(parts) < 2 || parts[0] == "" || parts[1] == "" {
46 return "", "", "", false
47 }
48 return PlatformGitHub, parts[0], parts[1], true
49
50 case host == "gitlab.com":
51 // GitLab: gitlab.com/{user}/{repo} or gitlab.com/{group}/{subgroup}/{repo}
52 // For nested groups, user = everything except last part, repo = last part
53 lastSlash := strings.LastIndex(path, "/")
54 if lastSlash == -1 || lastSlash == 0 {
55 return "", "", "", false
56 }
57 user = path[:lastSlash]
58 repo = path[lastSlash+1:]
59 if user == "" || repo == "" {
60 return "", "", "", false
61 }
62 return PlatformGitLab, user, repo, true
63
64 case host == "tangled.org" || host == "tangled.sh":
65 // Tangled: tangled.org/{user}/{repo} or tangled.sh/@{user}/{repo} (legacy)
66 // Strip leading @ from user if present
67 path = strings.TrimPrefix(path, "@")
68 parts := strings.SplitN(path, "/", 3)
69 if len(parts) < 2 || parts[0] == "" || parts[1] == "" {
70 return "", "", "", false
71 }
72 return PlatformTangled, parts[0], parts[1], true
73
74 default:
75 return "", "", "", false
76 }
77}
78
79// DeriveReadmeURL converts a source repository URL to a raw README URL.
80// Returns empty string if platform is not supported.
81func DeriveReadmeURL(sourceURL, branch string) string {
82 platform, user, repo, ok := ParseSourceURL(sourceURL)
83 if !ok {
84 return ""
85 }
86
87 switch platform {
88 case PlatformGitHub:
89 // https://raw.githubusercontent.com/{user}/{repo}/refs/heads/{branch}/README.md
90 return fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/refs/heads/%s/README.md", user, repo, branch)
91
92 case PlatformGitLab:
93 // https://gitlab.com/{user}/{repo}/-/raw/{branch}/README.md
94 return fmt.Sprintf("https://gitlab.com/%s/%s/-/raw/%s/README.md", user, repo, branch)
95
96 case PlatformTangled:
97 // https://tangled.org/{user}/{repo}/raw/{branch}/README.md
98 return fmt.Sprintf("https://tangled.org/%s/%s/raw/%s/README.md", user, repo, branch)
99
100 default:
101 return ""
102 }
103}