您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 

345 行
9.3 KiB

  1. // Copyright 2013 The Go Authors. All rights reserved.
  2. //
  3. // Use of this source code is governed by a BSD-style
  4. // license that can be found in the LICENSE file or at
  5. // https://developers.google.com/open-source/licenses/bsd.
  6. package gosrc
  7. import (
  8. "encoding/json"
  9. "fmt"
  10. "net/http"
  11. "net/url"
  12. "regexp"
  13. "strings"
  14. "time"
  15. )
  16. func init() {
  17. addService(&service{
  18. pattern: regexp.MustCompile(`^github\.com/(?P<owner>[a-z0-9A-Z_.\-]+)/(?P<repo>[a-z0-9A-Z_.\-]+)(?P<dir>/.*)?$`),
  19. prefix: "github.com/",
  20. get: getGitHubDir,
  21. getPresentation: getGitHubPresentation,
  22. getProject: getGitHubProject,
  23. })
  24. addService(&service{
  25. pattern: regexp.MustCompile(`^gist\.github\.com/(?P<gist>[a-z0-9A-Z_.\-]+)\.git$`),
  26. prefix: "gist.github.com/",
  27. get: getGistDir,
  28. })
  29. }
  30. var (
  31. gitHubRawHeader = http.Header{"Accept": {"application/vnd.github-blob.raw"}}
  32. gitHubPreviewHeader = http.Header{"Accept": {"application/vnd.github.preview"}}
  33. ownerRepoPat = regexp.MustCompile(`^https://api.github.com/repos/([^/]+)/([^/]+)/`)
  34. )
  35. type githubCommit struct {
  36. ID string `json:"sha"`
  37. Commit struct {
  38. Committer struct {
  39. Date time.Time `json:"date"`
  40. } `json:"committer"`
  41. } `json:"commit"`
  42. }
  43. func gitHubError(resp *http.Response) error {
  44. var e struct {
  45. Message string `json:"message"`
  46. }
  47. if err := json.NewDecoder(resp.Body).Decode(&e); err == nil {
  48. return &RemoteError{resp.Request.URL.Host, fmt.Errorf("%d: %s (%s)", resp.StatusCode, e.Message, resp.Request.URL.String())}
  49. }
  50. return &RemoteError{resp.Request.URL.Host, fmt.Errorf("%d: (%s)", resp.StatusCode, resp.Request.URL.String())}
  51. }
  52. func getGitHubDir(client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
  53. c := &httpClient{client: client, errFn: gitHubError}
  54. var repo struct {
  55. Fork bool `json:"fork"`
  56. Stars int `json:"stargazers_count"`
  57. CreatedAt time.Time `json:"created_at"`
  58. PushedAt time.Time `json:"pushed_at"`
  59. }
  60. if _, err := c.getJSON(expand("https://api.github.com/repos/{owner}/{repo}", match), &repo); err != nil {
  61. return nil, err
  62. }
  63. status := Active
  64. var commits []*githubCommit
  65. url := expand("https://api.github.com/repos/{owner}/{repo}/commits", match)
  66. if match["dir"] != "" {
  67. url += fmt.Sprintf("?path=%s", match["dir"])
  68. }
  69. if _, err := c.getJSON(url, &commits); err != nil {
  70. return nil, err
  71. }
  72. if len(commits) == 0 {
  73. return nil, NotFoundError{Message: "package directory changed or removed"}
  74. }
  75. lastCommitted := commits[0].Commit.Committer.Date
  76. if lastCommitted.Add(ExpiresAfter).Before(time.Now()) {
  77. status = NoRecentCommits
  78. } else if repo.Fork {
  79. if repo.PushedAt.Before(repo.CreatedAt) {
  80. status = DeadEndFork
  81. } else if isQuickFork(commits, repo.CreatedAt) {
  82. status = QuickFork
  83. }
  84. }
  85. if commits[0].ID == savedEtag {
  86. return nil, NotModifiedError{
  87. Since: lastCommitted,
  88. Status: status,
  89. }
  90. }
  91. var contents []*struct {
  92. Type string
  93. Name string
  94. GitURL string `json:"git_url"`
  95. HTMLURL string `json:"html_url"`
  96. }
  97. if _, err := c.getJSON(expand("https://api.github.com/repos/{owner}/{repo}/contents{dir}", match), &contents); err != nil {
  98. // The GitHub content API returns array values for directories
  99. // and object values for files. If there's a type mismatch at
  100. // the beginning of the response, then assume that the path is
  101. // for a file.
  102. if e, ok := err.(*json.UnmarshalTypeError); ok && e.Offset == 1 {
  103. return nil, NotFoundError{Message: "Not a directory"}
  104. }
  105. return nil, err
  106. }
  107. if len(contents) == 0 {
  108. return nil, NotFoundError{Message: "No files in directory."}
  109. }
  110. // GitHub owner and repo names are case-insensitive. Redirect if requested
  111. // names do not match the canonical names in API response.
  112. if m := ownerRepoPat.FindStringSubmatch(contents[0].GitURL); m != nil && (m[1] != match["owner"] || m[2] != match["repo"]) {
  113. match["owner"] = m[1]
  114. match["repo"] = m[2]
  115. return nil, NotFoundError{
  116. Message: "Github import path has incorrect case.",
  117. Redirect: expand("github.com/{owner}/{repo}{dir}", match),
  118. }
  119. }
  120. var files []*File
  121. var dataURLs []string
  122. var subdirs []string
  123. for _, item := range contents {
  124. switch {
  125. case item.Type == "dir":
  126. if isValidPathElement(item.Name) {
  127. subdirs = append(subdirs, item.Name)
  128. }
  129. case isDocFile(item.Name):
  130. files = append(files, &File{Name: item.Name, BrowseURL: item.HTMLURL})
  131. dataURLs = append(dataURLs, item.GitURL)
  132. }
  133. }
  134. c.header = gitHubRawHeader
  135. if err := c.getFiles(dataURLs, files); err != nil {
  136. return nil, err
  137. }
  138. browseURL := expand("https://github.com/{owner}/{repo}", match)
  139. if match["dir"] != "" {
  140. browseURL = expand("https://github.com/{owner}/{repo}/tree{dir}", match)
  141. }
  142. return &Directory{
  143. BrowseURL: browseURL,
  144. Etag: commits[0].ID,
  145. Files: files,
  146. LineFmt: "%s#L%d",
  147. ProjectName: match["repo"],
  148. ProjectRoot: expand("github.com/{owner}/{repo}", match),
  149. ProjectURL: expand("https://github.com/{owner}/{repo}", match),
  150. Subdirectories: subdirs,
  151. VCS: "git",
  152. Status: status,
  153. Fork: repo.Fork,
  154. Stars: repo.Stars,
  155. }, nil
  156. }
  157. // isQuickFork reports whether the repository is a "quick fork":
  158. // it has fewer than 3 commits, all within a week of the repo creation, createdAt.
  159. func isQuickFork(commits []*githubCommit, createdAt time.Time) bool {
  160. oneWeekOld := createdAt.Add(7 * 24 * time.Hour)
  161. if oneWeekOld.After(time.Now()) {
  162. return false // a newborn baby of a repository
  163. }
  164. n := 0
  165. for _, commit := range commits {
  166. if commit.Commit.Committer.Date.After(oneWeekOld) {
  167. return false
  168. }
  169. n++
  170. }
  171. return n < 3
  172. }
  173. func getGitHubPresentation(client *http.Client, match map[string]string) (*Presentation, error) {
  174. c := &httpClient{client: client, header: gitHubRawHeader}
  175. p, err := c.getBytes(expand("https://api.github.com/repos/{owner}/{repo}/contents{dir}/{file}", match))
  176. if err != nil {
  177. return nil, err
  178. }
  179. apiBase, err := url.Parse(expand("https://api.github.com/repos/{owner}/{repo}/contents{dir}/", match))
  180. if err != nil {
  181. return nil, err
  182. }
  183. rawBase, err := url.Parse(expand("https://raw.github.com/{owner}/{repo}/master{dir}/", match))
  184. if err != nil {
  185. return nil, err
  186. }
  187. c.header = gitHubRawHeader
  188. b := &presBuilder{
  189. data: p,
  190. filename: match["file"],
  191. fetch: func(fnames []string) ([]*File, error) {
  192. var files []*File
  193. var dataURLs []string
  194. for _, fname := range fnames {
  195. u, err := apiBase.Parse(fname)
  196. if err != nil {
  197. return nil, err
  198. }
  199. u.RawQuery = apiBase.RawQuery
  200. files = append(files, &File{Name: fname})
  201. dataURLs = append(dataURLs, u.String())
  202. }
  203. err := c.getFiles(dataURLs, files)
  204. return files, err
  205. },
  206. resolveURL: func(fname string) string {
  207. u, err := rawBase.Parse(fname)
  208. if err != nil {
  209. return "/notfound"
  210. }
  211. if strings.HasSuffix(fname, ".svg") {
  212. u.Host = "rawgithub.com"
  213. }
  214. return u.String()
  215. },
  216. }
  217. return b.build()
  218. }
  219. // GetGitHubUpdates returns the full names ("owner/repo") of recently pushed GitHub repositories.
  220. // by pushedAfter.
  221. func GetGitHubUpdates(client *http.Client, pushedAfter string) (maxPushedAt string, names []string, err error) {
  222. c := httpClient{client: client, header: gitHubPreviewHeader}
  223. if pushedAfter == "" {
  224. pushedAfter = time.Now().Add(-24 * time.Hour).UTC().Format("2006-01-02T15:04:05Z")
  225. }
  226. u := "https://api.github.com/search/repositories?order=asc&sort=updated&q=fork:true+language:Go+pushed:>" + pushedAfter
  227. var updates struct {
  228. Items []struct {
  229. FullName string `json:"full_name"`
  230. PushedAt string `json:"pushed_at"`
  231. }
  232. }
  233. _, err = c.getJSON(u, &updates)
  234. if err != nil {
  235. return pushedAfter, nil, err
  236. }
  237. maxPushedAt = pushedAfter
  238. for _, item := range updates.Items {
  239. names = append(names, item.FullName)
  240. if item.PushedAt > maxPushedAt {
  241. maxPushedAt = item.PushedAt
  242. }
  243. }
  244. return maxPushedAt, names, nil
  245. }
  246. func getGitHubProject(client *http.Client, match map[string]string) (*Project, error) {
  247. c := &httpClient{client: client, errFn: gitHubError}
  248. var repo struct {
  249. Description string
  250. }
  251. if _, err := c.getJSON(expand("https://api.github.com/repos/{owner}/{repo}", match), &repo); err != nil {
  252. return nil, err
  253. }
  254. return &Project{
  255. Description: repo.Description,
  256. }, nil
  257. }
  258. func getGistDir(client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
  259. c := &httpClient{client: client, errFn: gitHubError}
  260. var gist struct {
  261. Files map[string]struct {
  262. Content string
  263. }
  264. HTMLURL string `json:"html_url"`
  265. History []struct {
  266. Version string
  267. }
  268. }
  269. if _, err := c.getJSON(expand("https://api.github.com/gists/{gist}", match), &gist); err != nil {
  270. return nil, err
  271. }
  272. if len(gist.History) == 0 {
  273. return nil, NotFoundError{Message: "History not found."}
  274. }
  275. commit := gist.History[0].Version
  276. if commit == savedEtag {
  277. return nil, NotModifiedError{}
  278. }
  279. var files []*File
  280. for name, file := range gist.Files {
  281. if isDocFile(name) {
  282. files = append(files, &File{
  283. Name: name,
  284. Data: []byte(file.Content),
  285. BrowseURL: gist.HTMLURL + "#file-" + strings.Replace(name, ".", "-", -1),
  286. })
  287. }
  288. }
  289. return &Directory{
  290. BrowseURL: gist.HTMLURL,
  291. Etag: commit,
  292. Files: files,
  293. LineFmt: "%s-L%d",
  294. ProjectName: match["gist"],
  295. ProjectRoot: expand("gist.github.com/{gist}.git", match),
  296. ProjectURL: gist.HTMLURL,
  297. Subdirectories: nil,
  298. VCS: "git",
  299. }, nil
  300. }