code.gitea.io/gitea@v1.22.3/services/repository/create.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repository 5 6 import ( 7 "bytes" 8 "context" 9 "fmt" 10 "os" 11 "path/filepath" 12 "strings" 13 "time" 14 15 "code.gitea.io/gitea/models/db" 16 repo_model "code.gitea.io/gitea/models/repo" 17 user_model "code.gitea.io/gitea/models/user" 18 "code.gitea.io/gitea/modules/git" 19 "code.gitea.io/gitea/modules/gitrepo" 20 "code.gitea.io/gitea/modules/log" 21 "code.gitea.io/gitea/modules/options" 22 repo_module "code.gitea.io/gitea/modules/repository" 23 "code.gitea.io/gitea/modules/setting" 24 api "code.gitea.io/gitea/modules/structs" 25 "code.gitea.io/gitea/modules/templates/vars" 26 "code.gitea.io/gitea/modules/util" 27 ) 28 29 // CreateRepoOptions contains the create repository options 30 type CreateRepoOptions struct { 31 Name string 32 Description string 33 OriginalURL string 34 GitServiceType api.GitServiceType 35 Gitignores string 36 IssueLabels string 37 License string 38 Readme string 39 DefaultBranch string 40 IsPrivate bool 41 IsMirror bool 42 IsTemplate bool 43 AutoInit bool 44 Status repo_model.RepositoryStatus 45 TrustModel repo_model.TrustModelType 46 MirrorInterval string 47 ObjectFormatName string 48 } 49 50 func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error { 51 commitTimeStr := time.Now().Format(time.RFC3339) 52 authorSig := repo.Owner.NewGitSig() 53 54 // Because this may call hooks we should pass in the environment 55 env := append(os.Environ(), 56 "GIT_AUTHOR_NAME="+authorSig.Name, 57 "GIT_AUTHOR_EMAIL="+authorSig.Email, 58 "GIT_AUTHOR_DATE="+commitTimeStr, 59 "GIT_COMMITTER_NAME="+authorSig.Name, 60 "GIT_COMMITTER_EMAIL="+authorSig.Email, 61 "GIT_COMMITTER_DATE="+commitTimeStr, 62 ) 63 64 // Clone to temporary path and do the init commit. 65 if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir). 66 SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)). 67 RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil { 68 log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err) 69 return fmt.Errorf("git clone: %w", err) 70 } 71 72 // README 73 data, err := options.Readme(opts.Readme) 74 if err != nil { 75 return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) 76 } 77 78 cloneLink := repo.CloneLink() 79 match := map[string]string{ 80 "Name": repo.Name, 81 "Description": repo.Description, 82 "CloneURL.SSH": cloneLink.SSH, 83 "CloneURL.HTTPS": cloneLink.HTTPS, 84 "OwnerName": repo.OwnerName, 85 } 86 res, err := vars.Expand(string(data), match) 87 if err != nil { 88 // here we could just log the error and continue the rendering 89 log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err) 90 } 91 if err = os.WriteFile(filepath.Join(tmpDir, "README.md"), 92 []byte(res), 0o644); err != nil { 93 return fmt.Errorf("write README.md: %w", err) 94 } 95 96 // .gitignore 97 if len(opts.Gitignores) > 0 { 98 var buf bytes.Buffer 99 names := strings.Split(opts.Gitignores, ",") 100 for _, name := range names { 101 data, err = options.Gitignore(name) 102 if err != nil { 103 return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) 104 } 105 buf.WriteString("# ---> " + name + "\n") 106 buf.Write(data) 107 buf.WriteString("\n") 108 } 109 110 if buf.Len() > 0 { 111 if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil { 112 return fmt.Errorf("write .gitignore: %w", err) 113 } 114 } 115 } 116 117 // LICENSE 118 if len(opts.License) > 0 { 119 data, err = repo_module.GetLicense(opts.License, &repo_module.LicenseValues{ 120 Owner: repo.OwnerName, 121 Email: authorSig.Email, 122 Repo: repo.Name, 123 Year: time.Now().Format("2006"), 124 }) 125 if err != nil { 126 return fmt.Errorf("getLicense[%s]: %w", opts.License, err) 127 } 128 129 if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil { 130 return fmt.Errorf("write LICENSE: %w", err) 131 } 132 } 133 134 return nil 135 } 136 137 // InitRepository initializes README and .gitignore if needed. 138 func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) { 139 if err = repo_module.CheckInitRepository(ctx, repo.OwnerName, repo.Name, opts.ObjectFormatName); err != nil { 140 return err 141 } 142 143 // Initialize repository according to user's choice. 144 if opts.AutoInit { 145 tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name) 146 if err != nil { 147 return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err) 148 } 149 defer func() { 150 if err := util.RemoveAll(tmpDir); err != nil { 151 log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err) 152 } 153 }() 154 155 if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil { 156 return fmt.Errorf("prepareRepoCommit: %w", err) 157 } 158 159 // Apply changes and commit. 160 if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil { 161 return fmt.Errorf("initRepoCommit: %w", err) 162 } 163 } 164 165 // Re-fetch the repository from database before updating it (else it would 166 // override changes that were done earlier with sql) 167 if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { 168 return fmt.Errorf("getRepositoryByID: %w", err) 169 } 170 171 if !opts.AutoInit { 172 repo.IsEmpty = true 173 } 174 175 repo.DefaultBranch = setting.Repository.DefaultBranch 176 repo.DefaultWikiBranch = setting.Repository.DefaultBranch 177 178 if len(opts.DefaultBranch) > 0 { 179 repo.DefaultBranch = opts.DefaultBranch 180 if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { 181 return fmt.Errorf("setDefaultBranch: %w", err) 182 } 183 184 if !repo.IsEmpty { 185 if _, err := repo_module.SyncRepoBranches(ctx, repo.ID, u.ID); err != nil { 186 return fmt.Errorf("SyncRepoBranches: %w", err) 187 } 188 } 189 } 190 191 if err = UpdateRepository(ctx, repo, false); err != nil { 192 return fmt.Errorf("updateRepository: %w", err) 193 } 194 195 return nil 196 } 197 198 // CreateRepositoryDirectly creates a repository for the user/organization. 199 func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { 200 if !doer.IsAdmin && !u.CanCreateRepo() { 201 return nil, repo_model.ErrReachLimitOfRepo{ 202 Limit: u.MaxRepoCreation, 203 } 204 } 205 206 if len(opts.DefaultBranch) == 0 { 207 opts.DefaultBranch = setting.Repository.DefaultBranch 208 } 209 210 // Check if label template exist 211 if len(opts.IssueLabels) > 0 { 212 if _, err := repo_module.LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil { 213 return nil, err 214 } 215 } 216 217 if opts.ObjectFormatName == "" { 218 opts.ObjectFormatName = git.Sha1ObjectFormat.Name() 219 } 220 221 repo := &repo_model.Repository{ 222 OwnerID: u.ID, 223 Owner: u, 224 OwnerName: u.Name, 225 Name: opts.Name, 226 LowerName: strings.ToLower(opts.Name), 227 Description: opts.Description, 228 OriginalURL: opts.OriginalURL, 229 OriginalServiceType: opts.GitServiceType, 230 IsPrivate: opts.IsPrivate, 231 IsFsckEnabled: !opts.IsMirror, 232 IsTemplate: opts.IsTemplate, 233 CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, 234 Status: opts.Status, 235 IsEmpty: !opts.AutoInit, 236 TrustModel: opts.TrustModel, 237 IsMirror: opts.IsMirror, 238 DefaultBranch: opts.DefaultBranch, 239 DefaultWikiBranch: setting.Repository.DefaultBranch, 240 ObjectFormatName: opts.ObjectFormatName, 241 } 242 243 var rollbackRepo *repo_model.Repository 244 245 if err := db.WithTx(ctx, func(ctx context.Context) error { 246 if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { 247 return err 248 } 249 250 // No need for init mirror. 251 if opts.IsMirror { 252 return nil 253 } 254 255 repoPath := repo_model.RepoPath(u.Name, repo.Name) 256 isExist, err := util.IsExist(repoPath) 257 if err != nil { 258 log.Error("Unable to check if %s exists. Error: %v", repoPath, err) 259 return err 260 } 261 if isExist { 262 // repo already exists - We have two or three options. 263 // 1. We fail stating that the directory exists 264 // 2. We create the db repository to go with this data and adopt the git repo 265 // 3. We delete it and start afresh 266 // 267 // Previously Gitea would just delete and start afresh - this was naughty. 268 // So we will now fail and delegate to other functionality to adopt or delete 269 log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) 270 return repo_model.ErrRepoFilesAlreadyExist{ 271 Uname: u.Name, 272 Name: repo.Name, 273 } 274 } 275 276 if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil { 277 if err2 := util.RemoveAll(repoPath); err2 != nil { 278 log.Error("initRepository: %v", err) 279 return fmt.Errorf( 280 "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) 281 } 282 return fmt.Errorf("initRepository: %w", err) 283 } 284 285 // Initialize Issue Labels if selected 286 if len(opts.IssueLabels) > 0 { 287 if err = repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { 288 rollbackRepo = repo 289 rollbackRepo.OwnerID = u.ID 290 return fmt.Errorf("InitializeLabels: %w", err) 291 } 292 } 293 294 if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil { 295 return fmt.Errorf("checkDaemonExportOK: %w", err) 296 } 297 298 if stdout, _, err := git.NewCommand(ctx, "update-server-info"). 299 SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). 300 RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { 301 log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) 302 rollbackRepo = repo 303 rollbackRepo.OwnerID = u.ID 304 return fmt.Errorf("CreateRepository(git update-server-info): %w", err) 305 } 306 return nil 307 }); err != nil { 308 if rollbackRepo != nil { 309 if errDelete := DeleteRepositoryDirectly(ctx, doer, rollbackRepo.ID); errDelete != nil { 310 log.Error("Rollback deleteRepository: %v", errDelete) 311 } 312 } 313 314 return nil, err 315 } 316 317 return repo, nil 318 }