// Copyright (C) 2024 Umorpha Systems // SPDX-License-Identifier: AGPL-3.0-or-later package gitcache import ( _url "net/url" "path" "path/filepath" "regexp" "strings" ) // reIsURL mimics git's url.c:is_url() var reIsURL = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9+.-]*://`) // NormalizeURL parses a URL in accordance with the "GIT URLS" // section of git-fetch(1), and normalizes it. func NormalizeURL(url string) (string, bool) { switch { case reIsURL.MatchString(url): u, err := _url.Parse(url) if err != nil { return "", false } // work this left-to-right // 1. scheme switch u.Scheme { case "ssh", "git", "http", "https", "ftp", "ftps", "file": // OK default: return "", false } // 2. userinfo if u.User != nil { if u.Scheme == "ssh" { u.User = _url.User(u.User.Username()) } else { u.User = nil } } // 3. host[:port] if u.Host == "" && u.Scheme != "file" { return "", false } u.Host = strings.ToLower(u.Host) // 4. path u.Path = path.Clean(u.Path) switch u.Path { case ".", "/": u.Path = "" } if u.Host == "" && u.Path == "" { u.Path = "/" } // 5. query switch u.Scheme { case "http", "https": // > Clients MUST strip a trailing `/`, if present, from the // > user supplied `$GIT_URL` string // -- gitprotocol-http(5) // // ... and that might include the query u.RawQuery = strings.TrimRight(u.RawQuery, "/") } // 6. fragment u.Fragment = "" return u.String(), true case filepath.IsAbs(url): u := &_url.URL{ Scheme: "file", Path: filepath.ToSlash(url), } url, ok := NormalizeURL(u.String()) if !ok { panic("should not happen") } return url, true default: // scp-like uauth, upath, ok := strings.Cut(url, ":") if !ok { return "", false } if strings.Contains(uauth, "/") { return "", false } if !path.IsAbs(upath) { if strings.HasPrefix(upath, "~") { upath = "/" + upath } else { upath = "/~/" + upath } } u := &_url.URL{ Scheme: "ssh", Path: path.Clean(upath), } atIdx := strings.LastIndex(uauth, "@") if atIdx < 0 { u.Host = uauth } else { u.User = _url.User(uauth[:atIdx]) u.Host = uauth[atIdx+1:] } return NormalizeURL(u.String()) } }