repos_contents.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. // Copyright 2013 The go-github 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.
  5. // Repository contents API methods.
  6. // GitHub API docs: https://developer.github.com/v3/repos/contents/
  7. package github
  8. import (
  9. "context"
  10. "encoding/base64"
  11. "encoding/json"
  12. "fmt"
  13. "io"
  14. "net/http"
  15. "net/url"
  16. "path"
  17. )
  18. // RepositoryContent represents a file or directory in a github repository.
  19. type RepositoryContent struct {
  20. Type *string `json:"type,omitempty"`
  21. Encoding *string `json:"encoding,omitempty"`
  22. Size *int `json:"size,omitempty"`
  23. Name *string `json:"name,omitempty"`
  24. Path *string `json:"path,omitempty"`
  25. // Content contains the actual file content, which may be encoded.
  26. // Callers should call GetContent which will decode the content if
  27. // necessary.
  28. Content *string `json:"content,omitempty"`
  29. SHA *string `json:"sha,omitempty"`
  30. URL *string `json:"url,omitempty"`
  31. GitURL *string `json:"git_url,omitempty"`
  32. HTMLURL *string `json:"html_url,omitempty"`
  33. DownloadURL *string `json:"download_url,omitempty"`
  34. }
  35. // RepositoryContentResponse holds the parsed response from CreateFile, UpdateFile, and DeleteFile.
  36. type RepositoryContentResponse struct {
  37. Content *RepositoryContent `json:"content,omitempty"`
  38. Commit `json:"commit,omitempty"`
  39. }
  40. // RepositoryContentFileOptions specifies optional parameters for CreateFile, UpdateFile, and DeleteFile.
  41. type RepositoryContentFileOptions struct {
  42. Message *string `json:"message,omitempty"`
  43. Content []byte `json:"content,omitempty"` // unencoded
  44. SHA *string `json:"sha,omitempty"`
  45. Branch *string `json:"branch,omitempty"`
  46. Author *CommitAuthor `json:"author,omitempty"`
  47. Committer *CommitAuthor `json:"committer,omitempty"`
  48. }
  49. // RepositoryContentGetOptions represents an optional ref parameter, which can be a SHA,
  50. // branch, or tag
  51. type RepositoryContentGetOptions struct {
  52. Ref string `url:"ref,omitempty"`
  53. }
  54. // String converts RepositoryContent to a string. It's primarily for testing.
  55. func (r RepositoryContent) String() string {
  56. return Stringify(r)
  57. }
  58. // GetContent returns the content of r, decoding it if necessary.
  59. func (r *RepositoryContent) GetContent() (string, error) {
  60. var encoding string
  61. if r.Encoding != nil {
  62. encoding = *r.Encoding
  63. }
  64. switch encoding {
  65. case "base64":
  66. c, err := base64.StdEncoding.DecodeString(*r.Content)
  67. return string(c), err
  68. case "":
  69. if r.Content == nil {
  70. return "", nil
  71. }
  72. return *r.Content, nil
  73. default:
  74. return "", fmt.Errorf("unsupported content encoding: %v", encoding)
  75. }
  76. }
  77. // GetReadme gets the Readme file for the repository.
  78. //
  79. // GitHub API docs: https://developer.github.com/v3/repos/contents/#get-the-readme
  80. func (s *RepositoriesService) GetReadme(ctx context.Context, owner, repo string, opt *RepositoryContentGetOptions) (*RepositoryContent, *Response, error) {
  81. u := fmt.Sprintf("repos/%v/%v/readme", owner, repo)
  82. u, err := addOptions(u, opt)
  83. if err != nil {
  84. return nil, nil, err
  85. }
  86. req, err := s.client.NewRequest("GET", u, nil)
  87. if err != nil {
  88. return nil, nil, err
  89. }
  90. readme := new(RepositoryContent)
  91. resp, err := s.client.Do(ctx, req, readme)
  92. if err != nil {
  93. return nil, resp, err
  94. }
  95. return readme, resp, nil
  96. }
  97. // DownloadContents returns an io.ReadCloser that reads the contents of the
  98. // specified file. This function will work with files of any size, as opposed
  99. // to GetContents which is limited to 1 Mb files. It is the caller's
  100. // responsibility to close the ReadCloser.
  101. func (s *RepositoriesService) DownloadContents(ctx context.Context, owner, repo, filepath string, opt *RepositoryContentGetOptions) (io.ReadCloser, error) {
  102. dir := path.Dir(filepath)
  103. filename := path.Base(filepath)
  104. _, dirContents, _, err := s.GetContents(ctx, owner, repo, dir, opt)
  105. if err != nil {
  106. return nil, err
  107. }
  108. for _, contents := range dirContents {
  109. if *contents.Name == filename {
  110. if contents.DownloadURL == nil || *contents.DownloadURL == "" {
  111. return nil, fmt.Errorf("No download link found for %s", filepath)
  112. }
  113. resp, err := s.client.client.Get(*contents.DownloadURL)
  114. if err != nil {
  115. return nil, err
  116. }
  117. return resp.Body, nil
  118. }
  119. }
  120. return nil, fmt.Errorf("No file named %s found in %s", filename, dir)
  121. }
  122. // GetContents can return either the metadata and content of a single file
  123. // (when path references a file) or the metadata of all the files and/or
  124. // subdirectories of a directory (when path references a directory). To make it
  125. // easy to distinguish between both result types and to mimic the API as much
  126. // as possible, both result types will be returned but only one will contain a
  127. // value and the other will be nil.
  128. //
  129. // GitHub API docs: https://developer.github.com/v3/repos/contents/#get-contents
  130. func (s *RepositoriesService) GetContents(ctx context.Context, owner, repo, path string, opt *RepositoryContentGetOptions) (fileContent *RepositoryContent, directoryContent []*RepositoryContent, resp *Response, err error) {
  131. escapedPath := (&url.URL{Path: path}).String()
  132. u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, escapedPath)
  133. u, err = addOptions(u, opt)
  134. if err != nil {
  135. return nil, nil, nil, err
  136. }
  137. req, err := s.client.NewRequest("GET", u, nil)
  138. if err != nil {
  139. return nil, nil, nil, err
  140. }
  141. var rawJSON json.RawMessage
  142. resp, err = s.client.Do(ctx, req, &rawJSON)
  143. if err != nil {
  144. return nil, nil, resp, err
  145. }
  146. fileUnmarshalError := json.Unmarshal(rawJSON, &fileContent)
  147. if fileUnmarshalError == nil {
  148. return fileContent, nil, resp, nil
  149. }
  150. directoryUnmarshalError := json.Unmarshal(rawJSON, &directoryContent)
  151. if directoryUnmarshalError == nil {
  152. return nil, directoryContent, resp, nil
  153. }
  154. return nil, nil, resp, fmt.Errorf("unmarshalling failed for both file and directory content: %s and %s", fileUnmarshalError, directoryUnmarshalError)
  155. }
  156. // CreateFile creates a new file in a repository at the given path and returns
  157. // the commit and file metadata.
  158. //
  159. // GitHub API docs: https://developer.github.com/v3/repos/contents/#create-a-file
  160. func (s *RepositoriesService) CreateFile(ctx context.Context, owner, repo, path string, opt *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) {
  161. u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path)
  162. req, err := s.client.NewRequest("PUT", u, opt)
  163. if err != nil {
  164. return nil, nil, err
  165. }
  166. createResponse := new(RepositoryContentResponse)
  167. resp, err := s.client.Do(ctx, req, createResponse)
  168. if err != nil {
  169. return nil, resp, err
  170. }
  171. return createResponse, resp, nil
  172. }
  173. // UpdateFile updates a file in a repository at the given path and returns the
  174. // commit and file metadata. Requires the blob SHA of the file being updated.
  175. //
  176. // GitHub API docs: https://developer.github.com/v3/repos/contents/#update-a-file
  177. func (s *RepositoriesService) UpdateFile(ctx context.Context, owner, repo, path string, opt *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) {
  178. u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path)
  179. req, err := s.client.NewRequest("PUT", u, opt)
  180. if err != nil {
  181. return nil, nil, err
  182. }
  183. updateResponse := new(RepositoryContentResponse)
  184. resp, err := s.client.Do(ctx, req, updateResponse)
  185. if err != nil {
  186. return nil, resp, err
  187. }
  188. return updateResponse, resp, nil
  189. }
  190. // DeleteFile deletes a file from a repository and returns the commit.
  191. // Requires the blob SHA of the file to be deleted.
  192. //
  193. // GitHub API docs: https://developer.github.com/v3/repos/contents/#delete-a-file
  194. func (s *RepositoriesService) DeleteFile(ctx context.Context, owner, repo, path string, opt *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) {
  195. u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path)
  196. req, err := s.client.NewRequest("DELETE", u, opt)
  197. if err != nil {
  198. return nil, nil, err
  199. }
  200. deleteResponse := new(RepositoryContentResponse)
  201. resp, err := s.client.Do(ctx, req, deleteResponse)
  202. if err != nil {
  203. return nil, resp, err
  204. }
  205. return deleteResponse, resp, nil
  206. }
  207. // archiveFormat is used to define the archive type when calling GetArchiveLink.
  208. type archiveFormat string
  209. const (
  210. // Tarball specifies an archive in gzipped tar format.
  211. Tarball archiveFormat = "tarball"
  212. // Zipball specifies an archive in zip format.
  213. Zipball archiveFormat = "zipball"
  214. )
  215. // GetArchiveLink returns an URL to download a tarball or zipball archive for a
  216. // repository. The archiveFormat can be specified by either the github.Tarball
  217. // or github.Zipball constant.
  218. //
  219. // GitHub API docs: https://developer.github.com/v3/repos/contents/#get-archive-link
  220. func (s *RepositoriesService) GetArchiveLink(ctx context.Context, owner, repo string, archiveformat archiveFormat, opt *RepositoryContentGetOptions) (*url.URL, *Response, error) {
  221. u := fmt.Sprintf("repos/%s/%s/%s", owner, repo, archiveformat)
  222. if opt != nil && opt.Ref != "" {
  223. u += fmt.Sprintf("/%s", opt.Ref)
  224. }
  225. req, err := s.client.NewRequest("GET", u, nil)
  226. if err != nil {
  227. return nil, nil, err
  228. }
  229. var resp *http.Response
  230. // Use http.DefaultTransport if no custom Transport is configured
  231. req = withContext(ctx, req)
  232. if s.client.client.Transport == nil {
  233. resp, err = http.DefaultTransport.RoundTrip(req)
  234. } else {
  235. resp, err = s.client.client.Transport.RoundTrip(req)
  236. }
  237. if err != nil {
  238. return nil, nil, err
  239. }
  240. resp.Body.Close()
  241. if resp.StatusCode != http.StatusFound {
  242. return nil, newResponse(resp), fmt.Errorf("unexpected status code: %s", resp.Status)
  243. }
  244. parsedURL, err := url.Parse(resp.Header.Get("Location"))
  245. return parsedURL, newResponse(resp), err
  246. }