first-commit
This commit is contained in:
183
modules/git/url/url.go
Normal file
183
modules/git/url/url.go
Normal file
@@ -0,0 +1,183 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package url
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
stdurl "net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/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 Gitea 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 Gitea instance.
|
||||
func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, error) {
|
||||
// possible urls for git:
|
||||
// https://my.domain/sub-path/<owner>/<repo>[.git]
|
||||
// git+ssh://user@my.domain/<owner>/<repo>[.git]
|
||||
// ssh://user@my.domain/<owner>/<repo>[.git]
|
||||
// user@my.domain:<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.IsCurrentGiteaSiteURL(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:
|
||||
// * git@github.com: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 ""
|
||||
}
|
267
modules/git/url/url_test.go
Normal file
267
modules/git/url/url_test.go
Normal file
@@ -0,0 +1,267 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package url
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseGitURLs(t *testing.T) {
|
||||
kases := []struct {
|
||||
kase string
|
||||
expected *GitURL
|
||||
}{
|
||||
{
|
||||
kase: "git@127.0.0.1:go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "127.0.0.1",
|
||||
Path: "go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
kase: "git@[fe80:14fc:cec5:c174:d88%2510]:go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "[fe80:14fc:cec5:c174:d88%10]",
|
||||
Path: "go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
kase: "git@[::1]:go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "[::1]",
|
||||
Path: "go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
kase: "git@github.com:go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
kase: "ssh://git@github.com/go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "/go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
kase: "ssh://git@[::1]/go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "[::1]",
|
||||
Path: "/go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
kase: "/repositories/go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "file",
|
||||
Path: "/repositories/go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
kase: "file:///repositories/go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "file",
|
||||
Path: "/repositories/go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
kase: "https://github.com/go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "github.com",
|
||||
Path: "/go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
kase: "https://git:git@github.com/go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "github.com",
|
||||
User: url.UserPassword("git", "git"),
|
||||
Path: "/go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
kase: "https://[fe80:14fc:cec5:c174:d88%2510]:20/go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "[fe80:14fc:cec5:c174:d88%10]:20",
|
||||
Path: "/go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 0,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
kase: "git://github.com/go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "git",
|
||||
Host: "github.com",
|
||||
Path: "/go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, kase := range kases {
|
||||
t.Run(kase.kase, func(t *testing.T) {
|
||||
u, err := ParseGitURL(kase.kase)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, kase.expected.extraMark, u.extraMark)
|
||||
assert.Equal(t, *kase.expected, *u)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRepositoryURL(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000")()
|
||||
defer test.MockVariableValue(&setting.SSH.Domain, "try.gitea.io")()
|
||||
|
||||
ctxURL, _ := url.Parse("https://gitea")
|
||||
ctxReq := &http.Request{URL: ctxURL, Header: http.Header{}}
|
||||
ctxReq.Host = ctxURL.Host
|
||||
ctxReq.Header.Add("X-Forwarded-Proto", ctxURL.Scheme)
|
||||
ctx := context.WithValue(t.Context(), httplib.RequestContextKey, ctxReq)
|
||||
cases := []struct {
|
||||
input string
|
||||
ownerName, repoName, remaining string
|
||||
}{
|
||||
{input: "/user/repo"},
|
||||
|
||||
{input: "https://localhost:3000/user/repo", ownerName: "user", repoName: "repo"},
|
||||
{input: "https://external:3000/user/repo"},
|
||||
|
||||
{input: "https://localhost:3000/user/repo.git/other", ownerName: "user", repoName: "repo", remaining: "/other"},
|
||||
|
||||
{input: "https://gitea/user/repo", ownerName: "user", repoName: "repo"},
|
||||
{input: "https://gitea:3333/user/repo"},
|
||||
|
||||
{input: "ssh://try.gitea.io:2222/user/repo", ownerName: "user", repoName: "repo"},
|
||||
{input: "ssh://external:2222/user/repo"},
|
||||
|
||||
{input: "git+ssh://user@try.gitea.io/user/repo.git", ownerName: "user", repoName: "repo"},
|
||||
{input: "git+ssh://user@external/user/repo.git"},
|
||||
|
||||
{input: "root@try.gitea.io:user/repo.git", ownerName: "user", repoName: "repo"},
|
||||
{input: "root@gitea:user/repo.git", ownerName: "user", repoName: "repo"},
|
||||
{input: "root@external:user/repo.git"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.input, func(t *testing.T) {
|
||||
ret, _ := ParseRepositoryURL(ctx, c.input)
|
||||
assert.Equal(t, c.ownerName, ret.OwnerName)
|
||||
assert.Equal(t, c.repoName, ret.RepoName)
|
||||
assert.Equal(t, c.remaining, ret.RemainingPath)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("WithSubpath", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000/subpath")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "/subpath")()
|
||||
cases = []struct {
|
||||
input string
|
||||
ownerName, repoName, remaining string
|
||||
}{
|
||||
{input: "https://localhost:3000/user/repo"},
|
||||
{input: "https://localhost:3000/subpath/user/repo.git/other", ownerName: "user", repoName: "repo", remaining: "/other"},
|
||||
|
||||
{input: "ssh://try.gitea.io:2222/user/repo", ownerName: "user", repoName: "repo"},
|
||||
{input: "ssh://external:2222/user/repo"},
|
||||
|
||||
{input: "git+ssh://user@try.gitea.io/user/repo.git", ownerName: "user", repoName: "repo"},
|
||||
{input: "git+ssh://user@external/user/repo.git"},
|
||||
|
||||
{input: "root@try.gitea.io:user/repo.git", ownerName: "user", repoName: "repo"},
|
||||
{input: "root@external:user/repo.git"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.input, func(t *testing.T) {
|
||||
ret, _ := ParseRepositoryURL(ctx, c.input)
|
||||
assert.Equal(t, c.ownerName, ret.OwnerName)
|
||||
assert.Equal(t, c.repoName, ret.RepoName)
|
||||
assert.Equal(t, c.remaining, ret.RemainingPath)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMakeRepositoryBaseLink(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000/subpath")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "/subpath")()
|
||||
|
||||
u, err := ParseRepositoryURL(t.Context(), "https://localhost:3000/subpath/user/repo.git")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/subpath/user/repo", MakeRepositoryWebLink(u))
|
||||
|
||||
u, err = ParseRepositoryURL(t.Context(), "https://github.com/owner/repo.git")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/owner/repo", MakeRepositoryWebLink(u))
|
||||
|
||||
u, err = ParseRepositoryURL(t.Context(), "git@github.com:owner/repo.git")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/owner/repo", MakeRepositoryWebLink(u))
|
||||
|
||||
u, err = ParseRepositoryURL(t.Context(), "git+ssh://other:123/owner/repo.git")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://other/owner/repo", MakeRepositoryWebLink(u))
|
||||
}
|
Reference in New Issue
Block a user