You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

359 lines
11 KiB

Refactor editor upload, update and delete to use git plumbing and add LFS support (#5702) * Use git plumbing for upload: #5621 repo_editor.go: UploadRepoFile * Use git plumbing for upload: #5621 repo_editor.go: GetDiffPreview * Use git plumbing for upload: #5621 repo_editor.go: DeleteRepoFile * Use git plumbing for upload: #5621 repo_editor.go: UploadRepoFiles * Move branch checkout functions out of repo_editor.go as they are no longer used there * BUGFIX: The default permissions should be 100644 This is a change from the previous code but is more in keeping with the default behaviour of git. Signed-off-by: Andrew Thornton <art27@cantab.net> * Standardise cleanUploadFilename to more closely match git See verify_path in: https://github.com/git/git/blob/7f4e64169352e03476b0ea64e7e2973669e491a2/read-cache.c#L951 Signed-off-by: Andrew Thornton <art27@cantab.net> * Redirect on bad paths Signed-off-by: Andrew Thornton <art27@cantab.net> * Refactor to move the uploading functions out to a module Signed-off-by: Andrew Thornton <art27@cantab.net> * Add LFS support Signed-off-by: Andrew Thornton <art27@cantab.net> * Update upload.go attribution header Upload.go is essentially the remnants of repo_editor.go. The remaining code is essentially unchanged from the Gogs code, hence the Gogs attribution. * Delete upload files after session committed * Ensure that GIT_AUTHOR_NAME etc. are valid for git see #5774 Signed-off-by: Andrew Thornton <art27@cantab.net> * Add in test cases per @lafriks comment * Add space between gitea and github imports Signed-off-by: Andrew Thornton <art27@cantab.net> * more examples in TestCleanUploadName Signed-off-by: Andrew Thornton <art27@cantab.net> * fix formatting Signed-off-by: Andrew Thornton <art27@cantab.net> * Set the SSH_ORIGINAL_COMMAND to ensure hooks are run Signed-off-by: Andrew Thornton <art27@cantab.net> * Switch off SSH_ORIGINAL_COMMAND Signed-off-by: Andrew Thornton <art27@cantab.net>
5 years ago
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package uploader
  5. import (
  6. "bytes"
  7. "context"
  8. "fmt"
  9. "io"
  10. "os"
  11. "os/exec"
  12. "path"
  13. "strings"
  14. "time"
  15. "code.gitea.io/gitea/models"
  16. "code.gitea.io/gitea/modules/process"
  17. "code.gitea.io/gitea/modules/setting"
  18. "github.com/Unknwon/com"
  19. )
  20. // TemporaryUploadRepository is a type to wrap our upload repositories
  21. type TemporaryUploadRepository struct {
  22. repo *models.Repository
  23. basePath string
  24. }
  25. // NewTemporaryUploadRepository creates a new temporary upload repository
  26. func NewTemporaryUploadRepository(repo *models.Repository) (*TemporaryUploadRepository, error) {
  27. timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE
  28. basePath := path.Join(models.LocalCopyPath(), "upload-"+timeStr+".git")
  29. if err := os.MkdirAll(path.Dir(basePath), os.ModePerm); err != nil {
  30. return nil, fmt.Errorf("Failed to create dir %s: %v", basePath, err)
  31. }
  32. t := &TemporaryUploadRepository{repo: repo, basePath: basePath}
  33. return t, nil
  34. }
  35. // Close the repository cleaning up all files
  36. func (t *TemporaryUploadRepository) Close() {
  37. if _, err := os.Stat(t.basePath); !os.IsNotExist(err) {
  38. os.RemoveAll(t.basePath)
  39. }
  40. }
  41. // Clone the base repository to our path and set branch as the HEAD
  42. func (t *TemporaryUploadRepository) Clone(branch string) error {
  43. if _, stderr, err := process.GetManager().ExecTimeout(5*time.Minute,
  44. fmt.Sprintf("Clone (git clone -s --bare): %s", t.basePath),
  45. "git", "clone", "-s", "--bare", "-b", branch, t.repo.RepoPath(), t.basePath); err != nil {
  46. return fmt.Errorf("Clone: %v %s", err, stderr)
  47. }
  48. return nil
  49. }
  50. // SetDefaultIndex sets the git index to our HEAD
  51. func (t *TemporaryUploadRepository) SetDefaultIndex() error {
  52. if _, stderr, err := process.GetManager().ExecDir(5*time.Minute,
  53. t.basePath,
  54. fmt.Sprintf("SetDefaultIndex (git read-tree HEAD): %s", t.basePath),
  55. "git", "read-tree", "HEAD"); err != nil {
  56. return fmt.Errorf("SetDefaultIndex: %v %s", err, stderr)
  57. }
  58. return nil
  59. }
  60. // LsFiles checks if the given filename arguments are in the index
  61. func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, error) {
  62. stdOut := new(bytes.Buffer)
  63. stdErr := new(bytes.Buffer)
  64. timeout := 5 * time.Minute
  65. ctx, cancel := context.WithTimeout(context.Background(), timeout)
  66. defer cancel()
  67. cmdArgs := []string{"ls-files", "-z", "--"}
  68. for _, arg := range filenames {
  69. if arg != "" {
  70. cmdArgs = append(cmdArgs, arg)
  71. }
  72. }
  73. cmd := exec.CommandContext(ctx, "git", cmdArgs...)
  74. desc := fmt.Sprintf("lsFiles: (git ls-files) %v", cmdArgs)
  75. cmd.Dir = t.basePath
  76. cmd.Stdout = stdOut
  77. cmd.Stderr = stdErr
  78. if err := cmd.Start(); err != nil {
  79. return nil, fmt.Errorf("exec(%s) failed: %v(%v)", desc, err, ctx.Err())
  80. }
  81. pid := process.GetManager().Add(desc, cmd)
  82. err := cmd.Wait()
  83. process.GetManager().Remove(pid)
  84. if err != nil {
  85. err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr)
  86. return nil, err
  87. }
  88. filelist := make([]string, len(filenames))
  89. for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) {
  90. filelist = append(filelist, string(line))
  91. }
  92. return filelist, err
  93. }
  94. // RemoveFilesFromIndex removes the given files from the index
  95. func (t *TemporaryUploadRepository) RemoveFilesFromIndex(filenames ...string) error {
  96. stdOut := new(bytes.Buffer)
  97. stdErr := new(bytes.Buffer)
  98. stdIn := new(bytes.Buffer)
  99. for _, file := range filenames {
  100. if file != "" {
  101. stdIn.WriteString("0 0000000000000000000000000000000000000000\t")
  102. stdIn.WriteString(file)
  103. stdIn.WriteByte('\000')
  104. }
  105. }
  106. timeout := 5 * time.Minute
  107. ctx, cancel := context.WithTimeout(context.Background(), timeout)
  108. defer cancel()
  109. cmdArgs := []string{"update-index", "--remove", "-z", "--index-info"}
  110. cmd := exec.CommandContext(ctx, "git", cmdArgs...)
  111. desc := fmt.Sprintf("removeFilesFromIndex: (git update-index) %v", filenames)
  112. cmd.Dir = t.basePath
  113. cmd.Stdout = stdOut
  114. cmd.Stderr = stdErr
  115. cmd.Stdin = bytes.NewReader(stdIn.Bytes())
  116. if err := cmd.Start(); err != nil {
  117. return fmt.Errorf("exec(%s) failed: %v(%v)", desc, err, ctx.Err())
  118. }
  119. pid := process.GetManager().Add(desc, cmd)
  120. err := cmd.Wait()
  121. process.GetManager().Remove(pid)
  122. if err != nil {
  123. err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr)
  124. }
  125. return err
  126. }
  127. // HashObject writes the provided content to the object db and returns its hash
  128. func (t *TemporaryUploadRepository) HashObject(content io.Reader) (string, error) {
  129. timeout := 5 * time.Minute
  130. ctx, cancel := context.WithTimeout(context.Background(), timeout)
  131. defer cancel()
  132. hashCmd := exec.CommandContext(ctx, "git", "hash-object", "-w", "--stdin")
  133. hashCmd.Dir = t.basePath
  134. hashCmd.Stdin = content
  135. stdOutBuffer := new(bytes.Buffer)
  136. stdErrBuffer := new(bytes.Buffer)
  137. hashCmd.Stdout = stdOutBuffer
  138. hashCmd.Stderr = stdErrBuffer
  139. desc := fmt.Sprintf("hashObject: (git hash-object)")
  140. if err := hashCmd.Start(); err != nil {
  141. return "", fmt.Errorf("git hash-object: %s", err)
  142. }
  143. pid := process.GetManager().Add(desc, hashCmd)
  144. err := hashCmd.Wait()
  145. process.GetManager().Remove(pid)
  146. if err != nil {
  147. err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOutBuffer, stdErrBuffer)
  148. return "", err
  149. }
  150. return strings.TrimSpace(stdOutBuffer.String()), nil
  151. }
  152. // AddObjectToIndex adds the provided object hash to the index with the provided mode and path
  153. func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPath string) error {
  154. if _, stderr, err := process.GetManager().ExecDir(5*time.Minute,
  155. t.basePath,
  156. fmt.Sprintf("addObjectToIndex (git update-index): %s", t.basePath),
  157. "git", "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, objectPath); err != nil {
  158. return fmt.Errorf("git update-index: %s", stderr)
  159. }
  160. return nil
  161. }
  162. // WriteTree writes the current index as a tree to the object db and returns its hash
  163. func (t *TemporaryUploadRepository) WriteTree() (string, error) {
  164. treeHash, stderr, err := process.GetManager().ExecDir(5*time.Minute,
  165. t.basePath,
  166. fmt.Sprintf("WriteTree (git write-tree): %s", t.basePath),
  167. "git", "write-tree")
  168. if err != nil {
  169. return "", fmt.Errorf("git write-tree: %s", stderr)
  170. }
  171. return strings.TrimSpace(treeHash), nil
  172. }
  173. // CommitTree creates a commit from a given tree for the user with provided message
  174. func (t *TemporaryUploadRepository) CommitTree(doer *models.User, treeHash string, message string) (string, error) {
  175. commitTimeStr := time.Now().Format(time.UnixDate)
  176. sig := doer.NewGitSig()
  177. // FIXME: Should we add SSH_ORIGINAL_COMMAND to this
  178. // Because this may call hooks we should pass in the environment
  179. env := append(os.Environ(),
  180. "GIT_AUTHOR_NAME="+sig.Name,
  181. "GIT_AUTHOR_EMAIL="+sig.Email,
  182. "GIT_AUTHOR_DATE="+commitTimeStr,
  183. "GIT_COMMITTER_NAME="+sig.Name,
  184. "GIT_COMMITTER_EMAIL="+sig.Email,
  185. "GIT_COMMITTER_DATE="+commitTimeStr,
  186. )
  187. commitHash, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute,
  188. t.basePath,
  189. fmt.Sprintf("commitTree (git commit-tree): %s", t.basePath),
  190. env,
  191. "git", "commit-tree", treeHash, "-p", "HEAD", "-m", message)
  192. if err != nil {
  193. return "", fmt.Errorf("git commit-tree: %s", stderr)
  194. }
  195. return strings.TrimSpace(commitHash), nil
  196. }
  197. // Push the provided commitHash to the repository branch by the provided user
  198. func (t *TemporaryUploadRepository) Push(doer *models.User, commitHash string, branch string) error {
  199. isWiki := "false"
  200. if strings.HasSuffix(t.repo.Name, ".wiki") {
  201. isWiki = "true"
  202. }
  203. sig := doer.NewGitSig()
  204. // FIXME: Should we add SSH_ORIGINAL_COMMAND to this
  205. // Because calls hooks we need to pass in the environment
  206. env := append(os.Environ(),
  207. "GIT_AUTHOR_NAME="+sig.Name,
  208. "GIT_AUTHOR_EMAIL="+sig.Email,
  209. "GIT_COMMITTER_NAME="+sig.Name,
  210. "GIT_COMMITTER_EMAIL="+sig.Email,
  211. models.EnvRepoName+"="+t.repo.Name,
  212. models.EnvRepoUsername+"="+t.repo.OwnerName,
  213. models.EnvRepoIsWiki+"="+isWiki,
  214. models.EnvPusherName+"="+doer.Name,
  215. models.EnvPusherID+"="+fmt.Sprintf("%d", doer.ID),
  216. models.ProtectedBranchRepoID+"="+fmt.Sprintf("%d", t.repo.ID),
  217. )
  218. if _, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute,
  219. t.basePath,
  220. fmt.Sprintf("actuallyPush (git push): %s", t.basePath),
  221. env,
  222. "git", "push", t.repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)); err != nil {
  223. return fmt.Errorf("git push: %s", stderr)
  224. }
  225. return nil
  226. }
  227. // DiffIndex returns a Diff of the current index to the head
  228. func (t *TemporaryUploadRepository) DiffIndex() (diff *models.Diff, err error) {
  229. timeout := 5 * time.Minute
  230. ctx, cancel := context.WithTimeout(context.Background(), timeout)
  231. defer cancel()
  232. stdErr := new(bytes.Buffer)
  233. cmd := exec.CommandContext(ctx, "git", "diff-index", "--cached", "-p", "HEAD")
  234. cmd.Dir = t.basePath
  235. cmd.Stderr = stdErr
  236. stdout, err := cmd.StdoutPipe()
  237. if err != nil {
  238. return nil, fmt.Errorf("StdoutPipe: %v stderr %s", err, stdErr.String())
  239. }
  240. if err = cmd.Start(); err != nil {
  241. return nil, fmt.Errorf("Start: %v stderr %s", err, stdErr.String())
  242. }
  243. pid := process.GetManager().Add(fmt.Sprintf("diffIndex [repo_path: %s]", t.repo.RepoPath()), cmd)
  244. defer process.GetManager().Remove(pid)
  245. diff, err = models.ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdout)
  246. if err != nil {
  247. return nil, fmt.Errorf("ParsePatch: %v", err)
  248. }
  249. if err = cmd.Wait(); err != nil {
  250. return nil, fmt.Errorf("Wait: %v", err)
  251. }
  252. return diff, nil
  253. }
  254. // CheckAttribute checks the given attribute of the provided files
  255. func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...string) (map[string]map[string]string, error) {
  256. stdOut := new(bytes.Buffer)
  257. stdErr := new(bytes.Buffer)
  258. timeout := 5 * time.Minute
  259. ctx, cancel := context.WithTimeout(context.Background(), timeout)
  260. defer cancel()
  261. cmdArgs := []string{"check-attr", "-z", attribute, "--cached", "--"}
  262. for _, arg := range args {
  263. if arg != "" {
  264. cmdArgs = append(cmdArgs, arg)
  265. }
  266. }
  267. cmd := exec.CommandContext(ctx, "git", cmdArgs...)
  268. desc := fmt.Sprintf("checkAttr: (git check-attr) %s %v", attribute, cmdArgs)
  269. cmd.Dir = t.basePath
  270. cmd.Stdout = stdOut
  271. cmd.Stderr = stdErr
  272. if err := cmd.Start(); err != nil {
  273. return nil, fmt.Errorf("exec(%s) failed: %v(%v)", desc, err, ctx.Err())
  274. }
  275. pid := process.GetManager().Add(desc, cmd)
  276. err := cmd.Wait()
  277. process.GetManager().Remove(pid)
  278. if err != nil {
  279. err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr)
  280. return nil, err
  281. }
  282. fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})
  283. if len(fields)%3 != 1 {
  284. return nil, fmt.Errorf("Wrong number of fields in return from check-attr")
  285. }
  286. var name2attribute2info = make(map[string]map[string]string)
  287. for i := 0; i < (len(fields) / 3); i++ {
  288. filename := string(fields[3*i])
  289. attribute := string(fields[3*i+1])
  290. info := string(fields[3*i+2])
  291. attribute2info := name2attribute2info[filename]
  292. if attribute2info == nil {
  293. attribute2info = make(map[string]string)
  294. }
  295. attribute2info[attribute] = info
  296. name2attribute2info[filename] = attribute2info
  297. }
  298. return name2attribute2info, err
  299. }