gitea源码

url.go 4.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package url
  4. import (
  5. "context"
  6. "fmt"
  7. "net"
  8. stdurl "net/url"
  9. "strings"
  10. "code.gitea.io/gitea/modules/httplib"
  11. "code.gitea.io/gitea/modules/setting"
  12. "code.gitea.io/gitea/modules/util"
  13. )
  14. // ErrWrongURLFormat represents an error with wrong url format
  15. type ErrWrongURLFormat struct {
  16. URL string
  17. }
  18. func (err ErrWrongURLFormat) Error() string {
  19. return fmt.Sprintf("git URL %s format is wrong", err.URL)
  20. }
  21. // GitURL represents a git URL
  22. type GitURL struct {
  23. *stdurl.URL
  24. extraMark int // 0: standard URL with scheme, 1: scp short syntax (no scheme), 2: file path with no prefix
  25. }
  26. // String returns the URL's string
  27. func (u *GitURL) String() string {
  28. switch u.extraMark {
  29. case 0:
  30. return u.URL.String()
  31. case 1:
  32. return fmt.Sprintf("%s@%s:%s", u.User.Username(), u.Host, u.Path)
  33. case 2:
  34. return u.Path
  35. default:
  36. return ""
  37. }
  38. }
  39. // ParseGitURL parse all kinds of git URL:
  40. // * Full URL: http://git@host/path, http://git@host:port/path
  41. // * SCP short syntax: git@host:/path
  42. // * File path: /dir/repo/path
  43. func ParseGitURL(remote string) (*GitURL, error) {
  44. if strings.Contains(remote, "://") {
  45. u, err := stdurl.Parse(remote)
  46. if err != nil {
  47. return nil, err
  48. }
  49. return &GitURL{URL: u}, nil
  50. } else if strings.Contains(remote, "@") && strings.Contains(remote, ":") {
  51. url := stdurl.URL{
  52. Scheme: "ssh",
  53. }
  54. squareBrackets := false
  55. lastIndex := -1
  56. FOR:
  57. for i := 0; i < len(remote); i++ {
  58. switch remote[i] {
  59. case '@':
  60. url.User = stdurl.User(remote[:i])
  61. lastIndex = i + 1
  62. case ':':
  63. if !squareBrackets {
  64. url.Host = strings.ReplaceAll(remote[lastIndex:i], "%25", "%")
  65. if len(remote) <= i+1 {
  66. return nil, ErrWrongURLFormat{URL: remote}
  67. }
  68. url.Path = remote[i+1:]
  69. break FOR
  70. }
  71. case '[':
  72. squareBrackets = true
  73. case ']':
  74. squareBrackets = false
  75. }
  76. }
  77. return &GitURL{
  78. URL: &url,
  79. extraMark: 1,
  80. }, nil
  81. }
  82. return &GitURL{
  83. URL: &stdurl.URL{
  84. Scheme: "file",
  85. Path: remote,
  86. },
  87. extraMark: 2,
  88. }, nil
  89. }
  90. type RepositoryURL struct {
  91. GitURL *GitURL
  92. // if the URL belongs to current Gitea instance, then the below fields have values
  93. OwnerName string
  94. RepoName string
  95. RemainingPath string
  96. }
  97. // ParseRepositoryURL tries to parse a Git URL and extract the owner/repository name if it belongs to current Gitea instance.
  98. func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, error) {
  99. // possible urls for git:
  100. // https://my.domain/sub-path/<owner>/<repo>[.git]
  101. // git+ssh://user@my.domain/<owner>/<repo>[.git]
  102. // ssh://user@my.domain/<owner>/<repo>[.git]
  103. // user@my.domain:<owner>/<repo>[.git]
  104. parsed, err := ParseGitURL(repoURL)
  105. if err != nil {
  106. return nil, err
  107. }
  108. ret := &RepositoryURL{}
  109. ret.GitURL = parsed
  110. fillPathParts := func(s string) {
  111. s = strings.TrimPrefix(s, "/")
  112. fields := strings.SplitN(s, "/", 3)
  113. if len(fields) >= 2 {
  114. ret.OwnerName = fields[0]
  115. ret.RepoName = strings.TrimSuffix(fields[1], ".git")
  116. if len(fields) == 3 {
  117. ret.RemainingPath = "/" + fields[2]
  118. }
  119. }
  120. }
  121. switch parsed.URL.Scheme {
  122. case "http", "https":
  123. if !httplib.IsCurrentGiteaSiteURL(ctx, repoURL) {
  124. return ret, nil
  125. }
  126. fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL))
  127. case "ssh", "git+ssh":
  128. domainSSH := setting.SSH.Domain
  129. domainCur := httplib.GuessCurrentHostDomain(ctx)
  130. urlDomain, _, _ := net.SplitHostPort(parsed.URL.Host)
  131. urlDomain = util.IfZero(urlDomain, parsed.URL.Host)
  132. if urlDomain == "" {
  133. return ret, nil
  134. }
  135. // check whether URL domain is the App domain
  136. domainMatches := domainSSH == urlDomain
  137. // check whether URL domain is current domain from context
  138. domainMatches = domainMatches || (domainCur != "" && domainCur == urlDomain)
  139. if domainMatches {
  140. fillPathParts(parsed.URL.Path)
  141. }
  142. }
  143. return ret, nil
  144. }
  145. // MakeRepositoryWebLink generates a web link (http/https) for a git repository (by guessing sometimes)
  146. func MakeRepositoryWebLink(repoURL *RepositoryURL) string {
  147. if repoURL.OwnerName != "" {
  148. return setting.AppSubURL + "/" + repoURL.OwnerName + "/" + repoURL.RepoName
  149. }
  150. // now, let's guess, for example:
  151. // * git@github.com:owner/submodule.git
  152. // * https://github.com/example/submodule1.git
  153. switch repoURL.GitURL.Scheme {
  154. case "http", "https":
  155. return strings.TrimSuffix(repoURL.GitURL.String(), ".git")
  156. case "ssh", "git+ssh":
  157. hostname, _, _ := net.SplitHostPort(repoURL.GitURL.Host)
  158. hostname = util.IfZero(hostname, repoURL.GitURL.Host)
  159. urlPath := strings.TrimSuffix(repoURL.GitURL.Path, ".git")
  160. urlPath = strings.TrimPrefix(urlPath, "/")
  161. urlFull := fmt.Sprintf("https://%s/%s", hostname, urlPath)
  162. urlFull = strings.TrimSuffix(urlFull, "/")
  163. return urlFull
  164. }
  165. return ""
  166. }