code.gitea.io/gitea@v1.22.3/routers/web/repo/repo.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2020 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package repo 6 7 import ( 8 "errors" 9 "fmt" 10 "net/http" 11 "slices" 12 "strings" 13 14 "code.gitea.io/gitea/models" 15 "code.gitea.io/gitea/models/db" 16 git_model "code.gitea.io/gitea/models/git" 17 "code.gitea.io/gitea/models/organization" 18 access_model "code.gitea.io/gitea/models/perm/access" 19 repo_model "code.gitea.io/gitea/models/repo" 20 "code.gitea.io/gitea/models/unit" 21 user_model "code.gitea.io/gitea/models/user" 22 "code.gitea.io/gitea/modules/base" 23 "code.gitea.io/gitea/modules/cache" 24 "code.gitea.io/gitea/modules/git" 25 "code.gitea.io/gitea/modules/log" 26 "code.gitea.io/gitea/modules/optional" 27 repo_module "code.gitea.io/gitea/modules/repository" 28 "code.gitea.io/gitea/modules/setting" 29 "code.gitea.io/gitea/modules/storage" 30 api "code.gitea.io/gitea/modules/structs" 31 "code.gitea.io/gitea/modules/util" 32 "code.gitea.io/gitea/modules/web" 33 "code.gitea.io/gitea/services/context" 34 "code.gitea.io/gitea/services/convert" 35 "code.gitea.io/gitea/services/forms" 36 repo_service "code.gitea.io/gitea/services/repository" 37 archiver_service "code.gitea.io/gitea/services/repository/archiver" 38 commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" 39 ) 40 41 const ( 42 tplCreate base.TplName = "repo/create" 43 tplAlertDetails base.TplName = "base/alert_details" 44 ) 45 46 // MustBeNotEmpty render when a repo is a empty git dir 47 func MustBeNotEmpty(ctx *context.Context) { 48 if ctx.Repo.Repository.IsEmpty { 49 ctx.NotFound("MustBeNotEmpty", nil) 50 } 51 } 52 53 // MustBeEditable check that repo can be edited 54 func MustBeEditable(ctx *context.Context) { 55 if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit { 56 ctx.NotFound("", nil) 57 return 58 } 59 } 60 61 // MustBeAbleToUpload check that repo can be uploaded to 62 func MustBeAbleToUpload(ctx *context.Context) { 63 if !setting.Repository.Upload.Enabled { 64 ctx.NotFound("", nil) 65 } 66 } 67 68 func CommitInfoCache(ctx *context.Context) { 69 var err error 70 ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) 71 if err != nil { 72 ctx.ServerError("GetBranchCommit", err) 73 return 74 } 75 ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() 76 if err != nil { 77 ctx.ServerError("GetCommitsCount", err) 78 return 79 } 80 ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount 81 ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) 82 } 83 84 func checkContextUser(ctx *context.Context, uid int64) *user_model.User { 85 orgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID) 86 if err != nil { 87 ctx.ServerError("GetOrgsCanCreateRepoByUserID", err) 88 return nil 89 } 90 91 if !ctx.Doer.IsAdmin { 92 orgsAvailable := []*organization.Organization{} 93 for i := 0; i < len(orgs); i++ { 94 if orgs[i].CanCreateRepo() { 95 orgsAvailable = append(orgsAvailable, orgs[i]) 96 } 97 } 98 ctx.Data["Orgs"] = orgsAvailable 99 } else { 100 ctx.Data["Orgs"] = orgs 101 } 102 103 // Not equal means current user is an organization. 104 if uid == ctx.Doer.ID || uid == 0 { 105 return ctx.Doer 106 } 107 108 org, err := user_model.GetUserByID(ctx, uid) 109 if user_model.IsErrUserNotExist(err) { 110 return ctx.Doer 111 } 112 113 if err != nil { 114 ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %w", uid, err)) 115 return nil 116 } 117 118 // Check ownership of organization. 119 if !org.IsOrganization() { 120 ctx.Error(http.StatusForbidden) 121 return nil 122 } 123 if !ctx.Doer.IsAdmin { 124 canCreate, err := organization.OrgFromUser(org).CanCreateOrgRepo(ctx, ctx.Doer.ID) 125 if err != nil { 126 ctx.ServerError("CanCreateOrgRepo", err) 127 return nil 128 } else if !canCreate { 129 ctx.Error(http.StatusForbidden) 130 return nil 131 } 132 } else { 133 ctx.Data["Orgs"] = orgs 134 } 135 return org 136 } 137 138 func getRepoPrivate(ctx *context.Context) bool { 139 switch strings.ToLower(setting.Repository.DefaultPrivate) { 140 case setting.RepoCreatingLastUserVisibility: 141 return ctx.Doer.LastRepoVisibility 142 case setting.RepoCreatingPrivate: 143 return true 144 case setting.RepoCreatingPublic: 145 return false 146 default: 147 return ctx.Doer.LastRepoVisibility 148 } 149 } 150 151 // Create render creating repository page 152 func Create(ctx *context.Context) { 153 ctx.Data["Title"] = ctx.Tr("new_repo") 154 155 // Give default value for template to render. 156 ctx.Data["Gitignores"] = repo_module.Gitignores 157 ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles 158 ctx.Data["Licenses"] = repo_module.Licenses 159 ctx.Data["Readmes"] = repo_module.Readmes 160 ctx.Data["readme"] = "Default" 161 ctx.Data["private"] = getRepoPrivate(ctx) 162 ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate 163 ctx.Data["default_branch"] = setting.Repository.DefaultBranch 164 165 ctxUser := checkContextUser(ctx, ctx.FormInt64("org")) 166 if ctx.Written() { 167 return 168 } 169 ctx.Data["ContextUser"] = ctxUser 170 171 ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select") 172 templateID := ctx.FormInt64("template_id") 173 if templateID > 0 { 174 templateRepo, err := repo_model.GetRepositoryByID(ctx, templateID) 175 if err == nil && access_model.CheckRepoUnitUser(ctx, templateRepo, ctxUser, unit.TypeCode) { 176 ctx.Data["repo_template"] = templateID 177 ctx.Data["repo_template_name"] = templateRepo.Name 178 } 179 } 180 181 ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() 182 ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() 183 ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats 184 ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat 185 186 ctx.HTML(http.StatusOK, tplCreate) 187 } 188 189 func handleCreateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl base.TplName, form any) { 190 switch { 191 case repo_model.IsErrReachLimitOfRepo(err): 192 maxCreationLimit := owner.MaxCreationLimit() 193 msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) 194 ctx.RenderWithErr(msg, tpl, form) 195 case repo_model.IsErrRepoAlreadyExist(err): 196 ctx.Data["Err_RepoName"] = true 197 ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) 198 case repo_model.IsErrRepoFilesAlreadyExist(err): 199 ctx.Data["Err_RepoName"] = true 200 switch { 201 case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): 202 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) 203 case setting.Repository.AllowAdoptionOfUnadoptedRepositories: 204 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) 205 case setting.Repository.AllowDeleteOfUnadoptedRepositories: 206 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) 207 default: 208 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) 209 } 210 case db.IsErrNameReserved(err): 211 ctx.Data["Err_RepoName"] = true 212 ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) 213 case db.IsErrNamePatternNotAllowed(err): 214 ctx.Data["Err_RepoName"] = true 215 ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) 216 default: 217 ctx.ServerError(name, err) 218 } 219 } 220 221 // CreatePost response for creating repository 222 func CreatePost(ctx *context.Context) { 223 form := web.GetForm(ctx).(*forms.CreateRepoForm) 224 ctx.Data["Title"] = ctx.Tr("new_repo") 225 226 ctx.Data["Gitignores"] = repo_module.Gitignores 227 ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles 228 ctx.Data["Licenses"] = repo_module.Licenses 229 ctx.Data["Readmes"] = repo_module.Readmes 230 231 ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() 232 ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() 233 234 ctxUser := checkContextUser(ctx, form.UID) 235 if ctx.Written() { 236 return 237 } 238 ctx.Data["ContextUser"] = ctxUser 239 240 if ctx.HasError() { 241 ctx.HTML(http.StatusOK, tplCreate) 242 return 243 } 244 245 var repo *repo_model.Repository 246 var err error 247 if form.RepoTemplate > 0 { 248 opts := repo_service.GenerateRepoOptions{ 249 Name: form.RepoName, 250 Description: form.Description, 251 Private: form.Private || setting.Repository.ForcePrivate, 252 GitContent: form.GitContent, 253 Topics: form.Topics, 254 GitHooks: form.GitHooks, 255 Webhooks: form.Webhooks, 256 Avatar: form.Avatar, 257 IssueLabels: form.Labels, 258 ProtectedBranch: form.ProtectedBranch, 259 } 260 261 if !opts.IsValid() { 262 ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form) 263 return 264 } 265 266 templateRepo := getRepository(ctx, form.RepoTemplate) 267 if ctx.Written() { 268 return 269 } 270 271 if !templateRepo.IsTemplate { 272 ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form) 273 return 274 } 275 276 repo, err = repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, templateRepo, opts) 277 if err == nil { 278 log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) 279 ctx.Redirect(repo.Link()) 280 return 281 } 282 } else { 283 repo, err = repo_service.CreateRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ 284 Name: form.RepoName, 285 Description: form.Description, 286 Gitignores: form.Gitignores, 287 IssueLabels: form.IssueLabels, 288 License: form.License, 289 Readme: form.Readme, 290 IsPrivate: form.Private || setting.Repository.ForcePrivate, 291 DefaultBranch: form.DefaultBranch, 292 AutoInit: form.AutoInit, 293 IsTemplate: form.Template, 294 TrustModel: repo_model.DefaultTrustModel, 295 ObjectFormatName: form.ObjectFormatName, 296 }) 297 if err == nil { 298 log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) 299 ctx.Redirect(repo.Link()) 300 return 301 } 302 } 303 304 handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form) 305 } 306 307 const ( 308 tplWatchUnwatch base.TplName = "repo/watch_unwatch" 309 tplStarUnstar base.TplName = "repo/star_unstar" 310 ) 311 312 // Action response for actions to a repository 313 func Action(ctx *context.Context) { 314 var err error 315 switch ctx.Params(":action") { 316 case "watch": 317 err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) 318 case "unwatch": 319 err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) 320 case "star": 321 err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) 322 case "unstar": 323 err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) 324 case "accept_transfer": 325 err = acceptOrRejectRepoTransfer(ctx, true) 326 case "reject_transfer": 327 err = acceptOrRejectRepoTransfer(ctx, false) 328 case "desc": // FIXME: this is not used 329 if !ctx.Repo.IsOwner() { 330 ctx.Error(http.StatusNotFound) 331 return 332 } 333 334 ctx.Repo.Repository.Description = ctx.FormString("desc") 335 ctx.Repo.Repository.Website = ctx.FormString("site") 336 err = repo_service.UpdateRepository(ctx, ctx.Repo.Repository, false) 337 } 338 339 if err != nil { 340 if errors.Is(err, user_model.ErrBlockedUser) { 341 ctx.Flash.Error(ctx.Tr("repo.action.blocked_user")) 342 } else { 343 ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err) 344 return 345 } 346 } 347 348 switch ctx.Params(":action") { 349 case "watch", "unwatch": 350 ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) 351 case "star", "unstar": 352 ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) 353 } 354 355 switch ctx.Params(":action") { 356 case "watch", "unwatch", "star", "unstar": 357 // we have to reload the repository because NumStars or NumWatching (used in the templates) has just changed 358 ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name) 359 if err != nil { 360 ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err) 361 return 362 } 363 } 364 365 switch ctx.Params(":action") { 366 case "watch", "unwatch": 367 ctx.HTML(http.StatusOK, tplWatchUnwatch) 368 return 369 case "star", "unstar": 370 ctx.HTML(http.StatusOK, tplStarUnstar) 371 return 372 } 373 374 ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink) 375 } 376 377 func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error { 378 repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) 379 if err != nil { 380 return err 381 } 382 383 if err := repoTransfer.LoadAttributes(ctx); err != nil { 384 return err 385 } 386 387 if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) { 388 return errors.New("user does not have enough permissions") 389 } 390 391 if accept { 392 if ctx.Repo.GitRepo != nil { 393 ctx.Repo.GitRepo.Close() 394 ctx.Repo.GitRepo = nil 395 } 396 397 if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil { 398 return err 399 } 400 ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success")) 401 } else { 402 if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil { 403 return err 404 } 405 ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected")) 406 } 407 408 ctx.Redirect(ctx.Repo.Repository.Link()) 409 return nil 410 } 411 412 // RedirectDownload return a file based on the following infos: 413 func RedirectDownload(ctx *context.Context) { 414 var ( 415 vTag = ctx.Params("vTag") 416 fileName = ctx.Params("fileName") 417 ) 418 tagNames := []string{vTag} 419 curRepo := ctx.Repo.Repository 420 releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ 421 IncludeDrafts: ctx.Repo.CanWrite(unit.TypeReleases), 422 RepoID: curRepo.ID, 423 TagNames: tagNames, 424 }) 425 if err != nil { 426 ctx.ServerError("RedirectDownload", err) 427 return 428 } 429 if len(releases) == 1 { 430 release := releases[0] 431 att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName) 432 if err != nil { 433 ctx.Error(http.StatusNotFound) 434 return 435 } 436 if att != nil { 437 ServeAttachment(ctx, att.UUID) 438 return 439 } 440 } else if len(releases) == 0 && vTag == "latest" { 441 // GitHub supports the alias "latest" for the latest release 442 // We only fetch the latest release if the tag is "latest" and no release with the tag "latest" exists 443 release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID) 444 if err != nil { 445 ctx.Error(http.StatusNotFound) 446 return 447 } 448 att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName) 449 if err != nil { 450 ctx.Error(http.StatusNotFound) 451 return 452 } 453 if att != nil { 454 ServeAttachment(ctx, att.UUID) 455 return 456 } 457 } 458 ctx.Error(http.StatusNotFound) 459 } 460 461 // Download an archive of a repository 462 func Download(ctx *context.Context) { 463 uri := ctx.Params("*") 464 aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) 465 if err != nil { 466 if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { 467 ctx.Error(http.StatusBadRequest, err.Error()) 468 } else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) { 469 ctx.Error(http.StatusNotFound, err.Error()) 470 } else { 471 ctx.ServerError("archiver_service.NewRequest", err) 472 } 473 return 474 } 475 476 archiver, err := aReq.Await(ctx) 477 if err != nil { 478 ctx.ServerError("archiver.Await", err) 479 return 480 } 481 482 download(ctx, aReq.GetArchiveName(), archiver) 483 } 484 485 func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) { 486 downloadName := ctx.Repo.Repository.Name + "-" + archiveName 487 488 // Add nix format link header so tarballs lock correctly: 489 // https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md 490 ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.tar.gz?rev=%s>; rel="immutable"`, 491 ctx.Repo.Repository.APIURL(), 492 archiver.CommitID, archiver.CommitID)) 493 494 rPath := archiver.RelativePath() 495 if setting.RepoArchive.Storage.MinioConfig.ServeDirect { 496 // If we have a signed url (S3, object storage), redirect to this directly. 497 u, err := storage.RepoArchives.URL(rPath, downloadName) 498 if u != nil && err == nil { 499 ctx.Redirect(u.String()) 500 return 501 } 502 } 503 504 // If we have matched and access to release or issue 505 fr, err := storage.RepoArchives.Open(rPath) 506 if err != nil { 507 ctx.ServerError("Open", err) 508 return 509 } 510 defer fr.Close() 511 512 ctx.ServeContent(fr, &context.ServeHeaderOptions{ 513 Filename: downloadName, 514 LastModified: archiver.CreatedUnix.AsLocalTime(), 515 }) 516 } 517 518 // InitiateDownload will enqueue an archival request, as needed. It may submit 519 // a request that's already in-progress, but the archiver service will just 520 // kind of drop it on the floor if this is the case. 521 func InitiateDownload(ctx *context.Context) { 522 uri := ctx.Params("*") 523 aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) 524 if err != nil { 525 ctx.ServerError("archiver_service.NewRequest", err) 526 return 527 } 528 if aReq == nil { 529 ctx.Error(http.StatusNotFound) 530 return 531 } 532 533 archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID) 534 if err != nil { 535 ctx.ServerError("archiver_service.StartArchive", err) 536 return 537 } 538 if archiver == nil || archiver.Status != repo_model.ArchiverReady { 539 if err := archiver_service.StartArchive(aReq); err != nil { 540 ctx.ServerError("archiver_service.StartArchive", err) 541 return 542 } 543 } 544 545 var completed bool 546 if archiver != nil && archiver.Status == repo_model.ArchiverReady { 547 completed = true 548 } 549 550 ctx.JSON(http.StatusOK, map[string]any{ 551 "complete": completed, 552 }) 553 } 554 555 // SearchRepo repositories via options 556 func SearchRepo(ctx *context.Context) { 557 page := ctx.FormInt("page") 558 if page <= 0 { 559 page = 1 560 } 561 opts := &repo_model.SearchRepoOptions{ 562 ListOptions: db.ListOptions{ 563 Page: page, 564 PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), 565 }, 566 Actor: ctx.Doer, 567 Keyword: ctx.FormTrim("q"), 568 OwnerID: ctx.FormInt64("uid"), 569 PriorityOwnerID: ctx.FormInt64("priority_owner_id"), 570 TeamID: ctx.FormInt64("team_id"), 571 TopicOnly: ctx.FormBool("topic"), 572 Collaborate: optional.None[bool](), 573 Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")), 574 Template: optional.None[bool](), 575 StarredByID: ctx.FormInt64("starredBy"), 576 IncludeDescription: ctx.FormBool("includeDesc"), 577 } 578 579 if ctx.FormString("template") != "" { 580 opts.Template = optional.Some(ctx.FormBool("template")) 581 } 582 583 if ctx.FormBool("exclusive") { 584 opts.Collaborate = optional.Some(false) 585 } 586 587 mode := ctx.FormString("mode") 588 switch mode { 589 case "source": 590 opts.Fork = optional.Some(false) 591 opts.Mirror = optional.Some(false) 592 case "fork": 593 opts.Fork = optional.Some(true) 594 case "mirror": 595 opts.Mirror = optional.Some(true) 596 case "collaborative": 597 opts.Mirror = optional.Some(false) 598 opts.Collaborate = optional.Some(true) 599 case "": 600 default: 601 ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode)) 602 return 603 } 604 605 if ctx.FormString("archived") != "" { 606 opts.Archived = optional.Some(ctx.FormBool("archived")) 607 } 608 609 if ctx.FormString("is_private") != "" { 610 opts.IsPrivate = optional.Some(ctx.FormBool("is_private")) 611 } 612 613 sortMode := ctx.FormString("sort") 614 if len(sortMode) > 0 { 615 sortOrder := ctx.FormString("order") 616 if len(sortOrder) == 0 { 617 sortOrder = "asc" 618 } 619 if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok { 620 if orderBy, ok := searchModeMap[sortMode]; ok { 621 opts.OrderBy = orderBy 622 } else { 623 ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: \"%s\"", sortMode)) 624 return 625 } 626 } else { 627 ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: \"%s\"", sortOrder)) 628 return 629 } 630 } 631 632 // To improve performance when only the count is requested 633 if ctx.FormBool("count_only") { 634 if count, err := repo_model.CountRepository(ctx, opts); err != nil { 635 log.Error("CountRepository: %v", err) 636 ctx.JSON(http.StatusInternalServerError, nil) // frontend JS doesn't handle error response (same as below) 637 } else { 638 ctx.SetTotalCountHeader(count) 639 ctx.JSONOK() 640 } 641 return 642 } 643 644 repos, count, err := repo_model.SearchRepository(ctx, opts) 645 if err != nil { 646 log.Error("SearchRepository: %v", err) 647 ctx.JSON(http.StatusInternalServerError, nil) 648 return 649 } 650 651 ctx.SetTotalCountHeader(count) 652 653 latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(ctx, repos) 654 if err != nil { 655 log.Error("FindReposLastestCommitStatuses: %v", err) 656 ctx.JSON(http.StatusInternalServerError, nil) 657 return 658 } 659 660 results := make([]*repo_service.WebSearchRepository, len(repos)) 661 for i, repo := range repos { 662 results[i] = &repo_service.WebSearchRepository{ 663 Repository: &api.Repository{ 664 ID: repo.ID, 665 FullName: repo.FullName(), 666 Fork: repo.IsFork, 667 Private: repo.IsPrivate, 668 Template: repo.IsTemplate, 669 Mirror: repo.IsMirror, 670 Stars: repo.NumStars, 671 HTMLURL: repo.HTMLURL(), 672 Link: repo.Link(), 673 Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate, 674 }, 675 } 676 677 if latestCommitStatuses[i] != nil { 678 results[i].LatestCommitStatus = latestCommitStatuses[i] 679 results[i].LocaleLatestCommitStatus = latestCommitStatuses[i].LocaleString(ctx.Locale) 680 } 681 } 682 683 ctx.JSON(http.StatusOK, repo_service.WebSearchResults{ 684 OK: true, 685 Data: results, 686 }) 687 } 688 689 type branchTagSearchResponse struct { 690 Results []string `json:"results"` 691 } 692 693 // GetBranchesList get branches for current repo' 694 func GetBranchesList(ctx *context.Context) { 695 branchOpts := git_model.FindBranchOptions{ 696 RepoID: ctx.Repo.Repository.ID, 697 IsDeletedBranch: optional.Some(false), 698 ListOptions: db.ListOptionsAll, 699 } 700 branches, err := git_model.FindBranchNames(ctx, branchOpts) 701 if err != nil { 702 ctx.JSON(http.StatusInternalServerError, err) 703 return 704 } 705 resp := &branchTagSearchResponse{} 706 // always put default branch on the top if it exists 707 if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) { 708 branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch) 709 branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...) 710 } 711 resp.Results = branches 712 ctx.JSON(http.StatusOK, resp) 713 } 714 715 // GetTagList get tag list for current repo 716 func GetTagList(ctx *context.Context) { 717 tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) 718 if err != nil { 719 ctx.JSON(http.StatusInternalServerError, err) 720 return 721 } 722 resp := &branchTagSearchResponse{} 723 resp.Results = tags 724 ctx.JSON(http.StatusOK, resp) 725 } 726 727 func PrepareBranchList(ctx *context.Context) { 728 branchOpts := git_model.FindBranchOptions{ 729 RepoID: ctx.Repo.Repository.ID, 730 IsDeletedBranch: optional.Some(false), 731 ListOptions: db.ListOptionsAll, 732 } 733 brs, err := git_model.FindBranchNames(ctx, branchOpts) 734 if err != nil { 735 ctx.ServerError("GetBranches", err) 736 return 737 } 738 // always put default branch on the top if it exists 739 if slices.Contains(brs, ctx.Repo.Repository.DefaultBranch) { 740 brs = util.SliceRemoveAll(brs, ctx.Repo.Repository.DefaultBranch) 741 brs = append([]string{ctx.Repo.Repository.DefaultBranch}, brs...) 742 } 743 ctx.Data["Branches"] = brs 744 }