code.gitea.io/gitea@v1.19.3/modules/repository/create.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repository 5 6 import ( 7 "context" 8 "fmt" 9 "os" 10 "path" 11 "path/filepath" 12 "strings" 13 14 "code.gitea.io/gitea/models" 15 activities_model "code.gitea.io/gitea/models/activities" 16 "code.gitea.io/gitea/models/db" 17 git_model "code.gitea.io/gitea/models/git" 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 "code.gitea.io/gitea/models/unit" 23 user_model "code.gitea.io/gitea/models/user" 24 "code.gitea.io/gitea/models/webhook" 25 "code.gitea.io/gitea/modules/git" 26 "code.gitea.io/gitea/modules/log" 27 "code.gitea.io/gitea/modules/setting" 28 api "code.gitea.io/gitea/modules/structs" 29 "code.gitea.io/gitea/modules/util" 30 ) 31 32 // CreateRepositoryByExample creates a repository for the user/organization. 33 func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt, isFork bool) (err error) { 34 if err = repo_model.IsUsableRepoName(repo.Name); err != nil { 35 return err 36 } 37 38 has, err := repo_model.IsRepositoryExist(ctx, u, repo.Name) 39 if err != nil { 40 return fmt.Errorf("IsRepositoryExist: %w", err) 41 } else if has { 42 return repo_model.ErrRepoAlreadyExist{ 43 Uname: u.Name, 44 Name: repo.Name, 45 } 46 } 47 48 repoPath := repo_model.RepoPath(u.Name, repo.Name) 49 isExist, err := util.IsExist(repoPath) 50 if err != nil { 51 log.Error("Unable to check if %s exists. Error: %v", repoPath, err) 52 return err 53 } 54 if !overwriteOrAdopt && isExist { 55 log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) 56 return repo_model.ErrRepoFilesAlreadyExist{ 57 Uname: u.Name, 58 Name: repo.Name, 59 } 60 } 61 62 if err = db.Insert(ctx, repo); err != nil { 63 return err 64 } 65 if err = repo_model.DeleteRedirect(ctx, u.ID, repo.Name); err != nil { 66 return err 67 } 68 69 // insert units for repo 70 defaultUnits := unit.DefaultRepoUnits 71 if isFork { 72 defaultUnits = unit.DefaultForkRepoUnits 73 } 74 units := make([]repo_model.RepoUnit, 0, len(defaultUnits)) 75 for _, tp := range defaultUnits { 76 if tp == unit.TypeIssues { 77 units = append(units, repo_model.RepoUnit{ 78 RepoID: repo.ID, 79 Type: tp, 80 Config: &repo_model.IssuesConfig{ 81 EnableTimetracker: setting.Service.DefaultEnableTimetracking, 82 AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime, 83 EnableDependencies: setting.Service.DefaultEnableDependencies, 84 }, 85 }) 86 } else if tp == unit.TypePullRequests { 87 units = append(units, repo_model.RepoUnit{ 88 RepoID: repo.ID, 89 Type: tp, 90 Config: &repo_model.PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), AllowRebaseUpdate: true}, 91 }) 92 } else { 93 units = append(units, repo_model.RepoUnit{ 94 RepoID: repo.ID, 95 Type: tp, 96 }) 97 } 98 } 99 100 if err = db.Insert(ctx, units); err != nil { 101 return err 102 } 103 104 // Remember visibility preference. 105 u.LastRepoVisibility = repo.IsPrivate 106 if err = user_model.UpdateUserCols(ctx, u, "last_repo_visibility"); err != nil { 107 return fmt.Errorf("UpdateUserCols: %w", err) 108 } 109 110 if err = user_model.IncrUserRepoNum(ctx, u.ID); err != nil { 111 return fmt.Errorf("IncrUserRepoNum: %w", err) 112 } 113 u.NumRepos++ 114 115 // Give access to all members in teams with access to all repositories. 116 if u.IsOrganization() { 117 teams, err := organization.FindOrgTeams(ctx, u.ID) 118 if err != nil { 119 return fmt.Errorf("FindOrgTeams: %w", err) 120 } 121 for _, t := range teams { 122 if t.IncludesAllRepositories { 123 if err := models.AddRepository(ctx, t, repo); err != nil { 124 return fmt.Errorf("AddRepository: %w", err) 125 } 126 } 127 } 128 129 if isAdmin, err := access_model.IsUserRepoAdmin(ctx, repo, doer); err != nil { 130 return fmt.Errorf("IsUserRepoAdmin: %w", err) 131 } else if !isAdmin { 132 // Make creator repo admin if it wasn't assigned automatically 133 if err = AddCollaborator(ctx, repo, doer); err != nil { 134 return fmt.Errorf("AddCollaborator: %w", err) 135 } 136 if err = repo_model.ChangeCollaborationAccessMode(ctx, repo, doer.ID, perm.AccessModeAdmin); err != nil { 137 return fmt.Errorf("ChangeCollaborationAccessModeCtx: %w", err) 138 } 139 } 140 } else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { 141 // Organization automatically called this in AddRepository method. 142 return fmt.Errorf("RecalculateAccesses: %w", err) 143 } 144 145 if setting.Service.AutoWatchNewRepos { 146 if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil { 147 return fmt.Errorf("WatchRepo: %w", err) 148 } 149 } 150 151 if err = webhook.CopyDefaultWebhooksToRepo(ctx, repo.ID); err != nil { 152 return fmt.Errorf("CopyDefaultWebhooksToRepo: %w", err) 153 } 154 155 return nil 156 } 157 158 // CreateRepoOptions contains the create repository options 159 type CreateRepoOptions struct { 160 Name string 161 Description string 162 OriginalURL string 163 GitServiceType api.GitServiceType 164 Gitignores string 165 IssueLabels string 166 License string 167 Readme string 168 DefaultBranch string 169 IsPrivate bool 170 IsMirror bool 171 IsTemplate bool 172 AutoInit bool 173 Status repo_model.RepositoryStatus 174 TrustModel repo_model.TrustModelType 175 MirrorInterval string 176 } 177 178 // CreateRepository creates a repository for the user/organization. 179 func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { 180 if !doer.IsAdmin && !u.CanCreateRepo() { 181 return nil, repo_model.ErrReachLimitOfRepo{ 182 Limit: u.MaxRepoCreation, 183 } 184 } 185 186 if len(opts.DefaultBranch) == 0 { 187 opts.DefaultBranch = setting.Repository.DefaultBranch 188 } 189 190 // Check if label template exist 191 if len(opts.IssueLabels) > 0 { 192 if _, err := LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil { 193 return nil, err 194 } 195 } 196 197 repo := &repo_model.Repository{ 198 OwnerID: u.ID, 199 Owner: u, 200 OwnerName: u.Name, 201 Name: opts.Name, 202 LowerName: strings.ToLower(opts.Name), 203 Description: opts.Description, 204 OriginalURL: opts.OriginalURL, 205 OriginalServiceType: opts.GitServiceType, 206 IsPrivate: opts.IsPrivate, 207 IsFsckEnabled: !opts.IsMirror, 208 IsTemplate: opts.IsTemplate, 209 CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, 210 Status: opts.Status, 211 IsEmpty: !opts.AutoInit, 212 TrustModel: opts.TrustModel, 213 IsMirror: opts.IsMirror, 214 DefaultBranch: opts.DefaultBranch, 215 } 216 217 var rollbackRepo *repo_model.Repository 218 219 if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { 220 if err := CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { 221 return err 222 } 223 224 // No need for init mirror. 225 if opts.IsMirror { 226 return nil 227 } 228 229 repoPath := repo_model.RepoPath(u.Name, repo.Name) 230 isExist, err := util.IsExist(repoPath) 231 if err != nil { 232 log.Error("Unable to check if %s exists. Error: %v", repoPath, err) 233 return err 234 } 235 if isExist { 236 // repo already exists - We have two or three options. 237 // 1. We fail stating that the directory exists 238 // 2. We create the db repository to go with this data and adopt the git repo 239 // 3. We delete it and start afresh 240 // 241 // Previously Gitea would just delete and start afresh - this was naughty. 242 // So we will now fail and delegate to other functionality to adopt or delete 243 log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) 244 return repo_model.ErrRepoFilesAlreadyExist{ 245 Uname: u.Name, 246 Name: repo.Name, 247 } 248 } 249 250 if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil { 251 if err2 := util.RemoveAll(repoPath); err2 != nil { 252 log.Error("initRepository: %v", err) 253 return fmt.Errorf( 254 "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) 255 } 256 return fmt.Errorf("initRepository: %w", err) 257 } 258 259 // Initialize Issue Labels if selected 260 if len(opts.IssueLabels) > 0 { 261 if err = InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { 262 rollbackRepo = repo 263 rollbackRepo.OwnerID = u.ID 264 return fmt.Errorf("InitializeLabels: %w", err) 265 } 266 } 267 268 if err := CheckDaemonExportOK(ctx, repo); err != nil { 269 return fmt.Errorf("checkDaemonExportOK: %w", err) 270 } 271 272 if stdout, _, err := git.NewCommand(ctx, "update-server-info"). 273 SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). 274 RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { 275 log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) 276 rollbackRepo = repo 277 rollbackRepo.OwnerID = u.ID 278 return fmt.Errorf("CreateRepository(git update-server-info): %w", err) 279 } 280 return nil 281 }); err != nil { 282 if rollbackRepo != nil { 283 if errDelete := models.DeleteRepository(doer, rollbackRepo.OwnerID, rollbackRepo.ID); errDelete != nil { 284 log.Error("Rollback deleteRepository: %v", errDelete) 285 } 286 } 287 288 return nil, err 289 } 290 291 return repo, nil 292 } 293 294 const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular 295 296 // getDirectorySize returns the disk consumption for a given path 297 func getDirectorySize(path string) (int64, error) { 298 var size int64 299 err := filepath.WalkDir(path, func(_ string, info os.DirEntry, err error) error { 300 if err != nil { 301 if os.IsNotExist(err) { // ignore the error because the file maybe deleted during traversing. 302 return nil 303 } 304 return err 305 } 306 if info.IsDir() { 307 return nil 308 } 309 f, err := info.Info() 310 if err != nil { 311 return err 312 } 313 if (f.Mode() & notRegularFileMode) == 0 { 314 size += f.Size() 315 } 316 return err 317 }) 318 return size, err 319 } 320 321 // UpdateRepoSize updates the repository size, calculating it using getDirectorySize 322 func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error { 323 size, err := getDirectorySize(repo.RepoPath()) 324 if err != nil { 325 return fmt.Errorf("updateSize: %w", err) 326 } 327 328 lfsSize, err := git_model.GetRepoLFSSize(ctx, repo.ID) 329 if err != nil { 330 return fmt.Errorf("updateSize: GetLFSMetaObjects: %w", err) 331 } 332 333 return repo_model.UpdateRepoSize(ctx, repo.ID, size+lfsSize) 334 } 335 336 // CheckDaemonExportOK creates/removes git-daemon-export-ok for git-daemon... 337 func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error { 338 if err := repo.LoadOwner(ctx); err != nil { 339 return err 340 } 341 342 // Create/Remove git-daemon-export-ok for git-daemon... 343 daemonExportFile := path.Join(repo.RepoPath(), `git-daemon-export-ok`) 344 345 isExist, err := util.IsExist(daemonExportFile) 346 if err != nil { 347 log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err) 348 return err 349 } 350 351 isPublic := !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePublic 352 if !isPublic && isExist { 353 if err = util.Remove(daemonExportFile); err != nil { 354 log.Error("Failed to remove %s: %v", daemonExportFile, err) 355 } 356 } else if isPublic && !isExist { 357 if f, err := os.Create(daemonExportFile); err != nil { 358 log.Error("Failed to create %s: %v", daemonExportFile, err) 359 } else { 360 f.Close() 361 } 362 } 363 364 return nil 365 } 366 367 // UpdateRepository updates a repository with db context 368 func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) { 369 repo.LowerName = strings.ToLower(repo.Name) 370 371 e := db.GetEngine(ctx) 372 373 if _, err = e.ID(repo.ID).AllCols().Update(repo); err != nil { 374 return fmt.Errorf("update: %w", err) 375 } 376 377 if err = UpdateRepoSize(ctx, repo); err != nil { 378 log.Error("Failed to update size for repository: %v", err) 379 } 380 381 if visibilityChanged { 382 if err = repo.LoadOwner(ctx); err != nil { 383 return fmt.Errorf("LoadOwner: %w", err) 384 } 385 if repo.Owner.IsOrganization() { 386 // Organization repository need to recalculate access table when visibility is changed. 387 if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { 388 return fmt.Errorf("recalculateTeamAccesses: %w", err) 389 } 390 } 391 392 // If repo has become private, we need to set its actions to private. 393 if repo.IsPrivate { 394 _, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&activities_model.Action{ 395 IsPrivate: true, 396 }) 397 if err != nil { 398 return err 399 } 400 } 401 402 // Create/Remove git-daemon-export-ok for git-daemon... 403 if err := CheckDaemonExportOK(ctx, repo); err != nil { 404 return err 405 } 406 407 forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID) 408 if err != nil { 409 return fmt.Errorf("getRepositoriesByForkID: %w", err) 410 } 411 for i := range forkRepos { 412 forkRepos[i].IsPrivate = repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate 413 if err = UpdateRepository(ctx, forkRepos[i], true); err != nil { 414 return fmt.Errorf("updateRepository[%d]: %w", forkRepos[i].ID, err) 415 } 416 } 417 } 418 419 return nil 420 }