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