gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2017 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package context
  5. import (
  6. "context"
  7. "errors"
  8. "fmt"
  9. "html"
  10. "net/http"
  11. "net/url"
  12. "path"
  13. "strings"
  14. asymkey_model "code.gitea.io/gitea/models/asymkey"
  15. "code.gitea.io/gitea/models/db"
  16. git_model "code.gitea.io/gitea/models/git"
  17. issues_model "code.gitea.io/gitea/models/issues"
  18. access_model "code.gitea.io/gitea/models/perm/access"
  19. repo_model "code.gitea.io/gitea/models/repo"
  20. unit_model "code.gitea.io/gitea/models/unit"
  21. user_model "code.gitea.io/gitea/models/user"
  22. "code.gitea.io/gitea/modules/cache"
  23. "code.gitea.io/gitea/modules/git"
  24. "code.gitea.io/gitea/modules/gitrepo"
  25. "code.gitea.io/gitea/modules/httplib"
  26. code_indexer "code.gitea.io/gitea/modules/indexer/code"
  27. "code.gitea.io/gitea/modules/log"
  28. "code.gitea.io/gitea/modules/optional"
  29. repo_module "code.gitea.io/gitea/modules/repository"
  30. "code.gitea.io/gitea/modules/setting"
  31. "code.gitea.io/gitea/modules/util"
  32. asymkey_service "code.gitea.io/gitea/services/asymkey"
  33. "github.com/editorconfig/editorconfig-core-go/v2"
  34. )
  35. // PullRequest contains information to make a pull request
  36. type PullRequest struct {
  37. BaseRepo *repo_model.Repository
  38. Allowed bool // it only used by the web tmpl: "PullRequestCtx.Allowed"
  39. SameRepo bool // it only used by the web tmpl: "PullRequestCtx.SameRepo"
  40. }
  41. // Repository contains information to operate a repository
  42. type Repository struct {
  43. access_model.Permission
  44. Repository *repo_model.Repository
  45. Owner *user_model.User
  46. RepoLink string
  47. GitRepo *git.Repository
  48. // RefFullName is the full ref name that the user is viewing
  49. RefFullName git.RefName
  50. BranchName string // it is the RefFullName's short name if its type is "branch"
  51. TreePath string
  52. // Commit it is always set to the commit for the branch or tag, or just the commit that the user is viewing
  53. Commit *git.Commit
  54. CommitID string
  55. CommitsCount int64
  56. PullRequest *PullRequest
  57. }
  58. // CanWriteToBranch checks if the branch is writable by the user
  59. func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User, branch string) bool {
  60. return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user)
  61. }
  62. // CanCreateBranch returns true if repository is editable and user has proper access level.
  63. func (r *Repository) CanCreateBranch() bool {
  64. return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
  65. }
  66. func (r *Repository) GetObjectFormat() git.ObjectFormat {
  67. return git.ObjectFormatFromName(r.Repository.ObjectFormatName)
  68. }
  69. // RepoMustNotBeArchived checks if a repo is archived
  70. func RepoMustNotBeArchived() func(ctx *Context) {
  71. return func(ctx *Context) {
  72. if ctx.Repo.Repository.IsArchived {
  73. ctx.NotFound(errors.New(ctx.Locale.TrString("repo.archive.title")))
  74. }
  75. }
  76. }
  77. type CommitFormOptions struct {
  78. NeedFork bool
  79. TargetRepo *repo_model.Repository
  80. TargetFormAction string
  81. WillSubmitToFork bool
  82. CanCommitToBranch bool
  83. UserCanPush bool
  84. RequireSigned bool
  85. WillSign bool
  86. SigningKeyFormDisplay string
  87. WontSignReason string
  88. CanCreatePullRequest bool
  89. CanCreateBasePullRequest bool
  90. }
  91. func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) {
  92. if !refName.IsBranch() {
  93. // it shouldn't happen because middleware already checks
  94. return nil, util.NewInvalidArgumentErrorf("ref %q is not a branch", refName)
  95. }
  96. originRepo := targetRepo
  97. branchName := refName.ShortName()
  98. // TODO: CanMaintainerWriteToBranch is a bad name, but it really does what "CanWriteToBranch" does
  99. if !issues_model.CanMaintainerWriteToBranch(ctx, doerRepoPerm, branchName, doer) {
  100. targetRepo = repo_model.GetForkedRepo(ctx, doer.ID, targetRepo.ID)
  101. if targetRepo == nil {
  102. return &CommitFormOptions{NeedFork: true}, nil
  103. }
  104. // now, we get our own forked repo; it must be writable by us.
  105. }
  106. submitToForkedRepo := targetRepo.ID != originRepo.ID
  107. err := targetRepo.GetBaseRepo(ctx)
  108. if err != nil {
  109. return nil, err
  110. }
  111. protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName)
  112. if err != nil {
  113. return nil, err
  114. }
  115. canPushWithProtection := true
  116. protectionRequireSigned := false
  117. if protectedBranch != nil {
  118. protectedBranch.Repo = targetRepo
  119. canPushWithProtection = protectedBranch.CanUserPush(ctx, doer)
  120. protectionRequireSigned = protectedBranch.RequireSignedCommits
  121. }
  122. willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String())
  123. wontSignReason := ""
  124. if asymkey_service.IsErrWontSign(err) {
  125. wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
  126. } else if err != nil {
  127. return nil, err
  128. }
  129. canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection
  130. if protectionRequireSigned {
  131. canCommitToBranch = canCommitToBranch && willSign
  132. }
  133. canCreateBasePullRequest := targetRepo.BaseRepo != nil && targetRepo.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
  134. canCreatePullRequest := targetRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
  135. opts := &CommitFormOptions{
  136. TargetRepo: targetRepo,
  137. WillSubmitToFork: submitToForkedRepo,
  138. CanCommitToBranch: canCommitToBranch,
  139. UserCanPush: canPushWithProtection,
  140. RequireSigned: protectionRequireSigned,
  141. WillSign: willSign,
  142. SigningKeyFormDisplay: asymkey_model.GetDisplaySigningKey(signKey),
  143. WontSignReason: wontSignReason,
  144. CanCreatePullRequest: canCreatePullRequest,
  145. CanCreateBasePullRequest: canCreateBasePullRequest,
  146. }
  147. editorAction := ctx.PathParam("editor_action")
  148. editorPathParamRemaining := util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
  149. if submitToForkedRepo {
  150. // there is only "default branch" in forked repo, we will use "from_base_branch" to get a new branch from base repo
  151. editorPathParamRemaining = util.PathEscapeSegments(targetRepo.DefaultBranch) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + "?from_base_branch=" + url.QueryEscape(branchName)
  152. }
  153. if editorAction == "_cherrypick" {
  154. opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + ctx.PathParam("sha") + "/" + editorPathParamRemaining
  155. } else {
  156. opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + editorPathParamRemaining
  157. }
  158. if ctx.Req.URL.RawQuery != "" {
  159. opts.TargetFormAction += util.Iif(strings.Contains(opts.TargetFormAction, "?"), "&", "?") + ctx.Req.URL.RawQuery
  160. }
  161. return opts, nil
  162. }
  163. // CanUseTimetracker returns whether a user can use the timetracker.
  164. func (r *Repository) CanUseTimetracker(ctx context.Context, issue *issues_model.Issue, user *user_model.User) bool {
  165. // Checking for following:
  166. // 1. Is timetracker enabled
  167. // 2. Is the user a contributor, admin, poster or assignee and do the repository policies require this?
  168. isAssigned, _ := issues_model.IsUserAssignedToIssue(ctx, issue, user)
  169. return r.Repository.IsTimetrackerEnabled(ctx) && (!r.Repository.AllowOnlyContributorsToTrackTime(ctx) ||
  170. r.Permission.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsPoster(user.ID) || isAssigned)
  171. }
  172. // CanCreateIssueDependencies returns whether or not a user can create dependencies.
  173. func (r *Repository) CanCreateIssueDependencies(ctx context.Context, user *user_model.User, isPull bool) bool {
  174. return r.Repository.IsDependenciesEnabled(ctx) && r.Permission.CanWriteIssuesOrPulls(isPull)
  175. }
  176. // GetCommitsCount returns cached commit count for current view
  177. func (r *Repository) GetCommitsCount() (int64, error) {
  178. if r.Commit == nil {
  179. return 0, nil
  180. }
  181. contextName := r.RefFullName.ShortName()
  182. isRef := r.RefFullName.IsBranch() || r.RefFullName.IsTag()
  183. return cache.GetInt64(r.Repository.GetCommitsCountCacheKey(contextName, isRef), func() (int64, error) {
  184. return r.Commit.CommitsCount()
  185. })
  186. }
  187. // GetCommitGraphsCount returns cached commit count for current view
  188. func (r *Repository) GetCommitGraphsCount(ctx context.Context, hidePRRefs bool, branches, files []string) (int64, error) {
  189. cacheKey := fmt.Sprintf("commits-count-%d-graph-%t-%s-%s", r.Repository.ID, hidePRRefs, branches, files)
  190. return cache.GetInt64(cacheKey, func() (int64, error) {
  191. if len(branches) == 0 {
  192. return git.AllCommitsCount(ctx, r.Repository.RepoPath(), hidePRRefs, files...)
  193. }
  194. return git.CommitsCount(ctx,
  195. git.CommitsCountOptions{
  196. RepoPath: r.Repository.RepoPath(),
  197. Revision: branches,
  198. RelPath: files,
  199. })
  200. })
  201. }
  202. // RefTypeNameSubURL makes a sub-url for the current ref (branch/tag/commit) field, for example:
  203. // * "branch/master"
  204. // * "tag/v1.0.0"
  205. // * "commit/123456"
  206. // It is usually used to construct a link like ".../src/{{RefTypeNameSubURL}}/{{PathEscapeSegments TreePath}}"
  207. func (r *Repository) RefTypeNameSubURL() string {
  208. return r.RefFullName.RefWebLinkPath()
  209. }
  210. // GetEditorconfig returns the .editorconfig definition if found in the
  211. // HEAD of the default repo branch.
  212. func (r *Repository) GetEditorconfig(optCommit ...*git.Commit) (cfg *editorconfig.Editorconfig, warning, err error) {
  213. if r.GitRepo == nil {
  214. return nil, nil, nil
  215. }
  216. var commit *git.Commit
  217. if len(optCommit) != 0 {
  218. commit = optCommit[0]
  219. } else {
  220. commit, err = r.GitRepo.GetBranchCommit(r.Repository.DefaultBranch)
  221. if err != nil {
  222. return nil, nil, err
  223. }
  224. }
  225. treeEntry, err := commit.GetTreeEntryByPath(".editorconfig")
  226. if err != nil {
  227. return nil, nil, err
  228. }
  229. if treeEntry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
  230. return nil, nil, git.ErrNotExist{ID: "", RelPath: ".editorconfig"}
  231. }
  232. reader, err := treeEntry.Blob().DataAsync()
  233. if err != nil {
  234. return nil, nil, err
  235. }
  236. defer reader.Close()
  237. return editorconfig.ParseGraceful(reader)
  238. }
  239. // RetrieveBaseRepo retrieves base repository
  240. func RetrieveBaseRepo(ctx *Context, repo *repo_model.Repository) {
  241. // Non-fork repository will not return error in this method.
  242. if err := repo.GetBaseRepo(ctx); err != nil {
  243. if repo_model.IsErrRepoNotExist(err) {
  244. repo.IsFork = false
  245. repo.ForkID = 0
  246. return
  247. }
  248. ctx.ServerError("GetBaseRepo", err)
  249. return
  250. } else if err = repo.BaseRepo.LoadOwner(ctx); err != nil {
  251. ctx.ServerError("BaseRepo.LoadOwner", err)
  252. return
  253. }
  254. }
  255. // RetrieveTemplateRepo retrieves template repository used to generate this repository
  256. func RetrieveTemplateRepo(ctx *Context, repo *repo_model.Repository) {
  257. // Non-generated repository will not return error in this method.
  258. templateRepo, err := repo_model.GetTemplateRepo(ctx, repo)
  259. if err != nil {
  260. if repo_model.IsErrRepoNotExist(err) {
  261. repo.TemplateID = 0
  262. return
  263. }
  264. ctx.ServerError("GetTemplateRepo", err)
  265. return
  266. } else if err = templateRepo.LoadOwner(ctx); err != nil {
  267. ctx.ServerError("TemplateRepo.LoadOwner", err)
  268. return
  269. }
  270. perm, err := access_model.GetUserRepoPermission(ctx, templateRepo, ctx.Doer)
  271. if err != nil {
  272. ctx.ServerError("GetUserRepoPermission", err)
  273. return
  274. }
  275. if !perm.CanRead(unit_model.TypeCode) {
  276. repo.TemplateID = 0
  277. }
  278. }
  279. // ComposeGoGetImport returns go-get-import meta content.
  280. func ComposeGoGetImport(ctx context.Context, owner, repo string) string {
  281. curAppURL, _ := url.Parse(httplib.GuessCurrentAppURL(ctx))
  282. return path.Join(curAppURL.Host, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo))
  283. }
  284. // EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200
  285. // if user does not have actual access to the requested repository,
  286. // or the owner or repository does not exist at all.
  287. // This is particular a workaround for "go get" command which does not respect
  288. // .netrc file.
  289. func EarlyResponseForGoGetMeta(ctx *Context) {
  290. username := ctx.PathParam("username")
  291. reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
  292. if username == "" || reponame == "" {
  293. ctx.PlainText(http.StatusBadRequest, "invalid repository path")
  294. return
  295. }
  296. var cloneURL string
  297. if setting.Repository.GoGetCloneURLProtocol == "ssh" {
  298. cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame)
  299. } else {
  300. cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame)
  301. }
  302. goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), cloneURL)
  303. htmlMeta := fmt.Sprintf(`<meta name="go-import" content="%s">`, html.EscapeString(goImportContent))
  304. ctx.PlainText(http.StatusOK, htmlMeta)
  305. }
  306. // RedirectToRepo redirect to a differently-named repository
  307. func RedirectToRepo(ctx *Base, redirectRepoID int64) {
  308. ownerName := ctx.PathParam("username")
  309. previousRepoName := ctx.PathParam("reponame")
  310. repo, err := repo_model.GetRepositoryByID(ctx, redirectRepoID)
  311. if err != nil {
  312. log.Error("GetRepositoryByID: %v", err)
  313. ctx.HTTPError(http.StatusInternalServerError, "GetRepositoryByID")
  314. return
  315. }
  316. redirectPath := strings.Replace(
  317. ctx.Req.URL.EscapedPath(),
  318. url.PathEscape(ownerName)+"/"+url.PathEscape(previousRepoName),
  319. url.PathEscape(repo.OwnerName)+"/"+url.PathEscape(repo.Name),
  320. 1,
  321. )
  322. if ctx.Req.URL.RawQuery != "" {
  323. redirectPath += "?" + ctx.Req.URL.RawQuery
  324. }
  325. // Git client needs a 301 redirect by default to follow the new location
  326. // It's not documentated in git documentation, but it's the behavior of git client
  327. ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently)
  328. }
  329. func repoAssignment(ctx *Context, repo *repo_model.Repository) {
  330. var err error
  331. if err = repo.LoadOwner(ctx); err != nil {
  332. ctx.ServerError("LoadOwner", err)
  333. return
  334. }
  335. if ctx.DoerNeedTwoFactorAuth() {
  336. ctx.Repo.Permission = access_model.PermissionNoAccess()
  337. } else {
  338. ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
  339. if err != nil {
  340. ctx.ServerError("GetUserRepoPermission", err)
  341. return
  342. }
  343. }
  344. if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() && !canWriteAsMaintainer(ctx) {
  345. if ctx.FormString("go-get") == "1" {
  346. EarlyResponseForGoGetMeta(ctx)
  347. return
  348. }
  349. ctx.NotFound(nil)
  350. return
  351. }
  352. ctx.Data["Permission"] = &ctx.Repo.Permission
  353. if repo.IsMirror {
  354. pullMirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
  355. if err == nil {
  356. ctx.Data["PullMirror"] = pullMirror
  357. } else if err != repo_model.ErrMirrorNotExist {
  358. ctx.ServerError("GetMirrorByRepoID", err)
  359. return
  360. }
  361. }
  362. ctx.Repo.Repository = repo
  363. ctx.Data["RepoName"] = ctx.Repo.Repository.Name
  364. ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
  365. }
  366. // RepoAssignment returns a middleware to handle repository assignment
  367. func RepoAssignment(ctx *Context) {
  368. if ctx.Data["Repository"] != nil {
  369. setting.PanicInDevOrTesting("RepoAssignment should not be executed twice")
  370. }
  371. var err error
  372. userName := ctx.PathParam("username")
  373. repoName := ctx.PathParam("reponame")
  374. repoName = strings.TrimSuffix(repoName, ".git")
  375. if setting.Other.EnableFeed {
  376. ctx.Data["EnableFeed"] = true
  377. repoName = strings.TrimSuffix(repoName, ".rss")
  378. repoName = strings.TrimSuffix(repoName, ".atom")
  379. }
  380. // Check if the user is the same as the repository owner
  381. if ctx.IsSigned && strings.EqualFold(ctx.Doer.LowerName, userName) {
  382. ctx.Repo.Owner = ctx.Doer
  383. } else {
  384. ctx.Repo.Owner, err = user_model.GetUserByName(ctx, userName)
  385. if err != nil {
  386. if user_model.IsErrUserNotExist(err) {
  387. // go-get does not support redirects
  388. // https://github.com/golang/go/issues/19760
  389. if ctx.FormString("go-get") == "1" {
  390. EarlyResponseForGoGetMeta(ctx)
  391. return
  392. }
  393. if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
  394. RedirectToUser(ctx.Base, userName, redirectUserID)
  395. } else if user_model.IsErrUserRedirectNotExist(err) {
  396. ctx.NotFound(nil)
  397. } else {
  398. ctx.ServerError("LookupUserRedirect", err)
  399. }
  400. } else {
  401. ctx.ServerError("GetUserByName", err)
  402. }
  403. return
  404. }
  405. }
  406. ctx.ContextUser = ctx.Repo.Owner
  407. ctx.Data["ContextUser"] = ctx.ContextUser
  408. // redirect link to wiki
  409. if strings.HasSuffix(repoName, ".wiki") {
  410. // ctx.Req.URL.Path does not have the preceding appSubURL - any redirect must have this added
  411. // Now we happen to know that all of our paths are: /:username/:reponame/whatever_else
  412. originalRepoName := ctx.PathParam("reponame")
  413. redirectRepoName := strings.TrimSuffix(repoName, ".wiki")
  414. redirectRepoName += originalRepoName[len(redirectRepoName)+5:]
  415. redirectPath := strings.Replace(
  416. ctx.Req.URL.EscapedPath(),
  417. url.PathEscape(userName)+"/"+url.PathEscape(originalRepoName),
  418. url.PathEscape(userName)+"/"+url.PathEscape(redirectRepoName)+"/wiki",
  419. 1,
  420. )
  421. if ctx.Req.URL.RawQuery != "" {
  422. redirectPath += "?" + ctx.Req.URL.RawQuery
  423. }
  424. ctx.Redirect(path.Join(setting.AppSubURL, redirectPath))
  425. return
  426. }
  427. // Get repository.
  428. repo, err := repo_model.GetRepositoryByName(ctx, ctx.Repo.Owner.ID, repoName)
  429. if err != nil {
  430. if repo_model.IsErrRepoNotExist(err) {
  431. redirectRepoID, err := repo_model.LookupRedirect(ctx, ctx.Repo.Owner.ID, repoName)
  432. if err == nil {
  433. RedirectToRepo(ctx.Base, redirectRepoID)
  434. } else if repo_model.IsErrRedirectNotExist(err) {
  435. if ctx.FormString("go-get") == "1" {
  436. EarlyResponseForGoGetMeta(ctx)
  437. return
  438. }
  439. ctx.NotFound(nil)
  440. } else {
  441. ctx.ServerError("LookupRepoRedirect", err)
  442. }
  443. } else {
  444. ctx.ServerError("GetRepositoryByName", err)
  445. }
  446. return
  447. }
  448. repo.Owner = ctx.Repo.Owner
  449. repoAssignment(ctx, repo)
  450. if ctx.Written() {
  451. return
  452. }
  453. ctx.Repo.RepoLink = repo.Link()
  454. ctx.Data["RepoLink"] = ctx.Repo.RepoLink
  455. ctx.Data["FeedURL"] = ctx.Repo.RepoLink
  456. unit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeExternalTracker)
  457. if err == nil {
  458. ctx.Data["RepoExternalIssuesLink"] = unit.ExternalTrackerConfig().ExternalTrackerURL
  459. }
  460. ctx.Data["NumTags"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
  461. IncludeDrafts: true,
  462. IncludeTags: true,
  463. HasSha1: optional.Some(true), // only draft releases which are created with existing tags
  464. RepoID: ctx.Repo.Repository.ID,
  465. })
  466. if err != nil {
  467. ctx.ServerError("GetReleaseCountByRepoID", err)
  468. return
  469. }
  470. ctx.Data["NumReleases"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
  471. // only show draft releases for users who can write, read-only users shouldn't see draft releases.
  472. IncludeDrafts: ctx.Repo.CanWrite(unit_model.TypeReleases),
  473. RepoID: ctx.Repo.Repository.ID,
  474. })
  475. if err != nil {
  476. ctx.ServerError("GetReleaseCountByRepoID", err)
  477. return
  478. }
  479. ctx.Data["Title"] = repo.Owner.Name + "/" + repo.Name
  480. ctx.Data["Repository"] = repo
  481. ctx.Data["Owner"] = ctx.Repo.Repository.Owner
  482. ctx.Data["CanWriteCode"] = ctx.Repo.CanWrite(unit_model.TypeCode)
  483. ctx.Data["CanWriteIssues"] = ctx.Repo.CanWrite(unit_model.TypeIssues)
  484. ctx.Data["CanWritePulls"] = ctx.Repo.CanWrite(unit_model.TypePullRequests)
  485. ctx.Data["CanWriteActions"] = ctx.Repo.CanWrite(unit_model.TypeActions)
  486. canSignedUserFork, err := repo_module.CanUserForkRepo(ctx, ctx.Doer, ctx.Repo.Repository)
  487. if err != nil {
  488. ctx.ServerError("CanUserForkRepo", err)
  489. return
  490. }
  491. ctx.Data["CanSignedUserFork"] = canSignedUserFork
  492. userAndOrgForks, err := repo_model.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository)
  493. if err != nil {
  494. ctx.ServerError("GetForksByUserAndOrgs", err)
  495. return
  496. }
  497. ctx.Data["UserAndOrgForks"] = userAndOrgForks
  498. // canSignedUserFork is true if the current user doesn't have a fork of this repo yet or
  499. // if he owns an org that doesn't have a fork of this repo yet
  500. // If multiple forks are available or if the user can fork to another account, but there is already a fork: open selection dialog
  501. ctx.Data["ShowForkModal"] = len(userAndOrgForks) > 1 || (canSignedUserFork && len(userAndOrgForks) > 0)
  502. ctx.Data["RepoCloneLink"] = repo.CloneLink(ctx, ctx.Doer)
  503. cloneButtonShowHTTPS := !setting.Repository.DisableHTTPGit
  504. cloneButtonShowSSH := !setting.SSH.Disabled && (ctx.IsSigned || setting.SSH.ExposeAnonymous)
  505. if !cloneButtonShowHTTPS && !cloneButtonShowSSH {
  506. // We have to show at least one link, so we just show the HTTPS
  507. cloneButtonShowHTTPS = true
  508. }
  509. ctx.Data["CloneButtonShowHTTPS"] = cloneButtonShowHTTPS
  510. ctx.Data["CloneButtonShowSSH"] = cloneButtonShowSSH
  511. ctx.Data["CloneButtonOriginLink"] = ctx.Data["RepoCloneLink"] // it may be rewritten to the WikiCloneLink by the router middleware
  512. ctx.Data["RepoSearchEnabled"] = setting.Indexer.RepoIndexerEnabled
  513. if setting.Indexer.RepoIndexerEnabled {
  514. ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
  515. }
  516. if ctx.IsSigned {
  517. ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, repo.ID)
  518. ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID)
  519. }
  520. if repo.IsFork {
  521. RetrieveBaseRepo(ctx, repo)
  522. if ctx.Written() {
  523. return
  524. }
  525. }
  526. if repo.IsGenerated() {
  527. RetrieveTemplateRepo(ctx, repo)
  528. if ctx.Written() {
  529. return
  530. }
  531. }
  532. isHomeOrSettings := ctx.Link == ctx.Repo.RepoLink ||
  533. ctx.Link == ctx.Repo.RepoLink+"/settings" ||
  534. strings.HasPrefix(ctx.Link, ctx.Repo.RepoLink+"/settings/") ||
  535. ctx.Link == ctx.Repo.RepoLink+"/-/migrate/status"
  536. // Disable everything when the repo is being created
  537. if ctx.Repo.Repository.IsBeingCreated() || ctx.Repo.Repository.IsBroken() {
  538. if !isHomeOrSettings {
  539. ctx.Redirect(ctx.Repo.RepoLink)
  540. }
  541. return
  542. }
  543. if ctx.Repo.GitRepo != nil {
  544. setting.PanicInDevOrTesting("RepoAssignment: GitRepo should be nil")
  545. _ = ctx.Repo.GitRepo.Close()
  546. ctx.Repo.GitRepo = nil
  547. }
  548. ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
  549. if err != nil {
  550. if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") {
  551. log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err)
  552. ctx.Repo.Repository.MarkAsBrokenEmpty()
  553. // Only allow access to base of repo or settings
  554. if !isHomeOrSettings {
  555. ctx.Redirect(ctx.Repo.RepoLink)
  556. }
  557. return
  558. }
  559. ctx.ServerError("RepoAssignment Invalid repo "+repo.FullName(), err)
  560. return
  561. }
  562. // Stop at this point when the repo is empty.
  563. if ctx.Repo.Repository.IsEmpty {
  564. return
  565. }
  566. branchOpts := git_model.FindBranchOptions{
  567. RepoID: ctx.Repo.Repository.ID,
  568. IsDeletedBranch: optional.Some(false),
  569. ListOptions: db.ListOptionsAll,
  570. }
  571. branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts)
  572. if err != nil {
  573. ctx.ServerError("CountBranches", err)
  574. return
  575. }
  576. // non-empty repo should have at least 1 branch, so this repository's branches haven't been synced yet
  577. if branchesTotal == 0 { // fallback to do a sync immediately
  578. branchesTotal, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0)
  579. if err != nil {
  580. ctx.ServerError("SyncRepoBranches", err)
  581. return
  582. }
  583. }
  584. ctx.Data["BranchesCount"] = branchesTotal
  585. // People who have push access or have forked repository can propose a new pull request.
  586. canPush := ctx.Repo.CanWrite(unit_model.TypeCode) ||
  587. (ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID))
  588. canCompare := false
  589. // Pull request is allowed if this is a fork repository
  590. // and base repository accepts pull requests.
  591. if repo.BaseRepo != nil && repo.BaseRepo.AllowsPulls(ctx) {
  592. canCompare = true
  593. ctx.Data["BaseRepo"] = repo.BaseRepo
  594. ctx.Repo.PullRequest.BaseRepo = repo.BaseRepo
  595. ctx.Repo.PullRequest.Allowed = canPush
  596. } else if repo.AllowsPulls(ctx) {
  597. // Or, this is repository accepts pull requests between branches.
  598. canCompare = true
  599. ctx.Data["BaseRepo"] = repo
  600. ctx.Repo.PullRequest.BaseRepo = repo
  601. ctx.Repo.PullRequest.Allowed = canPush
  602. ctx.Repo.PullRequest.SameRepo = true
  603. }
  604. ctx.Data["CanCompareOrPull"] = canCompare
  605. ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest
  606. if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
  607. repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
  608. if err != nil {
  609. ctx.ServerError("GetPendingRepositoryTransfer", err)
  610. return
  611. }
  612. if err := repoTransfer.LoadAttributes(ctx); err != nil {
  613. ctx.ServerError("LoadRecipient", err)
  614. return
  615. }
  616. ctx.Data["RepoTransfer"] = repoTransfer
  617. if ctx.Doer != nil {
  618. ctx.Data["CanUserAcceptOrRejectTransfer"] = repoTransfer.CanUserAcceptOrRejectTransfer(ctx, ctx.Doer)
  619. }
  620. }
  621. if ctx.FormString("go-get") == "1" {
  622. ctx.Data["GoGetImport"] = ComposeGoGetImport(ctx, repo.Owner.Name, repo.Name)
  623. fullURLPrefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
  624. ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}"
  625. ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}"
  626. }
  627. }
  628. const headRefName = "HEAD"
  629. func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool) string {
  630. refName := ""
  631. parts := strings.Split(path, "/")
  632. for i, part := range parts {
  633. refName = strings.TrimPrefix(refName+"/"+part, "/")
  634. if isExist(refName) {
  635. repo.TreePath = strings.Join(parts[i+1:], "/")
  636. return refName
  637. }
  638. }
  639. return ""
  640. }
  641. func getRefNameLegacy(ctx *Base, repo *Repository, reqPath, extraRef string) (refName string, refType git.RefType, fallbackDefaultBranch bool) {
  642. reqRefPath := path.Join(extraRef, reqPath)
  643. reqRefPathParts := strings.Split(reqRefPath, "/")
  644. if refName := getRefName(ctx, repo, reqRefPath, git.RefTypeBranch); refName != "" {
  645. return refName, git.RefTypeBranch, false
  646. }
  647. if refName := getRefName(ctx, repo, reqRefPath, git.RefTypeTag); refName != "" {
  648. return refName, git.RefTypeTag, false
  649. }
  650. if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), reqRefPathParts[0]) {
  651. // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists
  652. repo.TreePath = strings.Join(reqRefPathParts[1:], "/")
  653. return reqRefPathParts[0], git.RefTypeCommit, false
  654. }
  655. // FIXME: the old code falls back to default branch if "ref" doesn't exist, there could be an edge case:
  656. // "README?ref=no-such" would read the README file from the default branch, but the user might expect a 404
  657. repo.TreePath = reqPath
  658. return repo.Repository.DefaultBranch, git.RefTypeBranch, true
  659. }
  660. func getRefName(ctx *Base, repo *Repository, path string, refType git.RefType) string {
  661. switch refType {
  662. case git.RefTypeBranch:
  663. ref := getRefNameFromPath(repo, path, repo.GitRepo.IsBranchExist)
  664. if len(ref) == 0 {
  665. // check if ref is HEAD
  666. parts := strings.Split(path, "/")
  667. if parts[0] == headRefName {
  668. repo.TreePath = strings.Join(parts[1:], "/")
  669. return repo.Repository.DefaultBranch
  670. }
  671. // maybe it's a renamed branch
  672. return getRefNameFromPath(repo, path, func(s string) bool {
  673. b, exist, err := git_model.FindRenamedBranch(ctx, repo.Repository.ID, s)
  674. if err != nil {
  675. log.Error("FindRenamedBranch: %v", err)
  676. return false
  677. }
  678. if !exist {
  679. return false
  680. }
  681. ctx.Data["IsRenamedBranch"] = true
  682. ctx.Data["RenamedBranchName"] = b.To
  683. return true
  684. })
  685. }
  686. return ref
  687. case git.RefTypeTag:
  688. return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist)
  689. case git.RefTypeCommit:
  690. parts := strings.Split(path, "/")
  691. if git.IsStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) {
  692. // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists
  693. repo.TreePath = strings.Join(parts[1:], "/")
  694. return parts[0]
  695. }
  696. if parts[0] == headRefName {
  697. // HEAD ref points to last default branch commit
  698. commit, err := repo.GitRepo.GetBranchCommit(repo.Repository.DefaultBranch)
  699. if err != nil {
  700. return ""
  701. }
  702. repo.TreePath = strings.Join(parts[1:], "/")
  703. return commit.ID.String()
  704. }
  705. default:
  706. panic(fmt.Sprintf("Unrecognized ref type: %v", refType))
  707. }
  708. return ""
  709. }
  710. func repoRefFullName(typ git.RefType, shortName string) git.RefName {
  711. switch typ {
  712. case git.RefTypeBranch:
  713. return git.RefNameFromBranch(shortName)
  714. case git.RefTypeTag:
  715. return git.RefNameFromTag(shortName)
  716. case git.RefTypeCommit:
  717. return git.RefNameFromCommit(shortName)
  718. default:
  719. setting.PanicInDevOrTesting("Unknown RepoRefType: %v", typ)
  720. return git.RefNameFromBranch("main") // just a dummy result, it shouldn't happen
  721. }
  722. }
  723. func RepoRefByDefaultBranch() func(*Context) {
  724. return func(ctx *Context) {
  725. ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch)
  726. ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
  727. ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
  728. ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount()
  729. ctx.Data["RefFullName"] = ctx.Repo.RefFullName
  730. ctx.Data["BranchName"] = ctx.Repo.BranchName
  731. ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
  732. }
  733. }
  734. // RepoRefByType handles repository reference name for a specific type
  735. // of repository reference
  736. func RepoRefByType(detectRefType git.RefType) func(*Context) {
  737. return func(ctx *Context) {
  738. var err error
  739. refType := detectRefType
  740. if ctx.Repo.Repository.IsBeingCreated() || ctx.Repo.Repository.IsBroken() {
  741. return // no git repo, so do nothing, users will see a "migrating" UI provided by "migrate/migrating.tmpl", or empty repo guide
  742. }
  743. // Empty repository does not have reference information.
  744. if ctx.Repo.Repository.IsEmpty {
  745. // assume the user is viewing the (non-existent) default branch
  746. ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
  747. ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.BranchName)
  748. // these variables are used by the template to "add/upload" new files
  749. ctx.Data["BranchName"] = ctx.Repo.BranchName
  750. ctx.Data["TreePath"] = ""
  751. return
  752. }
  753. // Get default branch.
  754. var refShortName string
  755. reqPath := ctx.PathParam("*")
  756. if reqPath == "" {
  757. refShortName = ctx.Repo.Repository.DefaultBranch
  758. if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, refShortName) {
  759. brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 1)
  760. if err == nil && len(brs) != 0 {
  761. refShortName = brs[0]
  762. } else if len(brs) == 0 {
  763. log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path)
  764. } else {
  765. log.Error("GetBranches error: %v", err)
  766. }
  767. }
  768. ctx.Repo.RefFullName = git.RefNameFromBranch(refShortName)
  769. ctx.Repo.BranchName = refShortName
  770. ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refShortName)
  771. if err == nil {
  772. ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
  773. } else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") {
  774. // if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users
  775. log.Error("GetBranchCommit: %v", err)
  776. } else {
  777. ctx.ServerError("GetBranchCommit", err)
  778. return
  779. }
  780. } else { // there is a path in request
  781. guessLegacyPath := refType == ""
  782. fallbackDefaultBranch := false
  783. if guessLegacyPath {
  784. refShortName, refType, fallbackDefaultBranch = getRefNameLegacy(ctx.Base, ctx.Repo, reqPath, "")
  785. } else {
  786. refShortName = getRefName(ctx.Base, ctx.Repo, reqPath, refType)
  787. }
  788. ctx.Repo.RefFullName = repoRefFullName(refType, refShortName)
  789. isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool)
  790. if isRenamedBranch && has {
  791. renamedBranchName := ctx.Data["RenamedBranchName"].(string)
  792. ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refShortName, renamedBranchName))
  793. link := setting.AppSubURL + strings.Replace(ctx.Req.URL.EscapedPath(), util.PathEscapeSegments(refShortName), util.PathEscapeSegments(renamedBranchName), 1)
  794. ctx.Redirect(link)
  795. return
  796. }
  797. if refType == git.RefTypeBranch && gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, refShortName) {
  798. ctx.Repo.BranchName = refShortName
  799. ctx.Repo.RefFullName = git.RefNameFromBranch(refShortName)
  800. ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refShortName)
  801. if err != nil {
  802. ctx.ServerError("GetBranchCommit", err)
  803. return
  804. }
  805. ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
  806. } else if refType == git.RefTypeTag && gitrepo.IsTagExist(ctx, ctx.Repo.Repository, refShortName) {
  807. ctx.Repo.RefFullName = git.RefNameFromTag(refShortName)
  808. ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refShortName)
  809. if err != nil {
  810. if git.IsErrNotExist(err) {
  811. ctx.NotFound(err)
  812. return
  813. }
  814. ctx.ServerError("GetTagCommit", err)
  815. return
  816. }
  817. ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
  818. } else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refShortName, 7) {
  819. ctx.Repo.RefFullName = git.RefNameFromCommit(refShortName)
  820. ctx.Repo.CommitID = refShortName
  821. ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refShortName)
  822. if err != nil {
  823. ctx.NotFound(err)
  824. return
  825. }
  826. // If short commit ID add canonical link header
  827. if len(refShortName) < ctx.Repo.GetObjectFormat().FullLength() {
  828. canonicalURL := util.URLJoin(httplib.GuessCurrentAppURL(ctx), strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refShortName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))
  829. ctx.RespHeader().Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, canonicalURL))
  830. }
  831. } else {
  832. ctx.NotFound(fmt.Errorf("branch or tag not exist: %s", refShortName))
  833. return
  834. }
  835. if guessLegacyPath {
  836. // redirect from old URL scheme to new URL scheme
  837. // * /user2/repo1/commits/master => /user2/repo1/commits/branch/master
  838. // * /user2/repo1/src/master => /user2/repo1/src/branch/master
  839. // * /user2/repo1/src/README.md => /user2/repo1/src/branch/master/README.md (fallback to default branch)
  840. var redirect string
  841. refSubPath := "src"
  842. // remove the "/subpath/owner/repo/" prefix, the names are case-insensitive
  843. remainingLowerPath, cut := strings.CutPrefix(setting.AppSubURL+strings.ToLower(ctx.Req.URL.Path), strings.ToLower(ctx.Repo.RepoLink)+"/")
  844. if cut {
  845. refSubPath, _, _ = strings.Cut(remainingLowerPath, "/") // it could be "src" or "commits"
  846. }
  847. if fallbackDefaultBranch {
  848. redirect = fmt.Sprintf("%s/%s/%s/%s/%s", ctx.Repo.RepoLink, refSubPath, refType, util.PathEscapeSegments(refShortName), ctx.PathParamRaw("*"))
  849. } else {
  850. redirect = fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, refSubPath, refType, ctx.PathParamRaw("*"))
  851. }
  852. if ctx.Req.URL.RawQuery != "" {
  853. redirect += "?" + ctx.Req.URL.RawQuery
  854. }
  855. ctx.Redirect(redirect)
  856. return
  857. }
  858. }
  859. ctx.Data["RefFullName"] = ctx.Repo.RefFullName
  860. ctx.Data["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
  861. ctx.Data["TreePath"] = ctx.Repo.TreePath
  862. ctx.Data["BranchName"] = ctx.Repo.BranchName
  863. ctx.Data["CommitID"] = ctx.Repo.CommitID
  864. ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() // only used by the branch selector dropdown: AllowCreateNewRef
  865. ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
  866. if err != nil {
  867. ctx.ServerError("GetCommitsCount", err)
  868. return
  869. }
  870. if ctx.Repo.RefFullName.IsTag() {
  871. rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Repo.RefFullName.TagName())
  872. if err == nil && rel.NumCommits <= 0 {
  873. rel.NumCommits = ctx.Repo.CommitsCount
  874. if err := repo_model.UpdateReleaseNumCommits(ctx, rel); err != nil {
  875. log.Error("UpdateReleaseNumCommits", err)
  876. }
  877. }
  878. }
  879. ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
  880. ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache())
  881. }
  882. }
  883. // GitHookService checks if repository Git hooks service has been enabled.
  884. func GitHookService() func(ctx *Context) {
  885. return func(ctx *Context) {
  886. if !ctx.Doer.CanEditGitHook() {
  887. ctx.NotFound(nil)
  888. return
  889. }
  890. }
  891. }
  892. // canWriteAsMaintainer check if the doer can write to a branch as a maintainer
  893. func canWriteAsMaintainer(ctx *Context) bool {
  894. branchName := getRefNameFromPath(ctx.Repo, ctx.PathParam("*"), func(branchName string) bool {
  895. return issues_model.CanMaintainerWriteToBranch(ctx, ctx.Repo.Permission, branchName, ctx.Doer)
  896. })
  897. return len(branchName) > 0
  898. }