code.gitea.io/gitea@v1.22.3/routers/api/v1/repo/repo.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2018 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package repo 6 7 import ( 8 "fmt" 9 "net/http" 10 "slices" 11 "strconv" 12 "strings" 13 "time" 14 15 actions_model "code.gitea.io/gitea/models/actions" 16 activities_model "code.gitea.io/gitea/models/activities" 17 "code.gitea.io/gitea/models/db" 18 "code.gitea.io/gitea/models/organization" 19 "code.gitea.io/gitea/models/perm" 20 access_model "code.gitea.io/gitea/models/perm/access" 21 repo_model "code.gitea.io/gitea/models/repo" 22 unit_model "code.gitea.io/gitea/models/unit" 23 user_model "code.gitea.io/gitea/models/user" 24 "code.gitea.io/gitea/modules/git" 25 "code.gitea.io/gitea/modules/gitrepo" 26 "code.gitea.io/gitea/modules/label" 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 api "code.gitea.io/gitea/modules/structs" 32 "code.gitea.io/gitea/modules/validation" 33 "code.gitea.io/gitea/modules/web" 34 "code.gitea.io/gitea/routers/api/v1/utils" 35 actions_service "code.gitea.io/gitea/services/actions" 36 "code.gitea.io/gitea/services/context" 37 "code.gitea.io/gitea/services/convert" 38 "code.gitea.io/gitea/services/issue" 39 repo_service "code.gitea.io/gitea/services/repository" 40 ) 41 42 // Search repositories via options 43 func Search(ctx *context.APIContext) { 44 // swagger:operation GET /repos/search repository repoSearch 45 // --- 46 // summary: Search for repositories 47 // produces: 48 // - application/json 49 // parameters: 50 // - name: q 51 // in: query 52 // description: keyword 53 // type: string 54 // - name: topic 55 // in: query 56 // description: Limit search to repositories with keyword as topic 57 // type: boolean 58 // - name: includeDesc 59 // in: query 60 // description: include search of keyword within repository description 61 // type: boolean 62 // - name: uid 63 // in: query 64 // description: search only for repos that the user with the given id owns or contributes to 65 // type: integer 66 // format: int64 67 // - name: priority_owner_id 68 // in: query 69 // description: repo owner to prioritize in the results 70 // type: integer 71 // format: int64 72 // - name: team_id 73 // in: query 74 // description: search only for repos that belong to the given team id 75 // type: integer 76 // format: int64 77 // - name: starredBy 78 // in: query 79 // description: search only for repos that the user with the given id has starred 80 // type: integer 81 // format: int64 82 // - name: private 83 // in: query 84 // description: include private repositories this user has access to (defaults to true) 85 // type: boolean 86 // - name: is_private 87 // in: query 88 // description: show only pubic, private or all repositories (defaults to all) 89 // type: boolean 90 // - name: template 91 // in: query 92 // description: include template repositories this user has access to (defaults to true) 93 // type: boolean 94 // - name: archived 95 // in: query 96 // description: show only archived, non-archived or all repositories (defaults to all) 97 // type: boolean 98 // - name: mode 99 // in: query 100 // description: type of repository to search for. Supported values are 101 // "fork", "source", "mirror" and "collaborative" 102 // type: string 103 // - name: exclusive 104 // in: query 105 // description: if `uid` is given, search only for repos that the user owns 106 // type: boolean 107 // - name: sort 108 // in: query 109 // description: sort repos by attribute. Supported values are 110 // "alpha", "created", "updated", "size", and "id". 111 // Default is "alpha" 112 // type: string 113 // - name: order 114 // in: query 115 // description: sort order, either "asc" (ascending) or "desc" (descending). 116 // Default is "asc", ignored if "sort" is not specified. 117 // type: string 118 // - name: page 119 // in: query 120 // description: page number of results to return (1-based) 121 // type: integer 122 // - name: limit 123 // in: query 124 // description: page size of results 125 // type: integer 126 // responses: 127 // "200": 128 // "$ref": "#/responses/SearchResults" 129 // "422": 130 // "$ref": "#/responses/validationError" 131 132 private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")) 133 if ctx.PublicOnly { 134 private = false 135 } 136 137 opts := &repo_model.SearchRepoOptions{ 138 ListOptions: utils.GetListOptions(ctx), 139 Actor: ctx.Doer, 140 Keyword: ctx.FormTrim("q"), 141 OwnerID: ctx.FormInt64("uid"), 142 PriorityOwnerID: ctx.FormInt64("priority_owner_id"), 143 TeamID: ctx.FormInt64("team_id"), 144 TopicOnly: ctx.FormBool("topic"), 145 Collaborate: optional.None[bool](), 146 Private: private, 147 Template: optional.None[bool](), 148 StarredByID: ctx.FormInt64("starredBy"), 149 IncludeDescription: ctx.FormBool("includeDesc"), 150 } 151 152 if ctx.FormString("template") != "" { 153 opts.Template = optional.Some(ctx.FormBool("template")) 154 } 155 156 if ctx.FormBool("exclusive") { 157 opts.Collaborate = optional.Some(false) 158 } 159 160 mode := ctx.FormString("mode") 161 switch mode { 162 case "source": 163 opts.Fork = optional.Some(false) 164 opts.Mirror = optional.Some(false) 165 case "fork": 166 opts.Fork = optional.Some(true) 167 case "mirror": 168 opts.Mirror = optional.Some(true) 169 case "collaborative": 170 opts.Mirror = optional.Some(false) 171 opts.Collaborate = optional.Some(true) 172 case "": 173 default: 174 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode)) 175 return 176 } 177 178 if ctx.FormString("archived") != "" { 179 opts.Archived = optional.Some(ctx.FormBool("archived")) 180 } 181 182 if ctx.FormString("is_private") != "" { 183 opts.IsPrivate = optional.Some(ctx.FormBool("is_private")) 184 } 185 186 sortMode := ctx.FormString("sort") 187 if len(sortMode) > 0 { 188 sortOrder := ctx.FormString("order") 189 if len(sortOrder) == 0 { 190 sortOrder = "asc" 191 } 192 if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok { 193 if orderBy, ok := searchModeMap[sortMode]; ok { 194 opts.OrderBy = orderBy 195 } else { 196 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort mode: \"%s\"", sortMode)) 197 return 198 } 199 } else { 200 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort order: \"%s\"", sortOrder)) 201 return 202 } 203 } 204 205 var err error 206 repos, count, err := repo_model.SearchRepository(ctx, opts) 207 if err != nil { 208 ctx.JSON(http.StatusInternalServerError, api.SearchError{ 209 OK: false, 210 Error: err.Error(), 211 }) 212 return 213 } 214 215 results := make([]*api.Repository, len(repos)) 216 for i, repo := range repos { 217 if err = repo.LoadOwner(ctx); err != nil { 218 ctx.JSON(http.StatusInternalServerError, api.SearchError{ 219 OK: false, 220 Error: err.Error(), 221 }) 222 return 223 } 224 permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) 225 if err != nil { 226 ctx.JSON(http.StatusInternalServerError, api.SearchError{ 227 OK: false, 228 Error: err.Error(), 229 }) 230 } 231 results[i] = convert.ToRepo(ctx, repo, permission) 232 } 233 ctx.SetLinkHeader(int(count), opts.PageSize) 234 ctx.SetTotalCountHeader(count) 235 ctx.JSON(http.StatusOK, api.SearchResults{ 236 OK: true, 237 Data: results, 238 }) 239 } 240 241 // CreateUserRepo create a repository for a user 242 func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.CreateRepoOption) { 243 if opt.AutoInit && opt.Readme == "" { 244 opt.Readme = "Default" 245 } 246 247 // If the readme template does not exist, a 400 will be returned. 248 if opt.AutoInit && len(opt.Readme) > 0 && !slices.Contains(repo_module.Readmes, opt.Readme) { 249 ctx.Error(http.StatusBadRequest, "", fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes)) 250 return 251 } 252 253 repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_service.CreateRepoOptions{ 254 Name: opt.Name, 255 Description: opt.Description, 256 IssueLabels: opt.IssueLabels, 257 Gitignores: opt.Gitignores, 258 License: opt.License, 259 Readme: opt.Readme, 260 IsPrivate: opt.Private || setting.Repository.ForcePrivate, 261 AutoInit: opt.AutoInit, 262 DefaultBranch: opt.DefaultBranch, 263 TrustModel: repo_model.ToTrustModel(opt.TrustModel), 264 IsTemplate: opt.Template, 265 ObjectFormatName: opt.ObjectFormatName, 266 }) 267 if err != nil { 268 if repo_model.IsErrRepoAlreadyExist(err) { 269 ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") 270 } else if db.IsErrNameReserved(err) || 271 db.IsErrNamePatternNotAllowed(err) || 272 label.IsErrTemplateLoad(err) { 273 ctx.Error(http.StatusUnprocessableEntity, "", err) 274 } else { 275 ctx.Error(http.StatusInternalServerError, "CreateRepository", err) 276 } 277 return 278 } 279 280 // reload repo from db to get a real state after creation 281 repo, err = repo_model.GetRepositoryByID(ctx, repo.ID) 282 if err != nil { 283 ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err) 284 } 285 286 ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner})) 287 } 288 289 // Create one repository of mine 290 func Create(ctx *context.APIContext) { 291 // swagger:operation POST /user/repos repository user createCurrentUserRepo 292 // --- 293 // summary: Create a repository 294 // consumes: 295 // - application/json 296 // produces: 297 // - application/json 298 // parameters: 299 // - name: body 300 // in: body 301 // schema: 302 // "$ref": "#/definitions/CreateRepoOption" 303 // responses: 304 // "201": 305 // "$ref": "#/responses/Repository" 306 // "400": 307 // "$ref": "#/responses/error" 308 // "409": 309 // description: The repository with the same name already exists. 310 // "422": 311 // "$ref": "#/responses/validationError" 312 opt := web.GetForm(ctx).(*api.CreateRepoOption) 313 if ctx.Doer.IsOrganization() { 314 // Shouldn't reach this condition, but just in case. 315 ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization") 316 return 317 } 318 CreateUserRepo(ctx, ctx.Doer, *opt) 319 } 320 321 // Generate Create a repository using a template 322 func Generate(ctx *context.APIContext) { 323 // swagger:operation POST /repos/{template_owner}/{template_repo}/generate repository generateRepo 324 // --- 325 // summary: Create a repository using a template 326 // consumes: 327 // - application/json 328 // produces: 329 // - application/json 330 // parameters: 331 // - name: template_owner 332 // in: path 333 // description: name of the template repository owner 334 // type: string 335 // required: true 336 // - name: template_repo 337 // in: path 338 // description: name of the template repository 339 // type: string 340 // required: true 341 // - name: body 342 // in: body 343 // schema: 344 // "$ref": "#/definitions/GenerateRepoOption" 345 // responses: 346 // "201": 347 // "$ref": "#/responses/Repository" 348 // "403": 349 // "$ref": "#/responses/forbidden" 350 // "404": 351 // "$ref": "#/responses/notFound" 352 // "409": 353 // description: The repository with the same name already exists. 354 // "422": 355 // "$ref": "#/responses/validationError" 356 form := web.GetForm(ctx).(*api.GenerateRepoOption) 357 358 if !ctx.Repo.Repository.IsTemplate { 359 ctx.Error(http.StatusUnprocessableEntity, "", "this is not a template repo") 360 return 361 } 362 363 if ctx.Doer.IsOrganization() { 364 ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization") 365 return 366 } 367 368 opts := repo_service.GenerateRepoOptions{ 369 Name: form.Name, 370 DefaultBranch: form.DefaultBranch, 371 Description: form.Description, 372 Private: form.Private || setting.Repository.ForcePrivate, 373 GitContent: form.GitContent, 374 Topics: form.Topics, 375 GitHooks: form.GitHooks, 376 Webhooks: form.Webhooks, 377 Avatar: form.Avatar, 378 IssueLabels: form.Labels, 379 ProtectedBranch: form.ProtectedBranch, 380 } 381 382 if !opts.IsValid() { 383 ctx.Error(http.StatusUnprocessableEntity, "", "must select at least one template item") 384 return 385 } 386 387 ctxUser := ctx.Doer 388 var err error 389 if form.Owner != ctxUser.Name { 390 ctxUser, err = user_model.GetUserByName(ctx, form.Owner) 391 if err != nil { 392 if user_model.IsErrUserNotExist(err) { 393 ctx.JSON(http.StatusNotFound, map[string]any{ 394 "error": "request owner `" + form.Owner + "` does not exist", 395 }) 396 return 397 } 398 399 ctx.Error(http.StatusInternalServerError, "GetUserByName", err) 400 return 401 } 402 403 if !ctx.Doer.IsAdmin && !ctxUser.IsOrganization() { 404 ctx.Error(http.StatusForbidden, "", "Only admin can generate repository for other user.") 405 return 406 } 407 408 if !ctx.Doer.IsAdmin { 409 canCreate, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID) 410 if err != nil { 411 ctx.ServerError("CanCreateOrgRepo", err) 412 return 413 } else if !canCreate { 414 ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.") 415 return 416 } 417 } 418 } 419 420 repo, err := repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, ctx.Repo.Repository, opts) 421 if err != nil { 422 if repo_model.IsErrRepoAlreadyExist(err) { 423 ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") 424 } else if db.IsErrNameReserved(err) || 425 db.IsErrNamePatternNotAllowed(err) { 426 ctx.Error(http.StatusUnprocessableEntity, "", err) 427 } else { 428 ctx.Error(http.StatusInternalServerError, "CreateRepository", err) 429 } 430 return 431 } 432 log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) 433 434 ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner})) 435 } 436 437 // CreateOrgRepoDeprecated create one repository of the organization 438 func CreateOrgRepoDeprecated(ctx *context.APIContext) { 439 // swagger:operation POST /org/{org}/repos organization createOrgRepoDeprecated 440 // --- 441 // summary: Create a repository in an organization 442 // deprecated: true 443 // consumes: 444 // - application/json 445 // produces: 446 // - application/json 447 // parameters: 448 // - name: org 449 // in: path 450 // description: name of organization 451 // type: string 452 // required: true 453 // - name: body 454 // in: body 455 // schema: 456 // "$ref": "#/definitions/CreateRepoOption" 457 // responses: 458 // "201": 459 // "$ref": "#/responses/Repository" 460 // "422": 461 // "$ref": "#/responses/validationError" 462 // "403": 463 // "$ref": "#/responses/forbidden" 464 // "404": 465 // "$ref": "#/responses/notFound" 466 467 CreateOrgRepo(ctx) 468 } 469 470 // CreateOrgRepo create one repository of the organization 471 func CreateOrgRepo(ctx *context.APIContext) { 472 // swagger:operation POST /orgs/{org}/repos organization createOrgRepo 473 // --- 474 // summary: Create a repository in an organization 475 // consumes: 476 // - application/json 477 // produces: 478 // - application/json 479 // parameters: 480 // - name: org 481 // in: path 482 // description: name of organization 483 // type: string 484 // required: true 485 // - name: body 486 // in: body 487 // schema: 488 // "$ref": "#/definitions/CreateRepoOption" 489 // responses: 490 // "201": 491 // "$ref": "#/responses/Repository" 492 // "400": 493 // "$ref": "#/responses/error" 494 // "404": 495 // "$ref": "#/responses/notFound" 496 // "403": 497 // "$ref": "#/responses/forbidden" 498 opt := web.GetForm(ctx).(*api.CreateRepoOption) 499 org, err := organization.GetOrgByName(ctx, ctx.Params(":org")) 500 if err != nil { 501 if organization.IsErrOrgNotExist(err) { 502 ctx.Error(http.StatusUnprocessableEntity, "", err) 503 } else { 504 ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) 505 } 506 return 507 } 508 509 if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) { 510 ctx.NotFound("HasOrgOrUserVisible", nil) 511 return 512 } 513 514 if !ctx.Doer.IsAdmin { 515 canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID) 516 if err != nil { 517 ctx.Error(http.StatusInternalServerError, "CanCreateOrgRepo", err) 518 return 519 } else if !canCreate { 520 ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.") 521 return 522 } 523 } 524 CreateUserRepo(ctx, org.AsUser(), *opt) 525 } 526 527 // Get one repository 528 func Get(ctx *context.APIContext) { 529 // swagger:operation GET /repos/{owner}/{repo} repository repoGet 530 // --- 531 // summary: Get a repository 532 // produces: 533 // - application/json 534 // parameters: 535 // - name: owner 536 // in: path 537 // description: owner of the repo 538 // type: string 539 // required: true 540 // - name: repo 541 // in: path 542 // description: name of the repo 543 // type: string 544 // required: true 545 // responses: 546 // "200": 547 // "$ref": "#/responses/Repository" 548 // "404": 549 // "$ref": "#/responses/notFound" 550 551 if err := ctx.Repo.Repository.LoadAttributes(ctx); err != nil { 552 ctx.Error(http.StatusInternalServerError, "Repository.LoadAttributes", err) 553 return 554 } 555 556 ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) 557 } 558 559 // GetByID returns a single Repository 560 func GetByID(ctx *context.APIContext) { 561 // swagger:operation GET /repositories/{id} repository repoGetByID 562 // --- 563 // summary: Get a repository by id 564 // produces: 565 // - application/json 566 // parameters: 567 // - name: id 568 // in: path 569 // description: id of the repo to get 570 // type: integer 571 // format: int64 572 // required: true 573 // responses: 574 // "200": 575 // "$ref": "#/responses/Repository" 576 // "404": 577 // "$ref": "#/responses/notFound" 578 579 repo, err := repo_model.GetRepositoryByID(ctx, ctx.ParamsInt64(":id")) 580 if err != nil { 581 if repo_model.IsErrRepoNotExist(err) { 582 ctx.NotFound() 583 } else { 584 ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err) 585 } 586 return 587 } 588 589 permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) 590 if err != nil { 591 ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) 592 return 593 } else if !permission.HasAnyUnitAccess() { 594 ctx.NotFound() 595 return 596 } 597 ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission)) 598 } 599 600 // Edit edit repository properties 601 func Edit(ctx *context.APIContext) { 602 // swagger:operation PATCH /repos/{owner}/{repo} repository repoEdit 603 // --- 604 // summary: Edit a repository's properties. Only fields that are set will be changed. 605 // produces: 606 // - application/json 607 // parameters: 608 // - name: owner 609 // in: path 610 // description: owner of the repo to edit 611 // type: string 612 // required: true 613 // - name: repo 614 // in: path 615 // description: name of the repo to edit 616 // type: string 617 // required: true 618 // - name: body 619 // in: body 620 // description: "Properties of a repo that you can edit" 621 // schema: 622 // "$ref": "#/definitions/EditRepoOption" 623 // responses: 624 // "200": 625 // "$ref": "#/responses/Repository" 626 // "403": 627 // "$ref": "#/responses/forbidden" 628 // "404": 629 // "$ref": "#/responses/notFound" 630 // "422": 631 // "$ref": "#/responses/validationError" 632 633 opts := *web.GetForm(ctx).(*api.EditRepoOption) 634 635 if err := updateBasicProperties(ctx, opts); err != nil { 636 return 637 } 638 639 if err := updateRepoUnits(ctx, opts); err != nil { 640 return 641 } 642 643 if opts.Archived != nil { 644 if err := updateRepoArchivedState(ctx, opts); err != nil { 645 return 646 } 647 } 648 649 if opts.MirrorInterval != nil || opts.EnablePrune != nil { 650 if err := updateMirror(ctx, opts); err != nil { 651 return 652 } 653 } 654 655 repo, err := repo_model.GetRepositoryByID(ctx, ctx.Repo.Repository.ID) 656 if err != nil { 657 ctx.InternalServerError(err) 658 return 659 } 660 661 ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, ctx.Repo.Permission)) 662 } 663 664 // updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility 665 func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) error { 666 owner := ctx.Repo.Owner 667 repo := ctx.Repo.Repository 668 newRepoName := repo.Name 669 if opts.Name != nil { 670 newRepoName = *opts.Name 671 } 672 // Check if repository name has been changed and not just a case change 673 if repo.LowerName != strings.ToLower(newRepoName) { 674 if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil { 675 switch { 676 case repo_model.IsErrRepoAlreadyExist(err): 677 ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err) 678 case db.IsErrNameReserved(err): 679 ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is reserved [name: %s]", newRepoName), err) 680 case db.IsErrNamePatternNotAllowed(err): 681 ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name's pattern is not allowed [name: %s, pattern: %s]", newRepoName, err.(db.ErrNamePatternNotAllowed).Pattern), err) 682 default: 683 ctx.Error(http.StatusUnprocessableEntity, "ChangeRepositoryName", err) 684 } 685 return err 686 } 687 688 log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName) 689 } 690 // Update the name in the repo object for the response 691 repo.Name = newRepoName 692 repo.LowerName = strings.ToLower(newRepoName) 693 694 if opts.Description != nil { 695 repo.Description = *opts.Description 696 } 697 698 if opts.Website != nil { 699 repo.Website = *opts.Website 700 } 701 702 visibilityChanged := false 703 if opts.Private != nil { 704 // Visibility of forked repository is forced sync with base repository. 705 if repo.IsFork { 706 if err := repo.GetBaseRepo(ctx); err != nil { 707 ctx.Error(http.StatusInternalServerError, "Unable to load base repository", err) 708 return err 709 } 710 *opts.Private = repo.BaseRepo.IsPrivate 711 } 712 713 visibilityChanged = repo.IsPrivate != *opts.Private 714 // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public 715 if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin { 716 err := fmt.Errorf("cannot change private repository to public") 717 ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err) 718 return err 719 } 720 721 repo.IsPrivate = *opts.Private 722 } 723 724 if opts.Template != nil { 725 repo.IsTemplate = *opts.Template 726 } 727 728 if ctx.Repo.GitRepo == nil && !repo.IsEmpty { 729 var err error 730 ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo) 731 if err != nil { 732 ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err) 733 return err 734 } 735 defer ctx.Repo.GitRepo.Close() 736 } 737 738 // Default branch only updated if changed and exist or the repository is empty 739 if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) { 740 if !repo.IsEmpty { 741 if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, *opts.DefaultBranch); err != nil { 742 if !git.IsErrUnsupportedVersion(err) { 743 ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err) 744 return err 745 } 746 } 747 } 748 repo.DefaultBranch = *opts.DefaultBranch 749 } 750 751 if err := repo_service.UpdateRepository(ctx, repo, visibilityChanged); err != nil { 752 ctx.Error(http.StatusInternalServerError, "UpdateRepository", err) 753 return err 754 } 755 756 log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name) 757 return nil 758 } 759 760 // updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings 761 func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { 762 owner := ctx.Repo.Owner 763 repo := ctx.Repo.Repository 764 765 var units []repo_model.RepoUnit 766 var deleteUnitTypes []unit_model.Type 767 768 currHasIssues := repo.UnitEnabled(ctx, unit_model.TypeIssues) 769 newHasIssues := currHasIssues 770 if opts.HasIssues != nil { 771 newHasIssues = *opts.HasIssues 772 } 773 if currHasIssues || newHasIssues { 774 if newHasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { 775 // Check that values are valid 776 if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) { 777 err := fmt.Errorf("External tracker URL not valid") 778 ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL", err) 779 return err 780 } 781 if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) { 782 err := fmt.Errorf("External tracker URL format not valid") 783 ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL format", err) 784 return err 785 } 786 787 units = append(units, repo_model.RepoUnit{ 788 RepoID: repo.ID, 789 Type: unit_model.TypeExternalTracker, 790 Config: &repo_model.ExternalTrackerConfig{ 791 ExternalTrackerURL: opts.ExternalTracker.ExternalTrackerURL, 792 ExternalTrackerFormat: opts.ExternalTracker.ExternalTrackerFormat, 793 ExternalTrackerStyle: opts.ExternalTracker.ExternalTrackerStyle, 794 ExternalTrackerRegexpPattern: opts.ExternalTracker.ExternalTrackerRegexpPattern, 795 }, 796 }) 797 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) 798 } else if newHasIssues && opts.ExternalTracker == nil && !unit_model.TypeIssues.UnitGlobalDisabled() { 799 // Default to built-in tracker 800 var config *repo_model.IssuesConfig 801 802 if opts.InternalTracker != nil { 803 config = &repo_model.IssuesConfig{ 804 EnableTimetracker: opts.InternalTracker.EnableTimeTracker, 805 AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime, 806 EnableDependencies: opts.InternalTracker.EnableIssueDependencies, 807 } 808 } else if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err != nil { 809 // Unit type doesn't exist so we make a new config file with default values 810 config = &repo_model.IssuesConfig{ 811 EnableTimetracker: true, 812 AllowOnlyContributorsToTrackTime: true, 813 EnableDependencies: true, 814 } 815 } else { 816 config = unit.IssuesConfig() 817 } 818 819 units = append(units, repo_model.RepoUnit{ 820 RepoID: repo.ID, 821 Type: unit_model.TypeIssues, 822 Config: config, 823 }) 824 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) 825 } else if !newHasIssues { 826 if !unit_model.TypeExternalTracker.UnitGlobalDisabled() { 827 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) 828 } 829 if !unit_model.TypeIssues.UnitGlobalDisabled() { 830 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) 831 } 832 } 833 } 834 835 currHasWiki := repo.UnitEnabled(ctx, unit_model.TypeWiki) 836 newHasWiki := currHasWiki 837 if opts.HasWiki != nil { 838 newHasWiki = *opts.HasWiki 839 } 840 if currHasWiki || newHasWiki { 841 if newHasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { 842 // Check that values are valid 843 if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) { 844 err := fmt.Errorf("External wiki URL not valid") 845 ctx.Error(http.StatusUnprocessableEntity, "", "Invalid external wiki URL") 846 return err 847 } 848 849 units = append(units, repo_model.RepoUnit{ 850 RepoID: repo.ID, 851 Type: unit_model.TypeExternalWiki, 852 Config: &repo_model.ExternalWikiConfig{ 853 ExternalWikiURL: opts.ExternalWiki.ExternalWikiURL, 854 }, 855 }) 856 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) 857 } else if newHasWiki && opts.ExternalWiki == nil && !unit_model.TypeWiki.UnitGlobalDisabled() { 858 config := &repo_model.UnitConfig{} 859 units = append(units, repo_model.RepoUnit{ 860 RepoID: repo.ID, 861 Type: unit_model.TypeWiki, 862 Config: config, 863 }) 864 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) 865 } else if !newHasWiki { 866 if !unit_model.TypeExternalWiki.UnitGlobalDisabled() { 867 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) 868 } 869 if !unit_model.TypeWiki.UnitGlobalDisabled() { 870 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) 871 } 872 } 873 } 874 875 currHasPullRequests := repo.UnitEnabled(ctx, unit_model.TypePullRequests) 876 newHasPullRequests := currHasPullRequests 877 if opts.HasPullRequests != nil { 878 newHasPullRequests = *opts.HasPullRequests 879 } 880 if currHasPullRequests || newHasPullRequests { 881 if newHasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() { 882 // We do allow setting individual PR settings through the API, so 883 // we get the config settings and then set them 884 // if those settings were provided in the opts. 885 unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests) 886 var config *repo_model.PullRequestsConfig 887 if err != nil { 888 // Unit type doesn't exist so we make a new config file with default values 889 config = &repo_model.PullRequestsConfig{ 890 IgnoreWhitespaceConflicts: false, 891 AllowMerge: true, 892 AllowRebase: true, 893 AllowRebaseMerge: true, 894 AllowSquash: true, 895 AllowFastForwardOnly: true, 896 AllowManualMerge: true, 897 AutodetectManualMerge: false, 898 AllowRebaseUpdate: true, 899 DefaultDeleteBranchAfterMerge: false, 900 DefaultMergeStyle: repo_model.MergeStyleMerge, 901 DefaultAllowMaintainerEdit: false, 902 } 903 } else { 904 config = unit.PullRequestsConfig() 905 } 906 907 if opts.IgnoreWhitespaceConflicts != nil { 908 config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts 909 } 910 if opts.AllowMerge != nil { 911 config.AllowMerge = *opts.AllowMerge 912 } 913 if opts.AllowRebase != nil { 914 config.AllowRebase = *opts.AllowRebase 915 } 916 if opts.AllowRebaseMerge != nil { 917 config.AllowRebaseMerge = *opts.AllowRebaseMerge 918 } 919 if opts.AllowSquash != nil { 920 config.AllowSquash = *opts.AllowSquash 921 } 922 if opts.AllowFastForwardOnly != nil { 923 config.AllowFastForwardOnly = *opts.AllowFastForwardOnly 924 } 925 if opts.AllowManualMerge != nil { 926 config.AllowManualMerge = *opts.AllowManualMerge 927 } 928 if opts.AutodetectManualMerge != nil { 929 config.AutodetectManualMerge = *opts.AutodetectManualMerge 930 } 931 if opts.AllowRebaseUpdate != nil { 932 config.AllowRebaseUpdate = *opts.AllowRebaseUpdate 933 } 934 if opts.DefaultDeleteBranchAfterMerge != nil { 935 config.DefaultDeleteBranchAfterMerge = *opts.DefaultDeleteBranchAfterMerge 936 } 937 if opts.DefaultMergeStyle != nil { 938 config.DefaultMergeStyle = repo_model.MergeStyle(*opts.DefaultMergeStyle) 939 } 940 if opts.DefaultAllowMaintainerEdit != nil { 941 config.DefaultAllowMaintainerEdit = *opts.DefaultAllowMaintainerEdit 942 } 943 944 units = append(units, repo_model.RepoUnit{ 945 RepoID: repo.ID, 946 Type: unit_model.TypePullRequests, 947 Config: config, 948 }) 949 } else if !newHasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() { 950 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests) 951 } 952 } 953 954 currHasProjects := repo.UnitEnabled(ctx, unit_model.TypeProjects) 955 newHasProjects := currHasProjects 956 if opts.HasProjects != nil { 957 newHasProjects = *opts.HasProjects 958 } 959 if currHasProjects || newHasProjects { 960 if newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { 961 unit, err := repo.GetUnit(ctx, unit_model.TypeProjects) 962 var config *repo_model.ProjectsConfig 963 if err != nil { 964 config = &repo_model.ProjectsConfig{ 965 ProjectsMode: repo_model.ProjectsModeAll, 966 } 967 } else { 968 config = unit.ProjectsConfig() 969 } 970 971 if opts.ProjectsMode != nil { 972 config.ProjectsMode = repo_model.ProjectsMode(*opts.ProjectsMode) 973 } 974 975 units = append(units, repo_model.RepoUnit{ 976 RepoID: repo.ID, 977 Type: unit_model.TypeProjects, 978 Config: config, 979 }) 980 } else if !newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { 981 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) 982 } 983 } 984 985 if opts.HasReleases != nil && !unit_model.TypeReleases.UnitGlobalDisabled() { 986 if *opts.HasReleases { 987 units = append(units, repo_model.RepoUnit{ 988 RepoID: repo.ID, 989 Type: unit_model.TypeReleases, 990 }) 991 } else { 992 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases) 993 } 994 } 995 996 if opts.HasPackages != nil && !unit_model.TypePackages.UnitGlobalDisabled() { 997 if *opts.HasPackages { 998 units = append(units, repo_model.RepoUnit{ 999 RepoID: repo.ID, 1000 Type: unit_model.TypePackages, 1001 }) 1002 } else { 1003 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) 1004 } 1005 } 1006 1007 if opts.HasActions != nil && !unit_model.TypeActions.UnitGlobalDisabled() { 1008 if *opts.HasActions { 1009 units = append(units, repo_model.RepoUnit{ 1010 RepoID: repo.ID, 1011 Type: unit_model.TypeActions, 1012 }) 1013 } else { 1014 deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions) 1015 } 1016 } 1017 1018 if len(units)+len(deleteUnitTypes) > 0 { 1019 if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { 1020 ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err) 1021 return err 1022 } 1023 } 1024 1025 log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name) 1026 return nil 1027 } 1028 1029 // updateRepoArchivedState updates repo's archive state 1030 func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) error { 1031 repo := ctx.Repo.Repository 1032 // archive / un-archive 1033 if opts.Archived != nil { 1034 if repo.IsMirror { 1035 err := fmt.Errorf("repo is a mirror, cannot archive/un-archive") 1036 ctx.Error(http.StatusUnprocessableEntity, err.Error(), err) 1037 return err 1038 } 1039 if *opts.Archived { 1040 if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil { 1041 log.Error("Tried to archive a repo: %s", err) 1042 ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err) 1043 return err 1044 } 1045 if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil { 1046 log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) 1047 } 1048 log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) 1049 } else { 1050 if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil { 1051 log.Error("Tried to un-archive a repo: %s", err) 1052 ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err) 1053 return err 1054 } 1055 if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) { 1056 if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil { 1057 log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) 1058 } 1059 } 1060 log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) 1061 } 1062 } 1063 return nil 1064 } 1065 1066 // updateMirror updates a repo's mirror Interval and EnablePrune 1067 func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { 1068 repo := ctx.Repo.Repository 1069 1070 // Skip this update if the repo is not a mirror, do not return error. 1071 // Because reporting errors only makes the logic more complex&fragile, it doesn't really help end users. 1072 if !repo.IsMirror { 1073 return nil 1074 } 1075 1076 // get the mirror from the repo 1077 mirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID) 1078 if err != nil { 1079 log.Error("Failed to get mirror: %s", err) 1080 ctx.Error(http.StatusInternalServerError, "MirrorInterval", err) 1081 return err 1082 } 1083 1084 // update MirrorInterval 1085 if opts.MirrorInterval != nil { 1086 // MirrorInterval should be a duration 1087 interval, err := time.ParseDuration(*opts.MirrorInterval) 1088 if err != nil { 1089 log.Error("Wrong format for MirrorInternal Sent: %s", err) 1090 ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err) 1091 return err 1092 } 1093 1094 // Ensure the provided duration is not too short 1095 if interval != 0 && interval < setting.Mirror.MinInterval { 1096 err := fmt.Errorf("invalid mirror interval: %s is below minimum interval: %s", interval, setting.Mirror.MinInterval) 1097 ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err) 1098 return err 1099 } 1100 1101 mirror.Interval = interval 1102 mirror.Repo = repo 1103 mirror.ScheduleNextUpdate() 1104 log.Trace("Repository %s Mirror[%d] Set Interval: %s NextUpdateUnix: %s", repo.FullName(), mirror.ID, interval, mirror.NextUpdateUnix) 1105 } 1106 1107 // update EnablePrune 1108 if opts.EnablePrune != nil { 1109 mirror.EnablePrune = *opts.EnablePrune 1110 log.Trace("Repository %s Mirror[%d] Set EnablePrune: %t", repo.FullName(), mirror.ID, mirror.EnablePrune) 1111 } 1112 1113 // finally update the mirror in the DB 1114 if err := repo_model.UpdateMirror(ctx, mirror); err != nil { 1115 log.Error("Failed to Set Mirror Interval: %s", err) 1116 ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err) 1117 return err 1118 } 1119 1120 return nil 1121 } 1122 1123 // Delete one repository 1124 func Delete(ctx *context.APIContext) { 1125 // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete 1126 // --- 1127 // summary: Delete a repository 1128 // produces: 1129 // - application/json 1130 // parameters: 1131 // - name: owner 1132 // in: path 1133 // description: owner of the repo to delete 1134 // type: string 1135 // required: true 1136 // - name: repo 1137 // in: path 1138 // description: name of the repo to delete 1139 // type: string 1140 // required: true 1141 // responses: 1142 // "204": 1143 // "$ref": "#/responses/empty" 1144 // "403": 1145 // "$ref": "#/responses/forbidden" 1146 // "404": 1147 // "$ref": "#/responses/notFound" 1148 1149 owner := ctx.Repo.Owner 1150 repo := ctx.Repo.Repository 1151 1152 canDelete, err := repo_module.CanUserDelete(ctx, repo, ctx.Doer) 1153 if err != nil { 1154 ctx.Error(http.StatusInternalServerError, "CanUserDelete", err) 1155 return 1156 } else if !canDelete { 1157 ctx.Error(http.StatusForbidden, "", "Given user is not owner of organization.") 1158 return 1159 } 1160 1161 if ctx.Repo.GitRepo != nil { 1162 ctx.Repo.GitRepo.Close() 1163 } 1164 1165 if err := repo_service.DeleteRepository(ctx, ctx.Doer, repo, true); err != nil { 1166 ctx.Error(http.StatusInternalServerError, "DeleteRepository", err) 1167 return 1168 } 1169 1170 log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name) 1171 ctx.Status(http.StatusNoContent) 1172 } 1173 1174 // GetIssueTemplates returns the issue templates for a repository 1175 func GetIssueTemplates(ctx *context.APIContext) { 1176 // swagger:operation GET /repos/{owner}/{repo}/issue_templates repository repoGetIssueTemplates 1177 // --- 1178 // summary: Get available issue templates for a repository 1179 // produces: 1180 // - application/json 1181 // parameters: 1182 // - name: owner 1183 // in: path 1184 // description: owner of the repo 1185 // type: string 1186 // required: true 1187 // - name: repo 1188 // in: path 1189 // description: name of the repo 1190 // type: string 1191 // required: true 1192 // responses: 1193 // "200": 1194 // "$ref": "#/responses/IssueTemplates" 1195 // "404": 1196 // "$ref": "#/responses/notFound" 1197 ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) 1198 if cnt := len(ret.TemplateErrors); cnt != 0 { 1199 ctx.Resp.Header().Add("X-Gitea-Warning", "error occurs when parsing issue template: count="+strconv.Itoa(cnt)) 1200 } 1201 ctx.JSON(http.StatusOK, ret.IssueTemplates) 1202 } 1203 1204 // GetIssueConfig returns the issue config for a repo 1205 func GetIssueConfig(ctx *context.APIContext) { 1206 // swagger:operation GET /repos/{owner}/{repo}/issue_config repository repoGetIssueConfig 1207 // --- 1208 // summary: Returns the issue config for a repo 1209 // produces: 1210 // - application/json 1211 // parameters: 1212 // - name: owner 1213 // in: path 1214 // description: owner of the repo 1215 // type: string 1216 // required: true 1217 // - name: repo 1218 // in: path 1219 // description: name of the repo 1220 // type: string 1221 // required: true 1222 // responses: 1223 // "200": 1224 // "$ref": "#/responses/RepoIssueConfig" 1225 // "404": 1226 // "$ref": "#/responses/notFound" 1227 issueConfig, _ := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) 1228 ctx.JSON(http.StatusOK, issueConfig) 1229 } 1230 1231 // ValidateIssueConfig returns validation errors for the issue config 1232 func ValidateIssueConfig(ctx *context.APIContext) { 1233 // swagger:operation GET /repos/{owner}/{repo}/issue_config/validate repository repoValidateIssueConfig 1234 // --- 1235 // summary: Returns the validation information for a issue config 1236 // produces: 1237 // - application/json 1238 // parameters: 1239 // - name: owner 1240 // in: path 1241 // description: owner of the repo 1242 // type: string 1243 // required: true 1244 // - name: repo 1245 // in: path 1246 // description: name of the repo 1247 // type: string 1248 // required: true 1249 // responses: 1250 // "200": 1251 // "$ref": "#/responses/RepoIssueConfigValidation" 1252 // "404": 1253 // "$ref": "#/responses/notFound" 1254 _, err := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) 1255 1256 if err == nil { 1257 ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""}) 1258 } else { 1259 ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: false, Message: err.Error()}) 1260 } 1261 } 1262 1263 func ListRepoActivityFeeds(ctx *context.APIContext) { 1264 // swagger:operation GET /repos/{owner}/{repo}/activities/feeds repository repoListActivityFeeds 1265 // --- 1266 // summary: List a repository's activity feeds 1267 // produces: 1268 // - application/json 1269 // parameters: 1270 // - name: owner 1271 // in: path 1272 // description: owner of the repo 1273 // type: string 1274 // required: true 1275 // - name: repo 1276 // in: path 1277 // description: name of the repo 1278 // type: string 1279 // required: true 1280 // - name: date 1281 // in: query 1282 // description: the date of the activities to be found 1283 // type: string 1284 // format: date 1285 // - name: page 1286 // in: query 1287 // description: page number of results to return (1-based) 1288 // type: integer 1289 // - name: limit 1290 // in: query 1291 // description: page size of results 1292 // type: integer 1293 // responses: 1294 // "200": 1295 // "$ref": "#/responses/ActivityFeedsList" 1296 // "404": 1297 // "$ref": "#/responses/notFound" 1298 1299 listOptions := utils.GetListOptions(ctx) 1300 1301 opts := activities_model.GetFeedsOptions{ 1302 RequestedRepo: ctx.Repo.Repository, 1303 Actor: ctx.Doer, 1304 IncludePrivate: true, 1305 Date: ctx.FormString("date"), 1306 ListOptions: listOptions, 1307 } 1308 1309 feeds, count, err := activities_model.GetFeeds(ctx, opts) 1310 if err != nil { 1311 ctx.Error(http.StatusInternalServerError, "GetFeeds", err) 1312 return 1313 } 1314 ctx.SetTotalCountHeader(count) 1315 1316 ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer)) 1317 }