gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package repo
  5. import (
  6. "bytes"
  7. "compress/gzip"
  8. gocontext "context"
  9. "fmt"
  10. "net/http"
  11. "os"
  12. "path/filepath"
  13. "regexp"
  14. "slices"
  15. "strconv"
  16. "strings"
  17. "sync"
  18. "time"
  19. actions_model "code.gitea.io/gitea/models/actions"
  20. auth_model "code.gitea.io/gitea/models/auth"
  21. "code.gitea.io/gitea/models/perm"
  22. access_model "code.gitea.io/gitea/models/perm/access"
  23. repo_model "code.gitea.io/gitea/models/repo"
  24. "code.gitea.io/gitea/models/unit"
  25. "code.gitea.io/gitea/modules/git"
  26. "code.gitea.io/gitea/modules/git/gitcmd"
  27. "code.gitea.io/gitea/modules/log"
  28. repo_module "code.gitea.io/gitea/modules/repository"
  29. "code.gitea.io/gitea/modules/setting"
  30. "code.gitea.io/gitea/modules/structs"
  31. "code.gitea.io/gitea/services/context"
  32. repo_service "code.gitea.io/gitea/services/repository"
  33. "github.com/go-chi/cors"
  34. )
  35. func HTTPGitEnabledHandler(ctx *context.Context) {
  36. if setting.Repository.DisableHTTPGit {
  37. ctx.Resp.WriteHeader(http.StatusForbidden)
  38. _, _ = ctx.Resp.Write([]byte("Interacting with repositories by HTTP protocol is not allowed"))
  39. }
  40. }
  41. func CorsHandler() func(next http.Handler) http.Handler {
  42. if setting.Repository.AccessControlAllowOrigin != "" {
  43. return cors.Handler(cors.Options{
  44. AllowedOrigins: []string{setting.Repository.AccessControlAllowOrigin},
  45. AllowedHeaders: []string{"Content-Type", "Authorization", "User-Agent"},
  46. })
  47. }
  48. return func(next http.Handler) http.Handler {
  49. return next
  50. }
  51. }
  52. // httpBase implementation git smart HTTP protocol
  53. func httpBase(ctx *context.Context) *serviceHandler {
  54. username := ctx.PathParam("username")
  55. reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
  56. if ctx.FormString("go-get") == "1" {
  57. context.EarlyResponseForGoGetMeta(ctx)
  58. return nil
  59. }
  60. var isPull, receivePack bool
  61. service := ctx.FormString("service")
  62. if service == "git-receive-pack" ||
  63. strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
  64. isPull = false
  65. receivePack = true
  66. } else if service == "git-upload-pack" ||
  67. strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
  68. isPull = true
  69. } else if service == "git-upload-archive" ||
  70. strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
  71. isPull = true
  72. } else {
  73. isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet
  74. }
  75. var accessMode perm.AccessMode
  76. if isPull {
  77. accessMode = perm.AccessModeRead
  78. } else {
  79. accessMode = perm.AccessModeWrite
  80. }
  81. isWiki := false
  82. unitType := unit.TypeCode
  83. if strings.HasSuffix(reponame, ".wiki") {
  84. isWiki = true
  85. unitType = unit.TypeWiki
  86. reponame = reponame[:len(reponame)-5]
  87. }
  88. owner := ctx.ContextUser
  89. if !owner.IsOrganization() && !owner.IsActive {
  90. ctx.PlainText(http.StatusForbidden, "Repository cannot be accessed. You cannot push or open issues/pull-requests.")
  91. return nil
  92. }
  93. repoExist := true
  94. repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, reponame)
  95. if err != nil {
  96. if !repo_model.IsErrRepoNotExist(err) {
  97. ctx.ServerError("GetRepositoryByName", err)
  98. return nil
  99. }
  100. if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil {
  101. context.RedirectToRepo(ctx.Base, redirectRepoID)
  102. return nil
  103. }
  104. repoExist = false
  105. }
  106. // Don't allow pushing if the repo is archived
  107. if repoExist && repo.IsArchived && !isPull {
  108. ctx.PlainText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.")
  109. return nil
  110. }
  111. // Only public pull don't need auth.
  112. isPublicPull := repoExist && !repo.IsPrivate && isPull
  113. var (
  114. askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
  115. environ []string
  116. )
  117. // don't allow anonymous pulls if organization is not public
  118. if isPublicPull {
  119. if err := repo.LoadOwner(ctx); err != nil {
  120. ctx.ServerError("LoadOwner", err)
  121. return nil
  122. }
  123. askAuth = askAuth || (repo.Owner.Visibility != structs.VisibleTypePublic)
  124. }
  125. // check access
  126. if askAuth {
  127. // rely on the results of Contexter
  128. if !ctx.IsSigned {
  129. // TODO: support digit auth - which would be Authorization header with digit
  130. ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
  131. ctx.HTTPError(http.StatusUnauthorized)
  132. return nil
  133. }
  134. context.CheckRepoScopedToken(ctx, repo, auth_model.GetScopeLevelFromAccessMode(accessMode))
  135. if ctx.Written() {
  136. return nil
  137. }
  138. if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true {
  139. _, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
  140. if err == nil {
  141. // TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
  142. ctx.PlainText(http.StatusUnauthorized, "Users with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password. Please create and use a personal access token on the user settings page")
  143. return nil
  144. } else if !auth_model.IsErrTwoFactorNotEnrolled(err) {
  145. ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
  146. return nil
  147. }
  148. }
  149. if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
  150. ctx.PlainText(http.StatusForbidden, "Your account is disabled.")
  151. return nil
  152. }
  153. environ = []string{
  154. repo_module.EnvRepoUsername + "=" + username,
  155. repo_module.EnvRepoName + "=" + reponame,
  156. repo_module.EnvPusherName + "=" + ctx.Doer.Name,
  157. repo_module.EnvPusherID + fmt.Sprintf("=%d", ctx.Doer.ID),
  158. repo_module.EnvAppURL + "=" + setting.AppURL,
  159. }
  160. if repoExist {
  161. // Because of special ref "refs/for" .. , need delay write permission check
  162. if git.DefaultFeatures().SupportProcReceive {
  163. accessMode = perm.AccessModeRead
  164. }
  165. if ctx.Data["IsActionsToken"] == true {
  166. taskID := ctx.Data["ActionsTaskID"].(int64)
  167. task, err := actions_model.GetTaskByID(ctx, taskID)
  168. if err != nil {
  169. ctx.ServerError("GetTaskByID", err)
  170. return nil
  171. }
  172. if task.RepoID != repo.ID {
  173. ctx.PlainText(http.StatusForbidden, "User permission denied")
  174. return nil
  175. }
  176. if task.IsForkPullRequest {
  177. if accessMode > perm.AccessModeRead {
  178. ctx.PlainText(http.StatusForbidden, "User permission denied")
  179. return nil
  180. }
  181. environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeRead))
  182. } else {
  183. if accessMode > perm.AccessModeWrite {
  184. ctx.PlainText(http.StatusForbidden, "User permission denied")
  185. return nil
  186. }
  187. environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite))
  188. }
  189. } else {
  190. p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
  191. if err != nil {
  192. ctx.ServerError("GetUserRepoPermission", err)
  193. return nil
  194. }
  195. if !p.CanAccess(accessMode, unitType) {
  196. ctx.PlainText(http.StatusNotFound, "Repository not found")
  197. return nil
  198. }
  199. }
  200. if !isPull && repo.IsMirror {
  201. ctx.PlainText(http.StatusForbidden, "mirror repository is read-only")
  202. return nil
  203. }
  204. }
  205. if !ctx.Doer.KeepEmailPrivate {
  206. environ = append(environ, repo_module.EnvPusherEmail+"="+ctx.Doer.Email)
  207. }
  208. if isWiki {
  209. environ = append(environ, repo_module.EnvRepoIsWiki+"=true")
  210. } else {
  211. environ = append(environ, repo_module.EnvRepoIsWiki+"=false")
  212. }
  213. }
  214. if !repoExist {
  215. if !receivePack {
  216. ctx.PlainText(http.StatusNotFound, "Repository not found")
  217. return nil
  218. }
  219. if isWiki { // you cannot send wiki operation before create the repository
  220. ctx.PlainText(http.StatusNotFound, "Repository not found")
  221. return nil
  222. }
  223. if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
  224. ctx.PlainText(http.StatusForbidden, "Push to create is not enabled for organizations.")
  225. return nil
  226. }
  227. if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
  228. ctx.PlainText(http.StatusForbidden, "Push to create is not enabled for users.")
  229. return nil
  230. }
  231. // Return dummy payload if GET receive-pack
  232. if ctx.Req.Method == http.MethodGet {
  233. dummyInfoRefs(ctx)
  234. return nil
  235. }
  236. repo, err = repo_service.PushCreateRepo(ctx, ctx.Doer, owner, reponame)
  237. if err != nil {
  238. log.Error("pushCreateRepo: %v", err)
  239. ctx.Status(http.StatusNotFound)
  240. return nil
  241. }
  242. }
  243. if isWiki {
  244. // Ensure the wiki is enabled before we allow access to it
  245. if _, err := repo.GetUnit(ctx, unit.TypeWiki); err != nil {
  246. if repo_model.IsErrUnitTypeNotExist(err) {
  247. ctx.PlainText(http.StatusForbidden, "repository wiki is disabled")
  248. return nil
  249. }
  250. log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
  251. ctx.ServerError("GetUnit(UnitTypeWiki) for "+repo.FullName(), err)
  252. return nil
  253. }
  254. }
  255. environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
  256. ctx.Req.URL.Path = strings.ToLower(ctx.Req.URL.Path) // blue: In case some repo name has upper case name
  257. return &serviceHandler{repo, isWiki, environ}
  258. }
  259. var (
  260. infoRefsCache []byte
  261. infoRefsOnce sync.Once
  262. )
  263. func dummyInfoRefs(ctx *context.Context) {
  264. infoRefsOnce.Do(func() {
  265. tmpDir, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-info-refs-cache")
  266. if err != nil {
  267. log.Error("Failed to create temp dir for git-receive-pack cache: %v", err)
  268. return
  269. }
  270. defer cleanup()
  271. if err := git.InitRepository(ctx, tmpDir, true, git.Sha1ObjectFormat.Name()); err != nil {
  272. log.Error("Failed to init bare repo for git-receive-pack cache: %v", err)
  273. return
  274. }
  275. refs, _, err := gitcmd.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(ctx, &gitcmd.RunOpts{Dir: tmpDir})
  276. if err != nil {
  277. log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
  278. }
  279. log.Debug("populating infoRefsCache: \n%s", string(refs))
  280. infoRefsCache = refs
  281. })
  282. ctx.RespHeader().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
  283. ctx.RespHeader().Set("Pragma", "no-cache")
  284. ctx.RespHeader().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
  285. ctx.RespHeader().Set("Content-Type", "application/x-git-receive-pack-advertisement")
  286. _, _ = ctx.Write(packetWrite("# service=git-receive-pack\n"))
  287. _, _ = ctx.Write([]byte("0000"))
  288. _, _ = ctx.Write(infoRefsCache)
  289. }
  290. type serviceHandler struct {
  291. repo *repo_model.Repository
  292. isWiki bool
  293. environ []string
  294. }
  295. func (h *serviceHandler) getRepoDir() string {
  296. if h.isWiki {
  297. return h.repo.WikiPath()
  298. }
  299. return h.repo.RepoPath()
  300. }
  301. func setHeaderNoCache(ctx *context.Context) {
  302. ctx.Resp.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
  303. ctx.Resp.Header().Set("Pragma", "no-cache")
  304. ctx.Resp.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
  305. }
  306. func setHeaderCacheForever(ctx *context.Context) {
  307. now := time.Now().Unix()
  308. expires := now + 31536000
  309. ctx.Resp.Header().Set("Date", strconv.FormatInt(now, 10))
  310. ctx.Resp.Header().Set("Expires", strconv.FormatInt(expires, 10))
  311. ctx.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
  312. }
  313. func containsParentDirectorySeparator(v string) bool {
  314. if !strings.Contains(v, "..") {
  315. return false
  316. }
  317. return slices.Contains(strings.FieldsFunc(v, isSlashRune), "..")
  318. }
  319. func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
  320. func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string) {
  321. if containsParentDirectorySeparator(file) {
  322. log.Error("request file path contains invalid path: %v", file)
  323. ctx.Resp.WriteHeader(http.StatusBadRequest)
  324. return
  325. }
  326. reqFile := filepath.Join(h.getRepoDir(), file)
  327. fi, err := os.Stat(reqFile)
  328. if os.IsNotExist(err) {
  329. ctx.Resp.WriteHeader(http.StatusNotFound)
  330. return
  331. }
  332. ctx.Resp.Header().Set("Content-Type", contentType)
  333. ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10))
  334. // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
  335. ctx.Resp.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat))
  336. http.ServeFile(ctx.Resp, ctx.Req, reqFile)
  337. }
  338. // one or more key=value pairs separated by colons
  339. var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
  340. func prepareGitCmdWithAllowedService(service string) (*gitcmd.Command, error) {
  341. if service == "receive-pack" {
  342. return gitcmd.NewCommand("receive-pack"), nil
  343. }
  344. if service == "upload-pack" {
  345. return gitcmd.NewCommand("upload-pack"), nil
  346. }
  347. return nil, fmt.Errorf("service %q is not allowed", service)
  348. }
  349. func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
  350. defer func() {
  351. if err := ctx.Req.Body.Close(); err != nil {
  352. log.Error("serviceRPC: Close: %v", err)
  353. }
  354. }()
  355. expectedContentType := fmt.Sprintf("application/x-git-%s-request", service)
  356. if ctx.Req.Header.Get("Content-Type") != expectedContentType {
  357. log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
  358. ctx.Resp.WriteHeader(http.StatusUnauthorized)
  359. return
  360. }
  361. cmd, err := prepareGitCmdWithAllowedService(service)
  362. if err != nil {
  363. log.Error("Failed to prepareGitCmdWithService: %v", err)
  364. ctx.Resp.WriteHeader(http.StatusUnauthorized)
  365. return
  366. }
  367. ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
  368. reqBody := ctx.Req.Body
  369. // Handle GZIP.
  370. if ctx.Req.Header.Get("Content-Encoding") == "gzip" {
  371. reqBody, err = gzip.NewReader(reqBody)
  372. if err != nil {
  373. log.Error("Fail to create gzip reader: %v", err)
  374. ctx.Resp.WriteHeader(http.StatusInternalServerError)
  375. return
  376. }
  377. }
  378. // set this for allow pre-receive and post-receive execute
  379. h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
  380. if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
  381. h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
  382. }
  383. var stderr bytes.Buffer
  384. cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.getRepoDir())
  385. if err := cmd.Run(ctx, &gitcmd.RunOpts{
  386. Dir: h.getRepoDir(),
  387. Env: append(os.Environ(), h.environ...),
  388. Stdout: ctx.Resp,
  389. Stdin: reqBody,
  390. Stderr: &stderr,
  391. UseContextTimeout: true,
  392. }); err != nil {
  393. if !git.IsErrCanceledOrKilled(err) {
  394. log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getRepoDir(), err, stderr.String())
  395. }
  396. return
  397. }
  398. }
  399. // ServiceUploadPack implements Git Smart HTTP protocol
  400. func ServiceUploadPack(ctx *context.Context) {
  401. h := httpBase(ctx)
  402. if h != nil {
  403. serviceRPC(ctx, h, "upload-pack")
  404. }
  405. }
  406. // ServiceReceivePack implements Git Smart HTTP protocol
  407. func ServiceReceivePack(ctx *context.Context) {
  408. h := httpBase(ctx)
  409. if h != nil {
  410. serviceRPC(ctx, h, "receive-pack")
  411. }
  412. }
  413. func getServiceType(ctx *context.Context) string {
  414. serviceType := ctx.Req.FormValue("service")
  415. if !strings.HasPrefix(serviceType, "git-") {
  416. return ""
  417. }
  418. return strings.TrimPrefix(serviceType, "git-")
  419. }
  420. func updateServerInfo(ctx gocontext.Context, dir string) []byte {
  421. out, _, err := gitcmd.NewCommand("update-server-info").RunStdBytes(ctx, &gitcmd.RunOpts{Dir: dir})
  422. if err != nil {
  423. log.Error(fmt.Sprintf("%v - %s", err, string(out)))
  424. }
  425. return out
  426. }
  427. func packetWrite(str string) []byte {
  428. s := strconv.FormatInt(int64(len(str)+4), 16)
  429. if len(s)%4 != 0 {
  430. s = strings.Repeat("0", 4-len(s)%4) + s
  431. }
  432. return []byte(s + str)
  433. }
  434. // GetInfoRefs implements Git dumb HTTP
  435. func GetInfoRefs(ctx *context.Context) {
  436. h := httpBase(ctx)
  437. if h == nil {
  438. return
  439. }
  440. setHeaderNoCache(ctx)
  441. service := getServiceType(ctx)
  442. cmd, err := prepareGitCmdWithAllowedService(service)
  443. if err == nil {
  444. if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
  445. h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
  446. }
  447. h.environ = append(os.Environ(), h.environ...)
  448. refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(ctx, &gitcmd.RunOpts{Env: h.environ, Dir: h.getRepoDir()})
  449. if err != nil {
  450. log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
  451. }
  452. ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
  453. ctx.Resp.WriteHeader(http.StatusOK)
  454. _, _ = ctx.Resp.Write(packetWrite("# service=git-" + service + "\n"))
  455. _, _ = ctx.Resp.Write([]byte("0000"))
  456. _, _ = ctx.Resp.Write(refs)
  457. } else {
  458. updateServerInfo(ctx, h.getRepoDir())
  459. h.sendFile(ctx, "text/plain; charset=utf-8", "info/refs")
  460. }
  461. }
  462. // GetTextFile implements Git dumb HTTP
  463. func GetTextFile(p string) func(*context.Context) {
  464. return func(ctx *context.Context) {
  465. h := httpBase(ctx)
  466. if h != nil {
  467. setHeaderNoCache(ctx)
  468. file := ctx.PathParam("file")
  469. if file != "" {
  470. h.sendFile(ctx, "text/plain", "objects/info/"+file)
  471. } else {
  472. h.sendFile(ctx, "text/plain", p)
  473. }
  474. }
  475. }
  476. }
  477. // GetInfoPacks implements Git dumb HTTP
  478. func GetInfoPacks(ctx *context.Context) {
  479. h := httpBase(ctx)
  480. if h != nil {
  481. setHeaderCacheForever(ctx)
  482. h.sendFile(ctx, "text/plain; charset=utf-8", "objects/info/packs")
  483. }
  484. }
  485. // GetLooseObject implements Git dumb HTTP
  486. func GetLooseObject(ctx *context.Context) {
  487. h := httpBase(ctx)
  488. if h != nil {
  489. setHeaderCacheForever(ctx)
  490. h.sendFile(ctx, "application/x-git-loose-object", fmt.Sprintf("objects/%s/%s",
  491. ctx.PathParam("head"), ctx.PathParam("hash")))
  492. }
  493. }
  494. // GetPackFile implements Git dumb HTTP
  495. func GetPackFile(ctx *context.Context) {
  496. h := httpBase(ctx)
  497. if h != nil {
  498. setHeaderCacheForever(ctx)
  499. h.sendFile(ctx, "application/x-git-packed-objects", "objects/pack/pack-"+ctx.PathParam("file")+".pack")
  500. }
  501. }
  502. // GetIdxFile implements Git dumb HTTP
  503. func GetIdxFile(ctx *context.Context) {
  504. h := httpBase(ctx)
  505. if h != nil {
  506. setHeaderCacheForever(ctx)
  507. h.sendFile(ctx, "application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.PathParam("file")+".idx")
  508. }
  509. }