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