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