mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
184 lines
4.7 KiB
Go
184 lines
4.7 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package url
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
stdurl "net/url"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/modules/httplib"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/util"
|
|
)
|
|
|
|
// ErrWrongURLFormat represents an error with wrong url format
|
|
type ErrWrongURLFormat struct {
|
|
URL string
|
|
}
|
|
|
|
func (err ErrWrongURLFormat) Error() string {
|
|
return fmt.Sprintf("git URL %s format is wrong", err.URL)
|
|
}
|
|
|
|
// GitURL represents a git URL
|
|
type GitURL struct {
|
|
*stdurl.URL
|
|
extraMark int // 0: standard URL with scheme, 1: scp short syntax (no scheme), 2: file path with no prefix
|
|
}
|
|
|
|
// String returns the URL's string
|
|
func (u *GitURL) String() string {
|
|
switch u.extraMark {
|
|
case 0:
|
|
return u.URL.String()
|
|
case 1:
|
|
return fmt.Sprintf("%s@%s:%s", u.User.Username(), u.Host, u.Path)
|
|
case 2:
|
|
return u.Path
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// ParseGitURL parse all kinds of git URL:
|
|
// * Full URL: http://git@host/path, http://git@host:port/path
|
|
// * SCP short syntax: git@host:/path
|
|
// * File path: /dir/repo/path
|
|
func ParseGitURL(remote string) (*GitURL, error) {
|
|
if strings.Contains(remote, "://") {
|
|
u, err := stdurl.Parse(remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &GitURL{URL: u}, nil
|
|
} else if strings.Contains(remote, "@") && strings.Contains(remote, ":") {
|
|
url := stdurl.URL{
|
|
Scheme: "ssh",
|
|
}
|
|
squareBrackets := false
|
|
lastIndex := -1
|
|
FOR:
|
|
for i := 0; i < len(remote); i++ {
|
|
switch remote[i] {
|
|
case '@':
|
|
url.User = stdurl.User(remote[:i])
|
|
lastIndex = i + 1
|
|
case ':':
|
|
if !squareBrackets {
|
|
url.Host = strings.ReplaceAll(remote[lastIndex:i], "%25", "%")
|
|
if len(remote) <= i+1 {
|
|
return nil, ErrWrongURLFormat{URL: remote}
|
|
}
|
|
url.Path = remote[i+1:]
|
|
break FOR
|
|
}
|
|
case '[':
|
|
squareBrackets = true
|
|
case ']':
|
|
squareBrackets = false
|
|
}
|
|
}
|
|
return &GitURL{
|
|
URL: &url,
|
|
extraMark: 1,
|
|
}, nil
|
|
}
|
|
|
|
return &GitURL{
|
|
URL: &stdurl.URL{
|
|
Scheme: "file",
|
|
Path: remote,
|
|
},
|
|
extraMark: 2,
|
|
}, nil
|
|
}
|
|
|
|
type RepositoryURL struct {
|
|
GitURL *GitURL
|
|
|
|
// if the URL belongs to current forge instance, then the below fields have values
|
|
OwnerName string
|
|
RepoName string
|
|
RemainingPath string
|
|
}
|
|
|
|
// ParseRepositoryURL tries to parse a Git URL and extract the owner/repository name if it belongs to current forge instance.
|
|
func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, error) {
|
|
// possible urls for git:
|
|
// https://my.domain/sub-path/<owner>/<repo>[.git]
|
|
// git+ssh://[email protected]/<owner>/<repo>[.git]
|
|
// ssh://[email protected]/<owner>/<repo>[.git]
|
|
// [email protected]:<owner>/<repo>[.git]
|
|
parsed, err := ParseGitURL(repoURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ret := &RepositoryURL{}
|
|
ret.GitURL = parsed
|
|
|
|
fillPathParts := func(s string) {
|
|
s = strings.TrimPrefix(s, "/")
|
|
fields := strings.SplitN(s, "/", 3)
|
|
if len(fields) >= 2 {
|
|
ret.OwnerName = fields[0]
|
|
ret.RepoName = strings.TrimSuffix(fields[1], ".git")
|
|
if len(fields) == 3 {
|
|
ret.RemainingPath = "/" + fields[2]
|
|
}
|
|
}
|
|
}
|
|
|
|
switch parsed.URL.Scheme {
|
|
case "http", "https":
|
|
if !httplib.IsCurrentGitSiteURL(ctx, repoURL) {
|
|
return ret, nil
|
|
}
|
|
fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL))
|
|
case "ssh", "git+ssh":
|
|
domainSSH := setting.SSH.Domain
|
|
domainCur := httplib.GuessCurrentHostDomain(ctx)
|
|
urlDomain, _, _ := net.SplitHostPort(parsed.URL.Host)
|
|
urlDomain = util.IfZero(urlDomain, parsed.URL.Host)
|
|
if urlDomain == "" {
|
|
return ret, nil
|
|
}
|
|
// check whether URL domain is the App domain
|
|
domainMatches := domainSSH == urlDomain
|
|
// check whether URL domain is current domain from context
|
|
domainMatches = domainMatches || (domainCur != "" && domainCur == urlDomain)
|
|
if domainMatches {
|
|
fillPathParts(parsed.URL.Path)
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// MakeRepositoryWebLink generates a web link (http/https) for a git repository (by guessing sometimes)
|
|
func MakeRepositoryWebLink(repoURL *RepositoryURL) string {
|
|
if repoURL.OwnerName != "" {
|
|
return setting.AppSubURL + "/" + repoURL.OwnerName + "/" + repoURL.RepoName
|
|
}
|
|
|
|
// now, let's guess, for example:
|
|
// * [email protected]:owner/submodule.git
|
|
// * https://github.com/example/submodule1.git
|
|
switch repoURL.GitURL.Scheme {
|
|
case "http", "https":
|
|
return strings.TrimSuffix(repoURL.GitURL.String(), ".git")
|
|
case "ssh", "git+ssh":
|
|
hostname, _, _ := net.SplitHostPort(repoURL.GitURL.Host)
|
|
hostname = util.IfZero(hostname, repoURL.GitURL.Host)
|
|
urlPath := strings.TrimSuffix(repoURL.GitURL.Path, ".git")
|
|
urlPath = strings.TrimPrefix(urlPath, "/")
|
|
urlFull := fmt.Sprintf("https://%s/%s", hostname, urlPath)
|
|
urlFull = strings.TrimSuffix(urlFull, "/")
|
|
return urlFull
|
|
}
|
|
return ""
|
|
}
|