package site import ( "bytes" "context" "net/url" "path" "strings" ) type submoduleConfig struct { Path string URL string } func (g *Generator) decorateSubmodules(ctx context.Context, oid, dir string, entries []TreeEntry) { repositories := g.submoduleRepositories(ctx, oid) for i := range entries { if entries[i].Type != "commit" { continue } filePath := path.Join(dir, entries[i].Name) if repository := repositories[filePath]; repository != "" { entries[i].SubmoduleURL = submoduleTargetURL(repository, entries[i].OID) } } } func (g *Generator) submoduleRepositories(ctx context.Context, oid string) map[string]string { g.submoduleMu.Lock() if cached, ok := g.submoduleCache[oid]; ok { g.submoduleMu.Unlock() return cached } g.submoduleMu.Unlock() result := map[string]string{} out, err := g.Git.Run(ctx, "config", "-z", "--blob", oid+":.gitmodules", "--get-regexp", `^submodule\..*\.(path|url)$`) if err == nil { configs := map[string]*submoduleConfig{} for _, record := range bytes.Split(out, []byte{0}) { key, value, ok := bytes.Cut(record, []byte{'\n'}) if !ok { continue } name := string(key) var field string switch { case strings.HasSuffix(name, ".path"): name = strings.TrimSuffix(name, ".path") field = "path" case strings.HasSuffix(name, ".url"): name = strings.TrimSuffix(name, ".url") field = "url" default: continue } config := configs[name] if config == nil { config = &submoduleConfig{} configs[name] = config } if field == "path" { config.Path = string(value) } else { config.URL = string(value) } } for _, config := range configs { if config.Path == "" { continue } if repository := browserRepositoryURL(g.CloneURL, config.URL); repository != "" { result[config.Path] = repository } } } g.submoduleMu.Lock() if g.submoduleCache == nil { g.submoduleCache = map[string]map[string]string{} } g.submoduleCache[oid] = result g.submoduleMu.Unlock() return result } func browserRepositoryURL(cloneURL, raw string) string { raw = strings.TrimSpace(strings.TrimPrefix(raw, "git+")) if raw == "" { return "" } if strings.HasPrefix(raw, "./") || strings.HasPrefix(raw, "../") { base, err := url.Parse(strings.TrimRight(cloneURL, "/") + "/") if err != nil || (base.Scheme != "http" && base.Scheme != "https") || base.Host == "" { return "" } reference, err := url.Parse(raw) if err != nil { return "" } return base.ResolveReference(reference).String() } if strings.HasPrefix(raw, "ssh://") { parsed, err := url.Parse(raw) if err != nil || parsed.Host == "" { return "" } parsed.Scheme = "https" parsed.User = nil return parsed.String() } if strings.HasPrefix(raw, "git://") { raw = "https://" + strings.TrimPrefix(raw, "git://") } else if !strings.Contains(raw, "://") { at := strings.IndexByte(raw, '@') colon := strings.IndexByte(raw, ':') if at >= 0 && colon > at { raw = "https://" + raw[at+1:colon] + "/" + raw[colon+1:] } } parsed, err := url.Parse(raw) if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || parsed.User != nil { return "" } parsed.RawQuery = "" parsed.Fragment = "" return parsed.String() } func submoduleTargetURL(repository string, oid string) string { parsed, err := url.Parse(repository) if err != nil || parsed.Host == "" { return repository } host := strings.ToLower(parsed.Hostname()) commitRoute := "" switch host { case "github.com", "codeberg.org", "gitea.com": commitRoute = "commit" case "gitlab.com": commitRoute = "-/commit" case "bitbucket.org": commitRoute = "commits" } if commitRoute == "" { return repository } parsed.Path = strings.TrimSuffix(parsed.Path, ".git") parsed.Path = path.Join(parsed.Path, commitRoute, oid) return parsed.String() }