code.gitea.io/gitea@v1.22.3/models/repo/repo.go (about) 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repo 5 6 import ( 7 "context" 8 "fmt" 9 "html/template" 10 "net" 11 "net/url" 12 "path/filepath" 13 "strconv" 14 "strings" 15 16 "code.gitea.io/gitea/models/db" 17 "code.gitea.io/gitea/models/unit" 18 user_model "code.gitea.io/gitea/models/user" 19 "code.gitea.io/gitea/modules/base" 20 "code.gitea.io/gitea/modules/git" 21 "code.gitea.io/gitea/modules/log" 22 "code.gitea.io/gitea/modules/markup" 23 "code.gitea.io/gitea/modules/optional" 24 "code.gitea.io/gitea/modules/setting" 25 api "code.gitea.io/gitea/modules/structs" 26 "code.gitea.io/gitea/modules/timeutil" 27 "code.gitea.io/gitea/modules/util" 28 29 "xorm.io/builder" 30 ) 31 32 // ErrUserDoesNotHaveAccessToRepo represents an error where the user doesn't has access to a given repo. 33 type ErrUserDoesNotHaveAccessToRepo struct { 34 UserID int64 35 RepoName string 36 } 37 38 // IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists. 39 func IsErrUserDoesNotHaveAccessToRepo(err error) bool { 40 _, ok := err.(ErrUserDoesNotHaveAccessToRepo) 41 return ok 42 } 43 44 func (err ErrUserDoesNotHaveAccessToRepo) Error() string { 45 return fmt.Sprintf("user doesn't have access to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName) 46 } 47 48 func (err ErrUserDoesNotHaveAccessToRepo) Unwrap() error { 49 return util.ErrPermissionDenied 50 } 51 52 type ErrRepoIsArchived struct { 53 Repo *Repository 54 } 55 56 func (err ErrRepoIsArchived) Error() string { 57 return fmt.Sprintf("%s is archived", err.Repo.LogString()) 58 } 59 60 var ( 61 reservedRepoNames = []string{".", "..", "-"} 62 reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"} 63 ) 64 65 // IsUsableRepoName returns true when repository is usable 66 func IsUsableRepoName(name string) error { 67 if db.AlphaDashDotPattern.MatchString(name) { 68 // Note: usually this error is normally caught up earlier in the UI 69 return db.ErrNameCharsNotAllowed{Name: name} 70 } 71 return db.IsUsableName(reservedRepoNames, reservedRepoPatterns, name) 72 } 73 74 // TrustModelType defines the types of trust model for this repository 75 type TrustModelType int 76 77 // kinds of TrustModel 78 const ( 79 DefaultTrustModel TrustModelType = iota // default trust model 80 CommitterTrustModel 81 CollaboratorTrustModel 82 CollaboratorCommitterTrustModel 83 ) 84 85 // String converts a TrustModelType to a string 86 func (t TrustModelType) String() string { 87 switch t { 88 case DefaultTrustModel: 89 return "default" 90 case CommitterTrustModel: 91 return "committer" 92 case CollaboratorTrustModel: 93 return "collaborator" 94 case CollaboratorCommitterTrustModel: 95 return "collaboratorcommitter" 96 } 97 return "default" 98 } 99 100 // ToTrustModel converts a string to a TrustModelType 101 func ToTrustModel(model string) TrustModelType { 102 switch strings.ToLower(strings.TrimSpace(model)) { 103 case "default": 104 return DefaultTrustModel 105 case "collaborator": 106 return CollaboratorTrustModel 107 case "committer": 108 return CommitterTrustModel 109 case "collaboratorcommitter": 110 return CollaboratorCommitterTrustModel 111 } 112 return DefaultTrustModel 113 } 114 115 // RepositoryStatus defines the status of repository 116 type RepositoryStatus int 117 118 // all kinds of RepositoryStatus 119 const ( 120 RepositoryReady RepositoryStatus = iota // a normal repository 121 RepositoryBeingMigrated // repository is migrating 122 RepositoryPendingTransfer // repository pending in ownership transfer state 123 RepositoryBroken // repository is in a permanently broken state 124 ) 125 126 // Repository represents a git repository. 127 type Repository struct { 128 ID int64 `xorm:"pk autoincr"` 129 OwnerID int64 `xorm:"UNIQUE(s) index"` 130 OwnerName string 131 Owner *user_model.User `xorm:"-"` 132 LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` 133 Name string `xorm:"INDEX NOT NULL"` 134 Description string `xorm:"TEXT"` 135 Website string `xorm:"VARCHAR(2048)"` 136 OriginalServiceType api.GitServiceType `xorm:"index"` 137 OriginalURL string `xorm:"VARCHAR(2048)"` 138 DefaultBranch string 139 DefaultWikiBranch string 140 141 NumWatches int 142 NumStars int 143 NumForks int 144 NumIssues int 145 NumClosedIssues int 146 NumOpenIssues int `xorm:"-"` 147 NumPulls int 148 NumClosedPulls int 149 NumOpenPulls int `xorm:"-"` 150 NumMilestones int `xorm:"NOT NULL DEFAULT 0"` 151 NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"` 152 NumOpenMilestones int `xorm:"-"` 153 NumProjects int `xorm:"NOT NULL DEFAULT 0"` 154 NumClosedProjects int `xorm:"NOT NULL DEFAULT 0"` 155 NumOpenProjects int `xorm:"-"` 156 NumActionRuns int `xorm:"NOT NULL DEFAULT 0"` 157 NumClosedActionRuns int `xorm:"NOT NULL DEFAULT 0"` 158 NumOpenActionRuns int `xorm:"-"` 159 160 IsPrivate bool `xorm:"INDEX"` 161 IsEmpty bool `xorm:"INDEX"` 162 IsArchived bool `xorm:"INDEX"` 163 IsMirror bool `xorm:"INDEX"` 164 165 Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"` 166 167 RenderingMetas map[string]string `xorm:"-"` 168 DocumentRenderingMetas map[string]string `xorm:"-"` 169 Units []*RepoUnit `xorm:"-"` 170 PrimaryLanguage *LanguageStat `xorm:"-"` 171 172 IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"` 173 ForkID int64 `xorm:"INDEX"` 174 BaseRepo *Repository `xorm:"-"` 175 IsTemplate bool `xorm:"INDEX NOT NULL DEFAULT false"` 176 TemplateID int64 `xorm:"INDEX"` 177 Size int64 `xorm:"NOT NULL DEFAULT 0"` 178 GitSize int64 `xorm:"NOT NULL DEFAULT 0"` 179 LFSSize int64 `xorm:"NOT NULL DEFAULT 0"` 180 CodeIndexerStatus *RepoIndexerStatus `xorm:"-"` 181 StatsIndexerStatus *RepoIndexerStatus `xorm:"-"` 182 IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"` 183 CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"` 184 Topics []string `xorm:"TEXT JSON"` 185 ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"` 186 187 TrustModel TrustModelType 188 189 // Avatar: ID(10-20)-md5(32) - must fit into 64 symbols 190 Avatar string `xorm:"VARCHAR(64)"` 191 192 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` 193 UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` 194 ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"` 195 } 196 197 func init() { 198 db.RegisterModel(new(Repository)) 199 } 200 201 func (repo *Repository) GetName() string { 202 return repo.Name 203 } 204 205 func (repo *Repository) GetOwnerName() string { 206 return repo.OwnerName 207 } 208 209 // SanitizedOriginalURL returns a sanitized OriginalURL 210 func (repo *Repository) SanitizedOriginalURL() string { 211 if repo.OriginalURL == "" { 212 return "" 213 } 214 u, _ := util.SanitizeURL(repo.OriginalURL) 215 return u 216 } 217 218 // text representations to be returned in SizeDetail.Name 219 const ( 220 SizeDetailNameGit = "git" 221 SizeDetailNameLFS = "lfs" 222 ) 223 224 type SizeDetail struct { 225 Name string 226 Size int64 227 } 228 229 // SizeDetails forms a struct with various size details about repository 230 func (repo *Repository) SizeDetails() []SizeDetail { 231 sizeDetails := []SizeDetail{ 232 { 233 Name: SizeDetailNameGit, 234 Size: repo.GitSize, 235 }, 236 { 237 Name: SizeDetailNameLFS, 238 Size: repo.LFSSize, 239 }, 240 } 241 return sizeDetails 242 } 243 244 // SizeDetailsString returns a concatenation of all repository size details as a string 245 func (repo *Repository) SizeDetailsString() string { 246 var str strings.Builder 247 sizeDetails := repo.SizeDetails() 248 for _, detail := range sizeDetails { 249 str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size))) 250 } 251 return strings.TrimSuffix(str.String(), ", ") 252 } 253 254 func (repo *Repository) LogString() string { 255 if repo == nil { 256 return "<Repository nil>" 257 } 258 return fmt.Sprintf("<Repository %d:%s/%s>", repo.ID, repo.OwnerName, repo.Name) 259 } 260 261 // IsBeingMigrated indicates that repository is being migrated 262 func (repo *Repository) IsBeingMigrated() bool { 263 return repo.Status == RepositoryBeingMigrated 264 } 265 266 // IsBeingCreated indicates that repository is being migrated or forked 267 func (repo *Repository) IsBeingCreated() bool { 268 return repo.IsBeingMigrated() 269 } 270 271 // IsBroken indicates that repository is broken 272 func (repo *Repository) IsBroken() bool { 273 return repo.Status == RepositoryBroken 274 } 275 276 // MarkAsBrokenEmpty marks the repo as broken and empty 277 func (repo *Repository) MarkAsBrokenEmpty() { 278 repo.Status = RepositoryBroken 279 repo.IsEmpty = true 280 } 281 282 // AfterLoad is invoked from XORM after setting the values of all fields of this object. 283 func (repo *Repository) AfterLoad() { 284 repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues 285 repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls 286 repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones 287 repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects 288 repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns 289 if repo.DefaultWikiBranch == "" { 290 repo.DefaultWikiBranch = setting.Repository.DefaultBranch 291 } 292 } 293 294 // LoadAttributes loads attributes of the repository. 295 func (repo *Repository) LoadAttributes(ctx context.Context) error { 296 // Load owner 297 if err := repo.LoadOwner(ctx); err != nil { 298 return fmt.Errorf("load owner: %w", err) 299 } 300 301 // Load primary language 302 stats := make(LanguageStatList, 0, 1) 303 if err := db.GetEngine(ctx). 304 Where("`repo_id` = ? AND `is_primary` = ? AND `language` != ?", repo.ID, true, "other"). 305 Find(&stats); err != nil { 306 return fmt.Errorf("find primary languages: %w", err) 307 } 308 stats.LoadAttributes() 309 for _, st := range stats { 310 if st.RepoID == repo.ID { 311 repo.PrimaryLanguage = st 312 break 313 } 314 } 315 return nil 316 } 317 318 // FullName returns the repository full name 319 func (repo *Repository) FullName() string { 320 return repo.OwnerName + "/" + repo.Name 321 } 322 323 // HTMLURL returns the repository HTML URL 324 func (repo *Repository) HTMLURL() string { 325 return setting.AppURL + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) 326 } 327 328 // CommitLink make link to by commit full ID 329 // note: won't check whether it's an right id 330 func (repo *Repository) CommitLink(commitID string) (result string) { 331 if git.IsEmptyCommitID(commitID) { 332 result = "" 333 } else { 334 result = repo.Link() + "/commit/" + url.PathEscape(commitID) 335 } 336 return result 337 } 338 339 // APIURL returns the repository API URL 340 func (repo *Repository) APIURL() string { 341 return setting.AppURL + "api/v1/repos/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) 342 } 343 344 // GetCommitsCountCacheKey returns cache key used for commits count caching. 345 func (repo *Repository) GetCommitsCountCacheKey(contextName string, isRef bool) string { 346 var prefix string 347 if isRef { 348 prefix = "ref" 349 } else { 350 prefix = "commit" 351 } 352 return fmt.Sprintf("commits-count-%d-%s-%s", repo.ID, prefix, contextName) 353 } 354 355 // LoadUnits loads repo units into repo.Units 356 func (repo *Repository) LoadUnits(ctx context.Context) (err error) { 357 if repo.Units != nil { 358 return nil 359 } 360 361 repo.Units, err = getUnitsByRepoID(ctx, repo.ID) 362 if log.IsTrace() { 363 unitTypeStrings := make([]string, len(repo.Units)) 364 for i, unit := range repo.Units { 365 unitTypeStrings[i] = unit.Type.String() 366 } 367 log.Trace("repo.Units, ID=%d, Types: [%s]", repo.ID, strings.Join(unitTypeStrings, ", ")) 368 } 369 370 return err 371 } 372 373 // UnitEnabled if this repository has the given unit enabled 374 func (repo *Repository) UnitEnabled(ctx context.Context, tp unit.Type) bool { 375 if err := repo.LoadUnits(ctx); err != nil { 376 log.Warn("Error loading repository (ID: %d) units: %s", repo.ID, err.Error()) 377 } 378 for _, unit := range repo.Units { 379 if unit.Type == tp { 380 return true 381 } 382 } 383 return false 384 } 385 386 // MustGetUnit always returns a RepoUnit object 387 func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit { 388 ru, err := repo.GetUnit(ctx, tp) 389 if err == nil { 390 return ru 391 } 392 393 if tp == unit.TypeExternalWiki { 394 return &RepoUnit{ 395 Type: tp, 396 Config: new(ExternalWikiConfig), 397 } 398 } else if tp == unit.TypeExternalTracker { 399 return &RepoUnit{ 400 Type: tp, 401 Config: new(ExternalTrackerConfig), 402 } 403 } else if tp == unit.TypePullRequests { 404 return &RepoUnit{ 405 Type: tp, 406 Config: new(PullRequestsConfig), 407 } 408 } else if tp == unit.TypeIssues { 409 return &RepoUnit{ 410 Type: tp, 411 Config: new(IssuesConfig), 412 } 413 } else if tp == unit.TypeActions { 414 return &RepoUnit{ 415 Type: tp, 416 Config: new(ActionsConfig), 417 } 418 } else if tp == unit.TypeProjects { 419 cfg := new(ProjectsConfig) 420 cfg.ProjectsMode = ProjectsModeNone 421 return &RepoUnit{ 422 Type: tp, 423 Config: cfg, 424 } 425 } 426 427 return &RepoUnit{ 428 Type: tp, 429 Config: new(UnitConfig), 430 } 431 } 432 433 // GetUnit returns a RepoUnit object 434 func (repo *Repository) GetUnit(ctx context.Context, tp unit.Type) (*RepoUnit, error) { 435 if err := repo.LoadUnits(ctx); err != nil { 436 return nil, err 437 } 438 for _, unit := range repo.Units { 439 if unit.Type == tp { 440 return unit, nil 441 } 442 } 443 return nil, ErrUnitTypeNotExist{tp} 444 } 445 446 // LoadOwner loads owner user 447 func (repo *Repository) LoadOwner(ctx context.Context) (err error) { 448 if repo.Owner != nil { 449 return nil 450 } 451 452 repo.Owner, err = user_model.GetUserByID(ctx, repo.OwnerID) 453 return err 454 } 455 456 // MustOwner always returns a valid *user_model.User object to avoid 457 // conceptually impossible error handling. 458 // It creates a fake object that contains error details 459 // when error occurs. 460 func (repo *Repository) MustOwner(ctx context.Context) *user_model.User { 461 if err := repo.LoadOwner(ctx); err != nil { 462 return &user_model.User{ 463 Name: "error", 464 FullName: err.Error(), 465 } 466 } 467 468 return repo.Owner 469 } 470 471 // ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers. 472 func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { 473 if len(repo.RenderingMetas) == 0 { 474 metas := map[string]string{ 475 "user": repo.OwnerName, 476 "repo": repo.Name, 477 "repoPath": repo.RepoPath(), 478 "mode": "comment", 479 } 480 481 unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker) 482 if err == nil { 483 metas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat 484 switch unit.ExternalTrackerConfig().ExternalTrackerStyle { 485 case markup.IssueNameStyleAlphanumeric: 486 metas["style"] = markup.IssueNameStyleAlphanumeric 487 case markup.IssueNameStyleRegexp: 488 metas["style"] = markup.IssueNameStyleRegexp 489 metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern 490 default: 491 metas["style"] = markup.IssueNameStyleNumeric 492 } 493 } 494 495 repo.MustOwner(ctx) 496 if repo.Owner.IsOrganization() { 497 teams := make([]string, 0, 5) 498 _ = db.GetEngine(ctx).Table("team_repo"). 499 Join("INNER", "team", "team.id = team_repo.team_id"). 500 Where("team_repo.repo_id = ?", repo.ID). 501 Select("team.lower_name"). 502 OrderBy("team.lower_name"). 503 Find(&teams) 504 metas["teams"] = "," + strings.Join(teams, ",") + "," 505 metas["org"] = strings.ToLower(repo.OwnerName) 506 } 507 508 repo.RenderingMetas = metas 509 } 510 return repo.RenderingMetas 511 } 512 513 // ComposeDocumentMetas composes a map of metas for properly rendering documents 514 func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string { 515 if len(repo.DocumentRenderingMetas) == 0 { 516 metas := map[string]string{} 517 for k, v := range repo.ComposeMetas(ctx) { 518 metas[k] = v 519 } 520 metas["mode"] = "document" 521 repo.DocumentRenderingMetas = metas 522 } 523 return repo.DocumentRenderingMetas 524 } 525 526 // GetBaseRepo populates repo.BaseRepo for a fork repository and 527 // returns an error on failure (NOTE: no error is returned for 528 // non-fork repositories, and BaseRepo will be left untouched) 529 func (repo *Repository) GetBaseRepo(ctx context.Context) (err error) { 530 if !repo.IsFork { 531 return nil 532 } 533 534 if repo.BaseRepo != nil { 535 return nil 536 } 537 repo.BaseRepo, err = GetRepositoryByID(ctx, repo.ForkID) 538 return err 539 } 540 541 // IsGenerated returns whether _this_ repository was generated from a template 542 func (repo *Repository) IsGenerated() bool { 543 return repo.TemplateID != 0 544 } 545 546 // RepoPath returns repository path by given user and repository name. 547 func RepoPath(userName, repoName string) string { //revive:disable-line:exported 548 return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".git") 549 } 550 551 // RepoPath returns the repository path 552 func (repo *Repository) RepoPath() string { 553 return RepoPath(repo.OwnerName, repo.Name) 554 } 555 556 // Link returns the repository relative url 557 func (repo *Repository) Link() string { 558 return setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) 559 } 560 561 // ComposeCompareURL returns the repository comparison URL 562 func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) string { 563 return fmt.Sprintf("%s/%s/compare/%s...%s", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), util.PathEscapeSegments(oldCommitID), util.PathEscapeSegments(newCommitID)) 564 } 565 566 func (repo *Repository) ComposeBranchCompareURL(baseRepo *Repository, branchName string) string { 567 if baseRepo == nil { 568 baseRepo = repo 569 } 570 var cmpBranchEscaped string 571 if repo.ID != baseRepo.ID { 572 cmpBranchEscaped = fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name)) 573 } 574 cmpBranchEscaped = fmt.Sprintf("%s%s", cmpBranchEscaped, util.PathEscapeSegments(branchName)) 575 return fmt.Sprintf("%s/compare/%s...%s", baseRepo.Link(), util.PathEscapeSegments(baseRepo.DefaultBranch), cmpBranchEscaped) 576 } 577 578 // IsOwnedBy returns true when user owns this repository 579 func (repo *Repository) IsOwnedBy(userID int64) bool { 580 return repo.OwnerID == userID 581 } 582 583 // CanCreateBranch returns true if repository meets the requirements for creating new branches. 584 func (repo *Repository) CanCreateBranch() bool { 585 return !repo.IsMirror 586 } 587 588 // CanEnablePulls returns true if repository meets the requirements of accepting pulls. 589 func (repo *Repository) CanEnablePulls() bool { 590 return !repo.IsMirror && !repo.IsEmpty 591 } 592 593 // AllowsPulls returns true if repository meets the requirements of accepting pulls and has them enabled. 594 func (repo *Repository) AllowsPulls(ctx context.Context) bool { 595 return repo.CanEnablePulls() && repo.UnitEnabled(ctx, unit.TypePullRequests) 596 } 597 598 // CanEnableEditor returns true if repository meets the requirements of web editor. 599 func (repo *Repository) CanEnableEditor() bool { 600 return !repo.IsMirror 601 } 602 603 // DescriptionHTML does special handles to description and return HTML string. 604 func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { 605 desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{ 606 Ctx: ctx, 607 // Don't use Metas to speedup requests 608 }, repo.Description) 609 if err != nil { 610 log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err) 611 return template.HTML(markup.SanitizeDescription(repo.Description)) 612 } 613 return template.HTML(markup.SanitizeDescription(desc)) 614 } 615 616 // CloneLink represents different types of clone URLs of repository. 617 type CloneLink struct { 618 SSH string 619 HTTPS string 620 } 621 622 // ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name. 623 func ComposeHTTPSCloneURL(owner, repo string) string { 624 return fmt.Sprintf("%s%s/%s.git", setting.AppURL, url.PathEscape(owner), url.PathEscape(repo)) 625 } 626 627 func ComposeSSHCloneURL(ownerName, repoName string) string { 628 sshUser := setting.SSH.User 629 sshDomain := setting.SSH.Domain 630 631 // non-standard port, it must use full URI 632 if setting.SSH.Port != 22 { 633 sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port)) 634 return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) 635 } 636 637 // for standard port, it can use a shorter URI (without the port) 638 sshHost := sshDomain 639 if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil { 640 sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets 641 } 642 if setting.Repository.UseCompatSSHURI { 643 return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) 644 } 645 return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) 646 } 647 648 func (repo *Repository) cloneLink(isWiki bool) *CloneLink { 649 repoName := repo.Name 650 if isWiki { 651 repoName += ".wiki" 652 } 653 654 cl := new(CloneLink) 655 cl.SSH = ComposeSSHCloneURL(repo.OwnerName, repoName) 656 cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName) 657 return cl 658 } 659 660 // CloneLink returns clone URLs of repository. 661 func (repo *Repository) CloneLink() (cl *CloneLink) { 662 return repo.cloneLink(false) 663 } 664 665 // GetOriginalURLHostname returns the hostname of a URL or the URL 666 func (repo *Repository) GetOriginalURLHostname() string { 667 u, err := url.Parse(repo.OriginalURL) 668 if err != nil { 669 return repo.OriginalURL 670 } 671 672 return u.Host 673 } 674 675 // GetTrustModel will get the TrustModel for the repo or the default trust model 676 func (repo *Repository) GetTrustModel() TrustModelType { 677 trustModel := repo.TrustModel 678 if trustModel == DefaultTrustModel { 679 trustModel = ToTrustModel(setting.Repository.Signing.DefaultTrustModel) 680 if trustModel == DefaultTrustModel { 681 return CollaboratorTrustModel 682 } 683 } 684 return trustModel 685 } 686 687 // MustNotBeArchived returns ErrRepoIsArchived if the repo is archived 688 func (repo *Repository) MustNotBeArchived() error { 689 if repo.IsArchived { 690 return ErrRepoIsArchived{Repo: repo} 691 } 692 return nil 693 } 694 695 // __________ .__ __ 696 // \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. 697 // | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | 698 // | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | 699 // |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| 700 // \/ \/|__| \/ \/ 701 702 // ErrRepoNotExist represents a "RepoNotExist" kind of error. 703 type ErrRepoNotExist struct { 704 ID int64 705 UID int64 706 OwnerName string 707 Name string 708 } 709 710 // IsErrRepoNotExist checks if an error is a ErrRepoNotExist. 711 func IsErrRepoNotExist(err error) bool { 712 _, ok := err.(ErrRepoNotExist) 713 return ok 714 } 715 716 func (err ErrRepoNotExist) Error() string { 717 return fmt.Sprintf("repository does not exist [id: %d, uid: %d, owner_name: %s, name: %s]", 718 err.ID, err.UID, err.OwnerName, err.Name) 719 } 720 721 // Unwrap unwraps this error as a ErrNotExist error 722 func (err ErrRepoNotExist) Unwrap() error { 723 return util.ErrNotExist 724 } 725 726 // GetRepositoryByOwnerAndName returns the repository by given owner name and repo name 727 func GetRepositoryByOwnerAndName(ctx context.Context, ownerName, repoName string) (*Repository, error) { 728 var repo Repository 729 has, err := db.GetEngine(ctx).Table("repository").Select("repository.*"). 730 Join("INNER", "`user`", "`user`.id = repository.owner_id"). 731 Where("repository.lower_name = ?", strings.ToLower(repoName)). 732 And("`user`.lower_name = ?", strings.ToLower(ownerName)). 733 Get(&repo) 734 if err != nil { 735 return nil, err 736 } else if !has { 737 return nil, ErrRepoNotExist{0, 0, ownerName, repoName} 738 } 739 return &repo, nil 740 } 741 742 // GetRepositoryByName returns the repository by given name under user if exists. 743 func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repository, error) { 744 var repo Repository 745 has, err := db.GetEngine(ctx). 746 Where("`owner_id`=?", ownerID). 747 And("`lower_name`=?", strings.ToLower(name)). 748 NoAutoCondition(). 749 Get(&repo) 750 if err != nil { 751 return nil, err 752 } else if !has { 753 return nil, ErrRepoNotExist{0, ownerID, "", name} 754 } 755 return &repo, err 756 } 757 758 // getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url 759 func getRepositoryURLPathSegments(repoURL string) []string { 760 if strings.HasPrefix(repoURL, setting.AppURL) { 761 return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/") 762 } 763 764 sshURLVariants := [4]string{ 765 setting.SSH.Domain + ":", 766 setting.SSH.User + "@" + setting.SSH.Domain + ":", 767 "git+ssh://" + setting.SSH.Domain + "/", 768 "git+ssh://" + setting.SSH.User + "@" + setting.SSH.Domain + "/", 769 } 770 771 for _, sshURL := range sshURLVariants { 772 if strings.HasPrefix(repoURL, sshURL) { 773 return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/") 774 } 775 } 776 777 return nil 778 } 779 780 // GetRepositoryByURL returns the repository by given url 781 func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) { 782 // possible urls for git: 783 // https://my.domain/sub-path/<owner>/<repo>.git 784 // https://my.domain/sub-path/<owner>/<repo> 785 // git+ssh://user@my.domain/<owner>/<repo>.git 786 // git+ssh://user@my.domain/<owner>/<repo> 787 // user@my.domain:<owner>/<repo>.git 788 // user@my.domain:<owner>/<repo> 789 790 pathSegments := getRepositoryURLPathSegments(repoURL) 791 792 if len(pathSegments) != 2 { 793 return nil, fmt.Errorf("unknown or malformed repository URL") 794 } 795 796 ownerName := pathSegments[0] 797 repoName := strings.TrimSuffix(pathSegments[1], ".git") 798 return GetRepositoryByOwnerAndName(ctx, ownerName, repoName) 799 } 800 801 // GetRepositoryByID returns the repository by given id if exists. 802 func GetRepositoryByID(ctx context.Context, id int64) (*Repository, error) { 803 repo := new(Repository) 804 has, err := db.GetEngine(ctx).ID(id).Get(repo) 805 if err != nil { 806 return nil, err 807 } else if !has { 808 return nil, ErrRepoNotExist{id, 0, "", ""} 809 } 810 return repo, nil 811 } 812 813 // GetRepositoriesMapByIDs returns the repositories by given id slice. 814 func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repository, error) { 815 repos := make(map[int64]*Repository, len(ids)) 816 return repos, db.GetEngine(ctx).In("id", ids).Find(&repos) 817 } 818 819 // IsRepositoryModelOrDirExist returns true if the repository with given name under user has already existed. 820 func IsRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) { 821 has, err := IsRepositoryModelExist(ctx, u, repoName) 822 if err != nil { 823 return false, err 824 } 825 isDir, err := util.IsDir(RepoPath(u.Name, repoName)) 826 return has || isDir, err 827 } 828 829 func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) { 830 return db.GetEngine(ctx).Get(&Repository{ 831 OwnerID: u.ID, 832 LowerName: strings.ToLower(repoName), 833 }) 834 } 835 836 // GetTemplateRepo populates repo.TemplateRepo for a generated repository and 837 // returns an error on failure (NOTE: no error is returned for 838 // non-generated repositories, and TemplateRepo will be left untouched) 839 func GetTemplateRepo(ctx context.Context, repo *Repository) (*Repository, error) { 840 if !repo.IsGenerated() { 841 return nil, nil 842 } 843 844 return GetRepositoryByID(ctx, repo.TemplateID) 845 } 846 847 // TemplateRepo returns the repository, which is template of this repository 848 func (repo *Repository) TemplateRepo(ctx context.Context) *Repository { 849 repo, err := GetTemplateRepo(ctx, repo) 850 if err != nil { 851 log.Error("TemplateRepo: %v", err) 852 return nil 853 } 854 return repo 855 } 856 857 type CountRepositoryOptions struct { 858 OwnerID int64 859 Private optional.Option[bool] 860 } 861 862 // CountRepositories returns number of repositories. 863 // Argument private only takes effect when it is false, 864 // set it true to count all repositories. 865 func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64, error) { 866 sess := db.GetEngine(ctx).Where("id > 0") 867 868 if opts.OwnerID > 0 { 869 sess.And("owner_id = ?", opts.OwnerID) 870 } 871 if opts.Private.Has() { 872 sess.And("is_private=?", opts.Private.Value()) 873 } 874 875 count, err := sess.Count(new(Repository)) 876 if err != nil { 877 return 0, fmt.Errorf("countRepositories: %w", err) 878 } 879 return count, nil 880 } 881 882 // UpdateRepoIssueNumbers updates one of a repositories amount of (open|closed) (issues|PRs) with the current count 883 func UpdateRepoIssueNumbers(ctx context.Context, repoID int64, isPull, isClosed bool) error { 884 field := "num_" 885 if isClosed { 886 field += "closed_" 887 } 888 if isPull { 889 field += "pulls" 890 } else { 891 field += "issues" 892 } 893 894 subQuery := builder.Select("count(*)"). 895 From("issue").Where(builder.Eq{ 896 "repo_id": repoID, 897 "is_pull": isPull, 898 }.And(builder.If(isClosed, builder.Eq{"is_closed": isClosed}))) 899 900 // builder.Update(cond) will generate SQL like UPDATE ... SET cond 901 query := builder.Update(builder.Eq{field: subQuery}). 902 From("repository"). 903 Where(builder.Eq{"id": repoID}) 904 _, err := db.Exec(ctx, query) 905 return err 906 } 907 908 // CountNullArchivedRepository counts the number of repositories with is_archived is null 909 func CountNullArchivedRepository(ctx context.Context) (int64, error) { 910 return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Count(new(Repository)) 911 } 912 913 // FixNullArchivedRepository sets is_archived to false where it is null 914 func FixNullArchivedRepository(ctx context.Context) (int64, error) { 915 return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Cols("is_archived").NoAutoTime().Update(&Repository{ 916 IsArchived: false, 917 }) 918 } 919 920 // UpdateRepositoryOwnerName updates the owner name of all repositories owned by the user 921 func UpdateRepositoryOwnerName(ctx context.Context, oldUserName, newUserName string) error { 922 if _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, oldUserName); err != nil { 923 return fmt.Errorf("change repo owner name: %w", err) 924 } 925 return nil 926 }