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

hook.go 12 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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 private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
  5. package private
  6. import (
  7. "fmt"
  8. "net/http"
  9. "os"
  10. "strings"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/git"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/private"
  15. "code.gitea.io/gitea/modules/repofiles"
  16. "code.gitea.io/gitea/modules/util"
  17. "gitea.com/macaron/macaron"
  18. )
  19. // HookPreReceive checks whether a individual commit is acceptable
  20. func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
  21. ownerName := ctx.Params(":owner")
  22. repoName := ctx.Params(":repo")
  23. repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
  24. if err != nil {
  25. log.Error("Unable to get repository: %s/%s Error: %v", ownerName, repoName, err)
  26. ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
  27. "err": err.Error(),
  28. })
  29. return
  30. }
  31. repo.OwnerName = ownerName
  32. for i := range opts.OldCommitIDs {
  33. oldCommitID := opts.OldCommitIDs[i]
  34. newCommitID := opts.NewCommitIDs[i]
  35. refFullName := opts.RefFullNames[i]
  36. branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
  37. protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName)
  38. if err != nil {
  39. log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
  40. ctx.JSON(500, map[string]interface{}{
  41. "err": err.Error(),
  42. })
  43. return
  44. }
  45. if protectBranch != nil && protectBranch.IsProtected() {
  46. // check and deletion
  47. if newCommitID == git.EmptySHA {
  48. log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
  49. ctx.JSON(http.StatusForbidden, map[string]interface{}{
  50. "err": fmt.Sprintf("branch %s is protected from deletion", branchName),
  51. })
  52. return
  53. }
  54. // detect force push
  55. if git.EmptySHA != oldCommitID {
  56. env := os.Environ()
  57. if opts.GitAlternativeObjectDirectories != "" {
  58. env = append(env,
  59. private.GitAlternativeObjectDirectories+"="+opts.GitAlternativeObjectDirectories)
  60. }
  61. if opts.GitObjectDirectory != "" {
  62. env = append(env,
  63. private.GitObjectDirectory+"="+opts.GitObjectDirectory)
  64. }
  65. if opts.GitQuarantinePath != "" {
  66. env = append(env,
  67. private.GitQuarantinePath+"="+opts.GitQuarantinePath)
  68. }
  69. output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env)
  70. if err != nil {
  71. log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
  72. ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
  73. "err": fmt.Sprintf("Fail to detect force push: %v", err),
  74. })
  75. return
  76. } else if len(output) > 0 {
  77. log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
  78. ctx.JSON(http.StatusForbidden, map[string]interface{}{
  79. "err": fmt.Sprintf("branch %s is protected from force push", branchName),
  80. })
  81. return
  82. }
  83. }
  84. canPush := false
  85. if opts.IsDeployKey {
  86. canPush = protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
  87. } else {
  88. canPush = protectBranch.CanUserPush(opts.UserID)
  89. }
  90. if !canPush && opts.ProtectedBranchID > 0 {
  91. pr, err := models.GetPullRequestByID(opts.ProtectedBranchID)
  92. if err != nil {
  93. log.Error("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err)
  94. ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
  95. "err": fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err),
  96. })
  97. return
  98. }
  99. if !protectBranch.HasEnoughApprovals(pr) {
  100. log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v and pr #%d does not have enough approvals", opts.UserID, branchName, repo, pr.Index)
  101. ctx.JSON(http.StatusForbidden, map[string]interface{}{
  102. "err": fmt.Sprintf("protected branch %s can not be pushed to and pr #%d does not have enough approvals", branchName, opts.ProtectedBranchID),
  103. })
  104. return
  105. }
  106. } else if !canPush {
  107. log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v", opts.UserID, branchName, repo)
  108. ctx.JSON(http.StatusForbidden, map[string]interface{}{
  109. "err": fmt.Sprintf("protected branch %s can not be pushed to", branchName),
  110. })
  111. return
  112. }
  113. }
  114. }
  115. ctx.PlainText(http.StatusOK, []byte("ok"))
  116. }
  117. // HookPostReceive updates services and users
  118. func HookPostReceive(ctx *macaron.Context, opts private.HookOptions) {
  119. ownerName := ctx.Params(":owner")
  120. repoName := ctx.Params(":repo")
  121. var repo *models.Repository
  122. updates := make([]*repofiles.PushUpdateOptions, 0, len(opts.OldCommitIDs))
  123. wasEmpty := false
  124. for i := range opts.OldCommitIDs {
  125. refFullName := opts.RefFullNames[i]
  126. branch := opts.RefFullNames[i]
  127. if strings.HasPrefix(branch, git.BranchPrefix) {
  128. branch = strings.TrimPrefix(branch, git.BranchPrefix)
  129. } else {
  130. branch = strings.TrimPrefix(branch, git.TagPrefix)
  131. }
  132. // Only trigger activity updates for changes to branches or
  133. // tags. Updates to other refs (eg, refs/notes, refs/changes,
  134. // or other less-standard refs spaces are ignored since there
  135. // may be a very large number of them).
  136. if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
  137. if repo == nil {
  138. var err error
  139. repo, err = models.GetRepositoryByOwnerAndName(ownerName, repoName)
  140. if err != nil {
  141. log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
  142. ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
  143. Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
  144. })
  145. return
  146. }
  147. if repo.OwnerName == "" {
  148. repo.OwnerName = ownerName
  149. }
  150. wasEmpty = repo.IsEmpty
  151. }
  152. option := repofiles.PushUpdateOptions{
  153. RefFullName: refFullName,
  154. OldCommitID: opts.OldCommitIDs[i],
  155. NewCommitID: opts.NewCommitIDs[i],
  156. Branch: branch,
  157. PusherID: opts.UserID,
  158. PusherName: opts.UserName,
  159. RepoUserName: ownerName,
  160. RepoName: repoName,
  161. }
  162. updates = append(updates, &option)
  163. if repo.IsEmpty && branch == "master" && strings.HasPrefix(refFullName, git.BranchPrefix) {
  164. // put the master branch first
  165. copy(updates[1:], updates)
  166. updates[0] = &option
  167. }
  168. }
  169. }
  170. if repo != nil && len(updates) > 0 {
  171. if err := repofiles.PushUpdates(repo, updates); err != nil {
  172. log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
  173. for i, update := range updates {
  174. log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.Branch)
  175. }
  176. log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
  177. ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
  178. Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
  179. })
  180. return
  181. }
  182. }
  183. results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs))
  184. // We have to reload the repo in case its state is changed above
  185. repo = nil
  186. var baseRepo *models.Repository
  187. for i := range opts.OldCommitIDs {
  188. refFullName := opts.RefFullNames[i]
  189. newCommitID := opts.NewCommitIDs[i]
  190. branch := git.RefEndName(opts.RefFullNames[i])
  191. if newCommitID != git.EmptySHA && strings.HasPrefix(refFullName, git.BranchPrefix) {
  192. if repo == nil {
  193. var err error
  194. repo, err = models.GetRepositoryByOwnerAndName(ownerName, repoName)
  195. if err != nil {
  196. log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
  197. ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
  198. Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
  199. RepoWasEmpty: wasEmpty,
  200. })
  201. return
  202. }
  203. if repo.OwnerName == "" {
  204. repo.OwnerName = ownerName
  205. }
  206. if !repo.AllowsPulls() {
  207. // We can stop there's no need to go any further
  208. ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
  209. RepoWasEmpty: wasEmpty,
  210. })
  211. return
  212. }
  213. baseRepo = repo
  214. if repo.IsFork {
  215. if err := repo.GetBaseRepo(); err != nil {
  216. log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
  217. ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
  218. Err: fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
  219. RepoWasEmpty: wasEmpty,
  220. })
  221. return
  222. }
  223. baseRepo = repo.BaseRepo
  224. }
  225. }
  226. if !repo.IsFork && branch == baseRepo.DefaultBranch {
  227. results = append(results, private.HookPostReceiveBranchResult{})
  228. continue
  229. }
  230. pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch)
  231. if err != nil && !models.IsErrPullRequestNotExist(err) {
  232. log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
  233. ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
  234. Err: fmt.Sprintf(
  235. "Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err),
  236. RepoWasEmpty: wasEmpty,
  237. })
  238. return
  239. }
  240. if pr == nil {
  241. if repo.IsFork {
  242. branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
  243. }
  244. results = append(results, private.HookPostReceiveBranchResult{
  245. Message: true,
  246. Create: true,
  247. Branch: branch,
  248. URL: fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)),
  249. })
  250. } else {
  251. results = append(results, private.HookPostReceiveBranchResult{
  252. Message: true,
  253. Create: false,
  254. Branch: branch,
  255. URL: fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index),
  256. })
  257. }
  258. }
  259. }
  260. ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
  261. Results: results,
  262. RepoWasEmpty: wasEmpty,
  263. })
  264. }
  265. // SetDefaultBranch updates the default branch
  266. func SetDefaultBranch(ctx *macaron.Context) {
  267. ownerName := ctx.Params(":owner")
  268. repoName := ctx.Params(":repo")
  269. branch := ctx.Params(":branch")
  270. repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
  271. if err != nil {
  272. log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
  273. ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
  274. "Err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
  275. })
  276. return
  277. }
  278. if repo.OwnerName == "" {
  279. repo.OwnerName = ownerName
  280. }
  281. repo.DefaultBranch = branch
  282. gitRepo, err := git.OpenRepository(repo.RepoPath())
  283. if err != nil {
  284. ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
  285. "Err": fmt.Sprintf("Failed to get git repository: %s/%s Error: %v", ownerName, repoName, err),
  286. })
  287. return
  288. }
  289. if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
  290. if !git.IsErrUnsupportedVersion(err) {
  291. gitRepo.Close()
  292. ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
  293. "Err": fmt.Sprintf("Unable to set default branch onrepository: %s/%s Error: %v", ownerName, repoName, err),
  294. })
  295. return
  296. }
  297. }
  298. gitRepo.Close()
  299. if err := repo.UpdateDefaultBranch(); err != nil {
  300. ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
  301. "Err": fmt.Sprintf("Unable to set default branch onrepository: %s/%s Error: %v", ownerName, repoName, err),
  302. })
  303. return
  304. }
  305. ctx.PlainText(200, []byte("success"))
  306. }