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.

repo.go 34KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069
  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. "fmt"
  8. "html"
  9. "net/http"
  10. "net/url"
  11. "path"
  12. "strings"
  13. "code.gitea.io/gitea/models"
  14. "code.gitea.io/gitea/models/db"
  15. git_model "code.gitea.io/gitea/models/git"
  16. issues_model "code.gitea.io/gitea/models/issues"
  17. access_model "code.gitea.io/gitea/models/perm/access"
  18. repo_model "code.gitea.io/gitea/models/repo"
  19. unit_model "code.gitea.io/gitea/models/unit"
  20. user_model "code.gitea.io/gitea/models/user"
  21. "code.gitea.io/gitea/modules/cache"
  22. "code.gitea.io/gitea/modules/git"
  23. code_indexer "code.gitea.io/gitea/modules/indexer/code"
  24. "code.gitea.io/gitea/modules/log"
  25. repo_module "code.gitea.io/gitea/modules/repository"
  26. "code.gitea.io/gitea/modules/setting"
  27. "code.gitea.io/gitea/modules/util"
  28. asymkey_service "code.gitea.io/gitea/services/asymkey"
  29. "github.com/editorconfig/editorconfig-core-go/v2"
  30. )
  31. // PullRequest contains information to make a pull request
  32. type PullRequest struct {
  33. BaseRepo *repo_model.Repository
  34. Allowed bool
  35. SameRepo bool
  36. HeadInfoSubURL string // [<user>:]<branch> url segment
  37. }
  38. // Repository contains information to operate a repository
  39. type Repository struct {
  40. access_model.Permission
  41. IsWatching bool
  42. IsViewBranch bool
  43. IsViewTag bool
  44. IsViewCommit bool
  45. Repository *repo_model.Repository
  46. Owner *user_model.User
  47. Commit *git.Commit
  48. Tag *git.Tag
  49. GitRepo *git.Repository
  50. RefName string
  51. BranchName string
  52. TagName string
  53. TreePath string
  54. CommitID string
  55. RepoLink string
  56. CloneLink repo_model.CloneLink
  57. CommitsCount int64
  58. PullRequest *PullRequest
  59. }
  60. // CanWriteToBranch checks if the branch is writable by the user
  61. func (r *Repository) CanWriteToBranch(user *user_model.User, branch string) bool {
  62. return issues_model.CanMaintainerWriteToBranch(r.Permission, branch, user)
  63. }
  64. // CanEnableEditor returns true if repository is editable and user has proper access level.
  65. func (r *Repository) CanEnableEditor(user *user_model.User) bool {
  66. return r.IsViewBranch && r.CanWriteToBranch(user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived
  67. }
  68. // CanCreateBranch returns true if repository is editable and user has proper access level.
  69. func (r *Repository) CanCreateBranch() bool {
  70. return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
  71. }
  72. // RepoMustNotBeArchived checks if a repo is archived
  73. func RepoMustNotBeArchived() func(ctx *Context) {
  74. return func(ctx *Context) {
  75. if ctx.Repo.Repository.IsArchived {
  76. ctx.NotFound("IsArchived", fmt.Errorf(ctx.Tr("repo.archive.title")))
  77. }
  78. }
  79. }
  80. // CanCommitToBranchResults represents the results of CanCommitToBranch
  81. type CanCommitToBranchResults struct {
  82. CanCommitToBranch bool
  83. EditorEnabled bool
  84. UserCanPush bool
  85. RequireSigned bool
  86. WillSign bool
  87. SigningKey string
  88. WontSignReason string
  89. }
  90. // CanCommitToBranch returns true if repository is editable and user has proper access level
  91. //
  92. // and branch is not protected for push
  93. func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.User) (CanCommitToBranchResults, error) {
  94. protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
  95. if err != nil {
  96. return CanCommitToBranchResults{}, err
  97. }
  98. userCanPush := true
  99. requireSigned := false
  100. if protectedBranch != nil {
  101. protectedBranch.Repo = r.Repository
  102. userCanPush = protectedBranch.CanUserPush(ctx, doer)
  103. requireSigned = protectedBranch.RequireSignedCommits
  104. }
  105. sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName)
  106. canCommit := r.CanEnableEditor(doer) && userCanPush
  107. if requireSigned {
  108. canCommit = canCommit && sign
  109. }
  110. wontSignReason := ""
  111. if err != nil {
  112. if asymkey_service.IsErrWontSign(err) {
  113. wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
  114. err = nil
  115. } else {
  116. wontSignReason = "error"
  117. }
  118. }
  119. return CanCommitToBranchResults{
  120. CanCommitToBranch: canCommit,
  121. EditorEnabled: r.CanEnableEditor(doer),
  122. UserCanPush: userCanPush,
  123. RequireSigned: requireSigned,
  124. WillSign: sign,
  125. SigningKey: keyID,
  126. WontSignReason: wontSignReason,
  127. }, err
  128. }
  129. // CanUseTimetracker returns whether or not a user can use the timetracker.
  130. func (r *Repository) CanUseTimetracker(issue *issues_model.Issue, user *user_model.User) bool {
  131. // Checking for following:
  132. // 1. Is timetracker enabled
  133. // 2. Is the user a contributor, admin, poster or assignee and do the repository policies require this?
  134. isAssigned, _ := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user)
  135. return r.Repository.IsTimetrackerEnabled(db.DefaultContext) && (!r.Repository.AllowOnlyContributorsToTrackTime(db.DefaultContext) ||
  136. r.Permission.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsPoster(user.ID) || isAssigned)
  137. }
  138. // CanCreateIssueDependencies returns whether or not a user can create dependencies.
  139. func (r *Repository) CanCreateIssueDependencies(user *user_model.User, isPull bool) bool {
  140. return r.Repository.IsDependenciesEnabled(db.DefaultContext) && r.Permission.CanWriteIssuesOrPulls(isPull)
  141. }
  142. // GetCommitsCount returns cached commit count for current view
  143. func (r *Repository) GetCommitsCount() (int64, error) {
  144. if r.Commit == nil {
  145. return 0, nil
  146. }
  147. var contextName string
  148. if r.IsViewBranch {
  149. contextName = r.BranchName
  150. } else if r.IsViewTag {
  151. contextName = r.TagName
  152. } else {
  153. contextName = r.CommitID
  154. }
  155. return cache.GetInt64(r.Repository.GetCommitsCountCacheKey(contextName, r.IsViewBranch || r.IsViewTag), func() (int64, error) {
  156. return r.Commit.CommitsCount()
  157. })
  158. }
  159. // GetCommitGraphsCount returns cached commit count for current view
  160. func (r *Repository) GetCommitGraphsCount(ctx context.Context, hidePRRefs bool, branches, files []string) (int64, error) {
  161. cacheKey := fmt.Sprintf("commits-count-%d-graph-%t-%s-%s", r.Repository.ID, hidePRRefs, branches, files)
  162. return cache.GetInt64(cacheKey, func() (int64, error) {
  163. if len(branches) == 0 {
  164. return git.AllCommitsCount(ctx, r.Repository.RepoPath(), hidePRRefs, files...)
  165. }
  166. return git.CommitsCount(ctx,
  167. git.CommitsCountOptions{
  168. RepoPath: r.Repository.RepoPath(),
  169. Revision: branches,
  170. RelPath: files,
  171. })
  172. })
  173. }
  174. // BranchNameSubURL sub-URL for the BranchName field
  175. func (r *Repository) BranchNameSubURL() string {
  176. switch {
  177. case r.IsViewBranch:
  178. return "branch/" + util.PathEscapeSegments(r.BranchName)
  179. case r.IsViewTag:
  180. return "tag/" + util.PathEscapeSegments(r.TagName)
  181. case r.IsViewCommit:
  182. return "commit/" + util.PathEscapeSegments(r.CommitID)
  183. }
  184. log.Error("Unknown view type for repo: %v", r)
  185. return ""
  186. }
  187. // FileExists returns true if a file exists in the given repo branch
  188. func (r *Repository) FileExists(path, branch string) (bool, error) {
  189. if branch == "" {
  190. branch = r.Repository.DefaultBranch
  191. }
  192. commit, err := r.GitRepo.GetBranchCommit(branch)
  193. if err != nil {
  194. return false, err
  195. }
  196. if _, err := commit.GetTreeEntryByPath(path); err != nil {
  197. return false, err
  198. }
  199. return true, nil
  200. }
  201. // GetEditorconfig returns the .editorconfig definition if found in the
  202. // HEAD of the default repo branch.
  203. func (r *Repository) GetEditorconfig(optCommit ...*git.Commit) (cfg *editorconfig.Editorconfig, warning, err error) {
  204. if r.GitRepo == nil {
  205. return nil, nil, nil
  206. }
  207. var commit *git.Commit
  208. if len(optCommit) != 0 {
  209. commit = optCommit[0]
  210. } else {
  211. commit, err = r.GitRepo.GetBranchCommit(r.Repository.DefaultBranch)
  212. if err != nil {
  213. return nil, nil, err
  214. }
  215. }
  216. treeEntry, err := commit.GetTreeEntryByPath(".editorconfig")
  217. if err != nil {
  218. return nil, nil, err
  219. }
  220. if treeEntry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
  221. return nil, nil, git.ErrNotExist{ID: "", RelPath: ".editorconfig"}
  222. }
  223. reader, err := treeEntry.Blob().DataAsync()
  224. if err != nil {
  225. return nil, nil, err
  226. }
  227. defer reader.Close()
  228. return editorconfig.ParseGraceful(reader)
  229. }
  230. // RetrieveBaseRepo retrieves base repository
  231. func RetrieveBaseRepo(ctx *Context, repo *repo_model.Repository) {
  232. // Non-fork repository will not return error in this method.
  233. if err := repo.GetBaseRepo(ctx); err != nil {
  234. if repo_model.IsErrRepoNotExist(err) {
  235. repo.IsFork = false
  236. repo.ForkID = 0
  237. return
  238. }
  239. ctx.ServerError("GetBaseRepo", err)
  240. return
  241. } else if err = repo.BaseRepo.LoadOwner(ctx); err != nil {
  242. ctx.ServerError("BaseRepo.LoadOwner", err)
  243. return
  244. }
  245. }
  246. // RetrieveTemplateRepo retrieves template repository used to generate this repository
  247. func RetrieveTemplateRepo(ctx *Context, repo *repo_model.Repository) {
  248. // Non-generated repository will not return error in this method.
  249. templateRepo, err := repo_model.GetTemplateRepo(ctx, repo)
  250. if err != nil {
  251. if repo_model.IsErrRepoNotExist(err) {
  252. repo.TemplateID = 0
  253. return
  254. }
  255. ctx.ServerError("GetTemplateRepo", err)
  256. return
  257. } else if err = templateRepo.LoadOwner(ctx); err != nil {
  258. ctx.ServerError("TemplateRepo.LoadOwner", err)
  259. return
  260. }
  261. perm, err := access_model.GetUserRepoPermission(ctx, templateRepo, ctx.Doer)
  262. if err != nil {
  263. ctx.ServerError("GetUserRepoPermission", err)
  264. return
  265. }
  266. if !perm.CanRead(unit_model.TypeCode) {
  267. repo.TemplateID = 0
  268. }
  269. }
  270. // ComposeGoGetImport returns go-get-import meta content.
  271. func ComposeGoGetImport(owner, repo string) string {
  272. /// setting.AppUrl is guaranteed to be parse as url
  273. appURL, _ := url.Parse(setting.AppURL)
  274. return path.Join(appURL.Host, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo))
  275. }
  276. // EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200
  277. // if user does not have actual access to the requested repository,
  278. // or the owner or repository does not exist at all.
  279. // This is particular a workaround for "go get" command which does not respect
  280. // .netrc file.
  281. func EarlyResponseForGoGetMeta(ctx *Context) {
  282. username := ctx.Params(":username")
  283. reponame := strings.TrimSuffix(ctx.Params(":reponame"), ".git")
  284. if username == "" || reponame == "" {
  285. ctx.PlainText(http.StatusBadRequest, "invalid repository path")
  286. return
  287. }
  288. var cloneURL string
  289. if setting.Repository.GoGetCloneURLProtocol == "ssh" {
  290. cloneURL = repo_model.ComposeSSHCloneURL(username, reponame)
  291. } else {
  292. cloneURL = repo_model.ComposeHTTPSCloneURL(username, reponame)
  293. }
  294. goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(username, reponame), cloneURL)
  295. htmlMeta := fmt.Sprintf(`<meta name="go-import" content="%s">`, html.EscapeString(goImportContent))
  296. ctx.PlainText(http.StatusOK, htmlMeta)
  297. }
  298. // RedirectToRepo redirect to a differently-named repository
  299. func RedirectToRepo(ctx *Base, redirectRepoID int64) {
  300. ownerName := ctx.Params(":username")
  301. previousRepoName := ctx.Params(":reponame")
  302. repo, err := repo_model.GetRepositoryByID(ctx, redirectRepoID)
  303. if err != nil {
  304. log.Error("GetRepositoryByID: %v", err)
  305. ctx.Error(http.StatusInternalServerError, "GetRepositoryByID")
  306. return
  307. }
  308. redirectPath := strings.Replace(
  309. ctx.Req.URL.EscapedPath(),
  310. url.PathEscape(ownerName)+"/"+url.PathEscape(previousRepoName),
  311. url.PathEscape(repo.OwnerName)+"/"+url.PathEscape(repo.Name),
  312. 1,
  313. )
  314. if ctx.Req.URL.RawQuery != "" {
  315. redirectPath += "?" + ctx.Req.URL.RawQuery
  316. }
  317. ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
  318. }
  319. func repoAssignment(ctx *Context, repo *repo_model.Repository) {
  320. var err error
  321. if err = repo.LoadOwner(ctx); err != nil {
  322. ctx.ServerError("LoadOwner", err)
  323. return
  324. }
  325. ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
  326. if err != nil {
  327. ctx.ServerError("GetUserRepoPermission", err)
  328. return
  329. }
  330. // Check access.
  331. if !ctx.Repo.Permission.HasAccess() {
  332. if ctx.FormString("go-get") == "1" {
  333. EarlyResponseForGoGetMeta(ctx)
  334. return
  335. }
  336. ctx.NotFound("no access right", nil)
  337. return
  338. }
  339. ctx.Data["HasAccess"] = true
  340. ctx.Data["Permission"] = &ctx.Repo.Permission
  341. if repo.IsMirror {
  342. pullMirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
  343. if err == nil {
  344. ctx.Data["PullMirror"] = pullMirror
  345. } else if err != repo_model.ErrMirrorNotExist {
  346. ctx.ServerError("GetMirrorByRepoID", err)
  347. return
  348. }
  349. }
  350. pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, db.ListOptions{})
  351. if err != nil {
  352. ctx.ServerError("GetPushMirrorsByRepoID", err)
  353. return
  354. }
  355. ctx.Repo.Repository = repo
  356. ctx.Data["PushMirrors"] = pushMirrors
  357. ctx.Data["RepoName"] = ctx.Repo.Repository.Name
  358. ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
  359. }
  360. // RepoIDAssignment returns a handler which assigns the repo to the context.
  361. func RepoIDAssignment() func(ctx *Context) {
  362. return func(ctx *Context) {
  363. repoID := ctx.ParamsInt64(":repoid")
  364. // Get repository.
  365. repo, err := repo_model.GetRepositoryByID(ctx, repoID)
  366. if err != nil {
  367. if repo_model.IsErrRepoNotExist(err) {
  368. ctx.NotFound("GetRepositoryByID", nil)
  369. } else {
  370. ctx.ServerError("GetRepositoryByID", err)
  371. }
  372. return
  373. }
  374. repoAssignment(ctx, repo)
  375. }
  376. }
  377. // RepoAssignment returns a middleware to handle repository assignment
  378. func RepoAssignment(ctx *Context) context.CancelFunc {
  379. if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce {
  380. log.Trace("RepoAssignment was exec already, skipping second call ...")
  381. return nil
  382. }
  383. ctx.Data["repoAssignmentExecuted"] = true
  384. var (
  385. owner *user_model.User
  386. err error
  387. )
  388. userName := ctx.Params(":username")
  389. repoName := ctx.Params(":reponame")
  390. repoName = strings.TrimSuffix(repoName, ".git")
  391. if setting.Other.EnableFeed {
  392. repoName = strings.TrimSuffix(repoName, ".rss")
  393. repoName = strings.TrimSuffix(repoName, ".atom")
  394. }
  395. // Check if the user is the same as the repository owner
  396. if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) {
  397. owner = ctx.Doer
  398. } else {
  399. owner, err = user_model.GetUserByName(ctx, userName)
  400. if err != nil {
  401. if user_model.IsErrUserNotExist(err) {
  402. // go-get does not support redirects
  403. // https://github.com/golang/go/issues/19760
  404. if ctx.FormString("go-get") == "1" {
  405. EarlyResponseForGoGetMeta(ctx)
  406. return nil
  407. }
  408. if redirectUserID, err := user_model.LookupUserRedirect(userName); err == nil {
  409. RedirectToUser(ctx.Base, userName, redirectUserID)
  410. } else if user_model.IsErrUserRedirectNotExist(err) {
  411. ctx.NotFound("GetUserByName", nil)
  412. } else {
  413. ctx.ServerError("LookupUserRedirect", err)
  414. }
  415. } else {
  416. ctx.ServerError("GetUserByName", err)
  417. }
  418. return nil
  419. }
  420. }
  421. ctx.Repo.Owner = owner
  422. ctx.ContextUser = owner
  423. ctx.Data["Username"] = ctx.Repo.Owner.Name
  424. // redirect link to wiki
  425. if strings.HasSuffix(repoName, ".wiki") {
  426. // ctx.Req.URL.Path does not have the preceding appSubURL - any redirect must have this added
  427. // Now we happen to know that all of our paths are: /:username/:reponame/whatever_else
  428. originalRepoName := ctx.Params(":reponame")
  429. redirectRepoName := strings.TrimSuffix(repoName, ".wiki")
  430. redirectRepoName += originalRepoName[len(redirectRepoName)+5:]
  431. redirectPath := strings.Replace(
  432. ctx.Req.URL.EscapedPath(),
  433. url.PathEscape(userName)+"/"+url.PathEscape(originalRepoName),
  434. url.PathEscape(userName)+"/"+url.PathEscape(redirectRepoName)+"/wiki",
  435. 1,
  436. )
  437. if ctx.Req.URL.RawQuery != "" {
  438. redirectPath += "?" + ctx.Req.URL.RawQuery
  439. }
  440. ctx.Redirect(path.Join(setting.AppSubURL, redirectPath))
  441. return nil
  442. }
  443. // Get repository.
  444. repo, err := repo_model.GetRepositoryByName(owner.ID, repoName)
  445. if err != nil {
  446. if repo_model.IsErrRepoNotExist(err) {
  447. redirectRepoID, err := repo_model.LookupRedirect(owner.ID, repoName)
  448. if err == nil {
  449. RedirectToRepo(ctx.Base, redirectRepoID)
  450. } else if repo_model.IsErrRedirectNotExist(err) {
  451. if ctx.FormString("go-get") == "1" {
  452. EarlyResponseForGoGetMeta(ctx)
  453. return nil
  454. }
  455. ctx.NotFound("GetRepositoryByName", nil)
  456. } else {
  457. ctx.ServerError("LookupRepoRedirect", err)
  458. }
  459. } else {
  460. ctx.ServerError("GetRepositoryByName", err)
  461. }
  462. return nil
  463. }
  464. repo.Owner = owner
  465. repoAssignment(ctx, repo)
  466. if ctx.Written() {
  467. return nil
  468. }
  469. ctx.Repo.RepoLink = repo.Link()
  470. ctx.Data["RepoLink"] = ctx.Repo.RepoLink
  471. ctx.Data["RepoRelPath"] = ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name
  472. if setting.Other.EnableFeed {
  473. ctx.Data["EnableFeed"] = true
  474. ctx.Data["FeedURL"] = ctx.Repo.RepoLink
  475. }
  476. unit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeExternalTracker)
  477. if err == nil {
  478. ctx.Data["RepoExternalIssuesLink"] = unit.ExternalTrackerConfig().ExternalTrackerURL
  479. }
  480. ctx.Data["NumTags"], err = repo_model.GetReleaseCountByRepoID(ctx, ctx.Repo.Repository.ID, repo_model.FindReleasesOptions{
  481. IncludeDrafts: true,
  482. IncludeTags: true,
  483. HasSha1: util.OptionalBoolTrue, // only draft releases which are created with existing tags
  484. })
  485. if err != nil {
  486. ctx.ServerError("GetReleaseCountByRepoID", err)
  487. return nil
  488. }
  489. ctx.Data["NumReleases"], err = repo_model.GetReleaseCountByRepoID(ctx, ctx.Repo.Repository.ID, repo_model.FindReleasesOptions{})
  490. if err != nil {
  491. ctx.ServerError("GetReleaseCountByRepoID", err)
  492. return nil
  493. }
  494. ctx.Data["Title"] = owner.Name + "/" + repo.Name
  495. ctx.Data["Repository"] = repo
  496. ctx.Data["Owner"] = ctx.Repo.Repository.Owner
  497. ctx.Data["IsRepositoryOwner"] = ctx.Repo.IsOwner()
  498. ctx.Data["IsRepositoryAdmin"] = ctx.Repo.IsAdmin()
  499. ctx.Data["RepoOwnerIsOrganization"] = repo.Owner.IsOrganization()
  500. ctx.Data["CanWriteCode"] = ctx.Repo.CanWrite(unit_model.TypeCode)
  501. ctx.Data["CanWriteIssues"] = ctx.Repo.CanWrite(unit_model.TypeIssues)
  502. ctx.Data["CanWritePulls"] = ctx.Repo.CanWrite(unit_model.TypePullRequests)
  503. canSignedUserFork, err := repo_module.CanUserForkRepo(ctx.Doer, ctx.Repo.Repository)
  504. if err != nil {
  505. ctx.ServerError("CanUserForkRepo", err)
  506. return nil
  507. }
  508. ctx.Data["CanSignedUserFork"] = canSignedUserFork
  509. userAndOrgForks, err := repo_model.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository)
  510. if err != nil {
  511. ctx.ServerError("GetForksByUserAndOrgs", err)
  512. return nil
  513. }
  514. ctx.Data["UserAndOrgForks"] = userAndOrgForks
  515. // canSignedUserFork is true if the current user doesn't have a fork of this repo yet or
  516. // if he owns an org that doesn't have a fork of this repo yet
  517. // If multiple forks are available or if the user can fork to another account, but there is already a fork: open selection dialog
  518. ctx.Data["ShowForkModal"] = len(userAndOrgForks) > 1 || (canSignedUserFork && len(userAndOrgForks) > 0)
  519. ctx.Data["RepoCloneLink"] = repo.CloneLink()
  520. cloneButtonShowHTTPS := !setting.Repository.DisableHTTPGit
  521. cloneButtonShowSSH := !setting.SSH.Disabled && (ctx.IsSigned || setting.SSH.ExposeAnonymous)
  522. if !cloneButtonShowHTTPS && !cloneButtonShowSSH {
  523. // We have to show at least one link, so we just show the HTTPS
  524. cloneButtonShowHTTPS = true
  525. }
  526. ctx.Data["CloneButtonShowHTTPS"] = cloneButtonShowHTTPS
  527. ctx.Data["CloneButtonShowSSH"] = cloneButtonShowSSH
  528. ctx.Data["CloneButtonOriginLink"] = ctx.Data["RepoCloneLink"] // it may be rewritten to the WikiCloneLink by the router middleware
  529. ctx.Data["RepoSearchEnabled"] = setting.Indexer.RepoIndexerEnabled
  530. if setting.Indexer.RepoIndexerEnabled {
  531. ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
  532. }
  533. if ctx.IsSigned {
  534. ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx.Doer.ID, repo.ID)
  535. ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID)
  536. }
  537. if repo.IsFork {
  538. RetrieveBaseRepo(ctx, repo)
  539. if ctx.Written() {
  540. return nil
  541. }
  542. }
  543. if repo.IsGenerated() {
  544. RetrieveTemplateRepo(ctx, repo)
  545. if ctx.Written() {
  546. return nil
  547. }
  548. }
  549. isHomeOrSettings := ctx.Link == ctx.Repo.RepoLink || ctx.Link == ctx.Repo.RepoLink+"/settings" || strings.HasPrefix(ctx.Link, ctx.Repo.RepoLink+"/settings/")
  550. // Disable everything when the repo is being created
  551. if ctx.Repo.Repository.IsBeingCreated() || ctx.Repo.Repository.IsBroken() {
  552. ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch
  553. if !isHomeOrSettings {
  554. ctx.Redirect(ctx.Repo.RepoLink)
  555. }
  556. return nil
  557. }
  558. gitRepo, err := git.OpenRepository(ctx, repo_model.RepoPath(userName, repoName))
  559. if err != nil {
  560. if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") {
  561. log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err)
  562. ctx.Repo.Repository.MarkAsBrokenEmpty()
  563. ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch
  564. // Only allow access to base of repo or settings
  565. if !isHomeOrSettings {
  566. ctx.Redirect(ctx.Repo.RepoLink)
  567. }
  568. return nil
  569. }
  570. ctx.ServerError("RepoAssignment Invalid repo "+repo_model.RepoPath(userName, repoName), err)
  571. return nil
  572. }
  573. if ctx.Repo.GitRepo != nil {
  574. ctx.Repo.GitRepo.Close()
  575. }
  576. ctx.Repo.GitRepo = gitRepo
  577. // We opened it, we should close it
  578. cancel := func() {
  579. // If it's been set to nil then assume someone else has closed it.
  580. if ctx.Repo.GitRepo != nil {
  581. ctx.Repo.GitRepo.Close()
  582. }
  583. }
  584. // Stop at this point when the repo is empty.
  585. if ctx.Repo.Repository.IsEmpty {
  586. ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch
  587. return cancel
  588. }
  589. tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
  590. if err != nil {
  591. ctx.ServerError("GetTagNamesByRepoID", err)
  592. return cancel
  593. }
  594. ctx.Data["Tags"] = tags
  595. branchOpts := git_model.FindBranchOptions{
  596. RepoID: ctx.Repo.Repository.ID,
  597. IsDeletedBranch: util.OptionalBoolFalse,
  598. ListOptions: db.ListOptions{
  599. ListAll: true,
  600. },
  601. }
  602. branchesTotal, err := git_model.CountBranches(ctx, branchOpts)
  603. if err != nil {
  604. ctx.ServerError("CountBranches", err)
  605. return cancel
  606. }
  607. // non empty repo should have at least 1 branch, so this repository's branches haven't been synced yet
  608. if branchesTotal == 0 { // fallback to do a sync immediately
  609. branchesTotal, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0)
  610. if err != nil {
  611. ctx.ServerError("SyncRepoBranches", err)
  612. return cancel
  613. }
  614. }
  615. // FIXME: use paganation and async loading
  616. branchOpts.ExcludeBranchNames = []string{ctx.Repo.Repository.DefaultBranch}
  617. brs, err := git_model.FindBranchNames(ctx, branchOpts)
  618. if err != nil {
  619. ctx.ServerError("GetBranches", err)
  620. return cancel
  621. }
  622. // always put default branch on the top
  623. ctx.Data["Branches"] = append(branchOpts.ExcludeBranchNames, brs...)
  624. ctx.Data["BranchesCount"] = branchesTotal
  625. // If not branch selected, try default one.
  626. // If default branch doesn't exist, fall back to some other branch.
  627. if len(ctx.Repo.BranchName) == 0 {
  628. if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) {
  629. ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
  630. } else if len(brs) > 0 {
  631. ctx.Repo.BranchName = brs[0]
  632. }
  633. ctx.Repo.RefName = ctx.Repo.BranchName
  634. }
  635. ctx.Data["BranchName"] = ctx.Repo.BranchName
  636. // People who have push access or have forked repository can propose a new pull request.
  637. canPush := ctx.Repo.CanWrite(unit_model.TypeCode) ||
  638. (ctx.IsSigned && repo_model.HasForkedRepo(ctx.Doer.ID, ctx.Repo.Repository.ID))
  639. canCompare := false
  640. // Pull request is allowed if this is a fork repository
  641. // and base repository accepts pull requests.
  642. if repo.BaseRepo != nil && repo.BaseRepo.AllowsPulls() {
  643. canCompare = true
  644. ctx.Data["BaseRepo"] = repo.BaseRepo
  645. ctx.Repo.PullRequest.BaseRepo = repo.BaseRepo
  646. ctx.Repo.PullRequest.Allowed = canPush
  647. ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Repo.Owner.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName)
  648. } else if repo.AllowsPulls() {
  649. // Or, this is repository accepts pull requests between branches.
  650. canCompare = true
  651. ctx.Data["BaseRepo"] = repo
  652. ctx.Repo.PullRequest.BaseRepo = repo
  653. ctx.Repo.PullRequest.Allowed = canPush
  654. ctx.Repo.PullRequest.SameRepo = true
  655. ctx.Repo.PullRequest.HeadInfoSubURL = util.PathEscapeSegments(ctx.Repo.BranchName)
  656. }
  657. ctx.Data["CanCompareOrPull"] = canCompare
  658. ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest
  659. if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
  660. repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
  661. if err != nil {
  662. ctx.ServerError("GetPendingRepositoryTransfer", err)
  663. return cancel
  664. }
  665. if err := repoTransfer.LoadAttributes(ctx); err != nil {
  666. ctx.ServerError("LoadRecipient", err)
  667. return cancel
  668. }
  669. ctx.Data["RepoTransfer"] = repoTransfer
  670. if ctx.Doer != nil {
  671. ctx.Data["CanUserAcceptTransfer"] = repoTransfer.CanUserAcceptTransfer(ctx.Doer)
  672. }
  673. }
  674. if ctx.FormString("go-get") == "1" {
  675. ctx.Data["GoGetImport"] = ComposeGoGetImport(owner.Name, repo.Name)
  676. fullURLPrefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
  677. ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}"
  678. ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}"
  679. }
  680. return cancel
  681. }
  682. // RepoRefType type of repo reference
  683. type RepoRefType int
  684. const (
  685. // RepoRefLegacy unknown type, make educated guess and redirect.
  686. // for backward compatibility with previous URL scheme
  687. RepoRefLegacy RepoRefType = iota
  688. // RepoRefAny is for usage where educated guess is needed
  689. // but redirect can not be made
  690. RepoRefAny
  691. // RepoRefBranch branch
  692. RepoRefBranch
  693. // RepoRefTag tag
  694. RepoRefTag
  695. // RepoRefCommit commit
  696. RepoRefCommit
  697. // RepoRefBlob blob
  698. RepoRefBlob
  699. )
  700. // RepoRef handles repository reference names when the ref name is not
  701. // explicitly given
  702. func RepoRef() func(*Context) context.CancelFunc {
  703. // since no ref name is explicitly specified, ok to just use branch
  704. return RepoRefByType(RepoRefBranch)
  705. }
  706. // RefTypeIncludesBranches returns true if ref type can be a branch
  707. func (rt RepoRefType) RefTypeIncludesBranches() bool {
  708. if rt == RepoRefLegacy || rt == RepoRefAny || rt == RepoRefBranch {
  709. return true
  710. }
  711. return false
  712. }
  713. // RefTypeIncludesTags returns true if ref type can be a tag
  714. func (rt RepoRefType) RefTypeIncludesTags() bool {
  715. if rt == RepoRefLegacy || rt == RepoRefAny || rt == RepoRefTag {
  716. return true
  717. }
  718. return false
  719. }
  720. func getRefNameFromPath(ctx *Base, repo *Repository, path string, isExist func(string) bool) string {
  721. refName := ""
  722. parts := strings.Split(path, "/")
  723. for i, part := range parts {
  724. refName = strings.TrimPrefix(refName+"/"+part, "/")
  725. if isExist(refName) {
  726. repo.TreePath = strings.Join(parts[i+1:], "/")
  727. return refName
  728. }
  729. }
  730. return ""
  731. }
  732. func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
  733. path := ctx.Params("*")
  734. switch pathType {
  735. case RepoRefLegacy, RepoRefAny:
  736. if refName := getRefName(ctx, repo, RepoRefBranch); len(refName) > 0 {
  737. return refName
  738. }
  739. if refName := getRefName(ctx, repo, RepoRefTag); len(refName) > 0 {
  740. return refName
  741. }
  742. // For legacy and API support only full commit sha
  743. parts := strings.Split(path, "/")
  744. if len(parts) > 0 && len(parts[0]) == git.SHAFullLength {
  745. repo.TreePath = strings.Join(parts[1:], "/")
  746. return parts[0]
  747. }
  748. if refName := getRefName(ctx, repo, RepoRefBlob); len(refName) > 0 {
  749. return refName
  750. }
  751. repo.TreePath = path
  752. return repo.Repository.DefaultBranch
  753. case RepoRefBranch:
  754. ref := getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsBranchExist)
  755. if len(ref) == 0 {
  756. // maybe it's a renamed branch
  757. return getRefNameFromPath(ctx, repo, path, func(s string) bool {
  758. b, exist, err := git_model.FindRenamedBranch(ctx, repo.Repository.ID, s)
  759. if err != nil {
  760. log.Error("FindRenamedBranch", err)
  761. return false
  762. }
  763. if !exist {
  764. return false
  765. }
  766. ctx.Data["IsRenamedBranch"] = true
  767. ctx.Data["RenamedBranchName"] = b.To
  768. return true
  769. })
  770. }
  771. return ref
  772. case RepoRefTag:
  773. return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist)
  774. case RepoRefCommit:
  775. parts := strings.Split(path, "/")
  776. if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= git.SHAFullLength {
  777. repo.TreePath = strings.Join(parts[1:], "/")
  778. return parts[0]
  779. }
  780. case RepoRefBlob:
  781. _, err := repo.GitRepo.GetBlob(path)
  782. if err != nil {
  783. return ""
  784. }
  785. return path
  786. default:
  787. log.Error("Unrecognized path type: %v", path)
  788. }
  789. return ""
  790. }
  791. // RepoRefByType handles repository reference name for a specific type
  792. // of repository reference
  793. func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context) context.CancelFunc {
  794. return func(ctx *Context) (cancel context.CancelFunc) {
  795. // Empty repository does not have reference information.
  796. if ctx.Repo.Repository.IsEmpty {
  797. // assume the user is viewing the (non-existent) default branch
  798. ctx.Repo.IsViewBranch = true
  799. ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
  800. ctx.Data["TreePath"] = ""
  801. return nil
  802. }
  803. var (
  804. refName string
  805. err error
  806. )
  807. if ctx.Repo.GitRepo == nil {
  808. repoPath := repo_model.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
  809. ctx.Repo.GitRepo, err = git.OpenRepository(ctx, repoPath)
  810. if err != nil {
  811. ctx.ServerError("RepoRef Invalid repo "+repoPath, err)
  812. return nil
  813. }
  814. // We opened it, we should close it
  815. cancel = func() {
  816. // If it's been set to nil then assume someone else has closed it.
  817. if ctx.Repo.GitRepo != nil {
  818. ctx.Repo.GitRepo.Close()
  819. }
  820. }
  821. }
  822. // Get default branch.
  823. if len(ctx.Params("*")) == 0 {
  824. refName = ctx.Repo.Repository.DefaultBranch
  825. if !ctx.Repo.GitRepo.IsBranchExist(refName) {
  826. brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 1)
  827. if err == nil && len(brs) != 0 {
  828. refName = brs[0].Name
  829. } else if len(brs) == 0 {
  830. log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path)
  831. ctx.Repo.Repository.MarkAsBrokenEmpty()
  832. } else {
  833. log.Error("GetBranches error: %v", err)
  834. ctx.Repo.Repository.MarkAsBrokenEmpty()
  835. }
  836. }
  837. ctx.Repo.RefName = refName
  838. ctx.Repo.BranchName = refName
  839. ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
  840. if err == nil {
  841. ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
  842. } else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") {
  843. // if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users
  844. log.Error("GetBranchCommit: %v", err)
  845. ctx.Repo.Repository.MarkAsBrokenEmpty()
  846. } else {
  847. ctx.ServerError("GetBranchCommit", err)
  848. return cancel
  849. }
  850. ctx.Repo.IsViewBranch = true
  851. } else {
  852. refName = getRefName(ctx.Base, ctx.Repo, refType)
  853. ctx.Repo.RefName = refName
  854. isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool)
  855. if isRenamedBranch && has {
  856. renamedBranchName := ctx.Data["RenamedBranchName"].(string)
  857. ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refName, renamedBranchName))
  858. link := setting.AppSubURL + strings.Replace(ctx.Req.URL.EscapedPath(), util.PathEscapeSegments(refName), util.PathEscapeSegments(renamedBranchName), 1)
  859. ctx.Redirect(link)
  860. return cancel
  861. }
  862. if refType.RefTypeIncludesBranches() && ctx.Repo.GitRepo.IsBranchExist(refName) {
  863. ctx.Repo.IsViewBranch = true
  864. ctx.Repo.BranchName = refName
  865. ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
  866. if err != nil {
  867. ctx.ServerError("GetBranchCommit", err)
  868. return cancel
  869. }
  870. ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
  871. } else if refType.RefTypeIncludesTags() && ctx.Repo.GitRepo.IsTagExist(refName) {
  872. ctx.Repo.IsViewTag = true
  873. ctx.Repo.TagName = refName
  874. ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refName)
  875. if err != nil {
  876. if git.IsErrNotExist(err) {
  877. ctx.NotFound("GetTagCommit", err)
  878. return cancel
  879. }
  880. ctx.ServerError("GetTagCommit", err)
  881. return cancel
  882. }
  883. ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
  884. } else if len(refName) >= 7 && len(refName) <= git.SHAFullLength {
  885. ctx.Repo.IsViewCommit = true
  886. ctx.Repo.CommitID = refName
  887. ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
  888. if err != nil {
  889. ctx.NotFound("GetCommit", err)
  890. return cancel
  891. }
  892. // If short commit ID add canonical link header
  893. if len(refName) < git.SHAFullLength {
  894. ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"",
  895. util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))))
  896. }
  897. } else {
  898. if len(ignoreNotExistErr) > 0 && ignoreNotExistErr[0] {
  899. return cancel
  900. }
  901. ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refName))
  902. return cancel
  903. }
  904. if refType == RepoRefLegacy {
  905. // redirect from old URL scheme to new URL scheme
  906. prefix := strings.TrimPrefix(setting.AppSubURL+strings.ToLower(strings.TrimSuffix(ctx.Req.URL.Path, ctx.Params("*"))), strings.ToLower(ctx.Repo.RepoLink))
  907. ctx.Redirect(path.Join(
  908. ctx.Repo.RepoLink,
  909. util.PathEscapeSegments(prefix),
  910. ctx.Repo.BranchNameSubURL(),
  911. util.PathEscapeSegments(ctx.Repo.TreePath)))
  912. return cancel
  913. }
  914. }
  915. ctx.Data["BranchName"] = ctx.Repo.BranchName
  916. ctx.Data["RefName"] = ctx.Repo.RefName
  917. ctx.Data["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
  918. ctx.Data["TagName"] = ctx.Repo.TagName
  919. ctx.Data["CommitID"] = ctx.Repo.CommitID
  920. ctx.Data["TreePath"] = ctx.Repo.TreePath
  921. ctx.Data["IsViewBranch"] = ctx.Repo.IsViewBranch
  922. ctx.Data["IsViewTag"] = ctx.Repo.IsViewTag
  923. ctx.Data["IsViewCommit"] = ctx.Repo.IsViewCommit
  924. ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch()
  925. ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
  926. if err != nil {
  927. ctx.ServerError("GetCommitsCount", err)
  928. return cancel
  929. }
  930. ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
  931. ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache())
  932. return cancel
  933. }
  934. }
  935. // GitHookService checks if repository Git hooks service has been enabled.
  936. func GitHookService() func(ctx *Context) {
  937. return func(ctx *Context) {
  938. if !ctx.Doer.CanEditGitHook() {
  939. ctx.NotFound("GitHookService", nil)
  940. return
  941. }
  942. }
  943. }
  944. // UnitTypes returns a middleware to set unit types to context variables.
  945. func UnitTypes() func(ctx *Context) {
  946. return func(ctx *Context) {
  947. ctx.Data["UnitTypeCode"] = unit_model.TypeCode
  948. ctx.Data["UnitTypeIssues"] = unit_model.TypeIssues
  949. ctx.Data["UnitTypePullRequests"] = unit_model.TypePullRequests
  950. ctx.Data["UnitTypeReleases"] = unit_model.TypeReleases
  951. ctx.Data["UnitTypeWiki"] = unit_model.TypeWiki
  952. ctx.Data["UnitTypeExternalWiki"] = unit_model.TypeExternalWiki
  953. ctx.Data["UnitTypeExternalTracker"] = unit_model.TypeExternalTracker
  954. ctx.Data["UnitTypeProjects"] = unit_model.TypeProjects
  955. ctx.Data["UnitTypePackages"] = unit_model.TypePackages
  956. ctx.Data["UnitTypeActions"] = unit_model.TypeActions
  957. }
  958. }