code.gitea.io/gitea@v1.19.3/modules/repository/init.go (about) 1 // Copyright 2019 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 "sort" 13 "strings" 14 "time" 15 16 issues_model "code.gitea.io/gitea/models/issues" 17 repo_model "code.gitea.io/gitea/models/repo" 18 user_model "code.gitea.io/gitea/models/user" 19 "code.gitea.io/gitea/modules/git" 20 "code.gitea.io/gitea/modules/label" 21 "code.gitea.io/gitea/modules/log" 22 "code.gitea.io/gitea/modules/options" 23 "code.gitea.io/gitea/modules/setting" 24 "code.gitea.io/gitea/modules/templates/vars" 25 "code.gitea.io/gitea/modules/util" 26 asymkey_service "code.gitea.io/gitea/services/asymkey" 27 ) 28 29 type OptionFile struct { 30 DisplayName string 31 Description string 32 } 33 34 var ( 35 // Gitignores contains the gitiginore files 36 Gitignores []string 37 38 // Licenses contains the license files 39 Licenses []string 40 41 // Readmes contains the readme files 42 Readmes []string 43 44 // LabelTemplateFiles contains the label template files, each item has its DisplayName and Description 45 LabelTemplateFiles []OptionFile 46 labelTemplateFileMap = map[string]string{} // DisplayName => FileName mapping 47 ) 48 49 type optionFileList struct { 50 all []string // all files provided by bindata & custom-path. Sorted. 51 custom []string // custom files provided by custom-path. Non-sorted, internal use only. 52 } 53 54 // mergeCustomLabelFiles merges the custom label files. Always use the file's main name (DisplayName) as the key to de-duplicate. 55 func mergeCustomLabelFiles(fl optionFileList) []string { 56 exts := map[string]int{"": 0, ".yml": 1, ".yaml": 2} // "yaml" file has the highest priority to be used. 57 58 m := map[string]string{} 59 merge := func(list []string) { 60 sort.Slice(list, func(i, j int) bool { return exts[filepath.Ext(list[i])] < exts[filepath.Ext(list[j])] }) 61 for _, f := range list { 62 m[strings.TrimSuffix(f, filepath.Ext(f))] = f 63 } 64 } 65 merge(fl.all) 66 merge(fl.custom) 67 68 files := make([]string, 0, len(m)) 69 for _, f := range m { 70 files = append(files, f) 71 } 72 sort.Strings(files) 73 return files 74 } 75 76 // LoadRepoConfig loads the repository config 77 func LoadRepoConfig() error { 78 types := []string{"gitignore", "license", "readme", "label"} // option file directories 79 typeFiles := make([]optionFileList, len(types)) 80 for i, t := range types { 81 var err error 82 if typeFiles[i].all, err = options.Dir(t); err != nil { 83 return fmt.Errorf("failed to list %s files: %w", t, err) 84 } 85 sort.Strings(typeFiles[i].all) 86 customPath := filepath.Join(setting.CustomPath, "options", t) 87 if isDir, err := util.IsDir(customPath); err != nil { 88 return fmt.Errorf("failed to check custom %s dir: %w", t, err) 89 } else if isDir { 90 if typeFiles[i].custom, err = util.StatDir(customPath); err != nil { 91 return fmt.Errorf("failed to list custom %s files: %w", t, err) 92 } 93 } 94 } 95 96 Gitignores = typeFiles[0].all 97 Licenses = typeFiles[1].all 98 Readmes = typeFiles[2].all 99 100 // Load label templates 101 LabelTemplateFiles = nil 102 labelTemplateFileMap = map[string]string{} 103 for _, file := range mergeCustomLabelFiles(typeFiles[3]) { 104 description, err := label.LoadTemplateDescription(file) 105 if err != nil { 106 return fmt.Errorf("failed to load labels: %w", err) 107 } 108 displayName := strings.TrimSuffix(file, filepath.Ext(file)) 109 labelTemplateFileMap[displayName] = file 110 LabelTemplateFiles = append(LabelTemplateFiles, OptionFile{DisplayName: displayName, Description: description}) 111 } 112 113 // Filter out invalid names and promote preferred licenses. 114 sortedLicenses := make([]string, 0, len(Licenses)) 115 for _, name := range setting.Repository.PreferredLicenses { 116 if util.SliceContainsString(Licenses, name, true) { 117 sortedLicenses = append(sortedLicenses, name) 118 } 119 } 120 for _, name := range Licenses { 121 if !util.SliceContainsString(setting.Repository.PreferredLicenses, name, true) { 122 sortedLicenses = append(sortedLicenses, name) 123 } 124 } 125 Licenses = sortedLicenses 126 return nil 127 } 128 129 func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error { 130 commitTimeStr := time.Now().Format(time.RFC3339) 131 authorSig := repo.Owner.NewGitSig() 132 133 // Because this may call hooks we should pass in the environment 134 env := append(os.Environ(), 135 "GIT_AUTHOR_NAME="+authorSig.Name, 136 "GIT_AUTHOR_EMAIL="+authorSig.Email, 137 "GIT_AUTHOR_DATE="+commitTimeStr, 138 "GIT_COMMITTER_NAME="+authorSig.Name, 139 "GIT_COMMITTER_EMAIL="+authorSig.Email, 140 "GIT_COMMITTER_DATE="+commitTimeStr, 141 ) 142 143 // Clone to temporary path and do the init commit. 144 if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir). 145 SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)). 146 RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil { 147 log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err) 148 return fmt.Errorf("git clone: %w", err) 149 } 150 151 // README 152 data, err := options.GetRepoInitFile("readme", opts.Readme) 153 if err != nil { 154 return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) 155 } 156 157 cloneLink := repo.CloneLink() 158 match := map[string]string{ 159 "Name": repo.Name, 160 "Description": repo.Description, 161 "CloneURL.SSH": cloneLink.SSH, 162 "CloneURL.HTTPS": cloneLink.HTTPS, 163 "OwnerName": repo.OwnerName, 164 } 165 res, err := vars.Expand(string(data), match) 166 if err != nil { 167 // here we could just log the error and continue the rendering 168 log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err) 169 } 170 if err = os.WriteFile(filepath.Join(tmpDir, "README.md"), 171 []byte(res), 0o644); err != nil { 172 return fmt.Errorf("write README.md: %w", err) 173 } 174 175 // .gitignore 176 if len(opts.Gitignores) > 0 { 177 var buf bytes.Buffer 178 names := strings.Split(opts.Gitignores, ",") 179 for _, name := range names { 180 data, err = options.GetRepoInitFile("gitignore", name) 181 if err != nil { 182 return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) 183 } 184 buf.WriteString("# ---> " + name + "\n") 185 buf.Write(data) 186 buf.WriteString("\n") 187 } 188 189 if buf.Len() > 0 { 190 if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil { 191 return fmt.Errorf("write .gitignore: %w", err) 192 } 193 } 194 } 195 196 // LICENSE 197 if len(opts.License) > 0 { 198 data, err = options.GetRepoInitFile("license", opts.License) 199 if err != nil { 200 return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.License, err) 201 } 202 203 if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil { 204 return fmt.Errorf("write LICENSE: %w", err) 205 } 206 } 207 208 return nil 209 } 210 211 // initRepoCommit temporarily changes with work directory. 212 func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) { 213 commitTimeStr := time.Now().Format(time.RFC3339) 214 215 sig := u.NewGitSig() 216 // Because this may call hooks we should pass in the environment 217 env := append(os.Environ(), 218 "GIT_AUTHOR_NAME="+sig.Name, 219 "GIT_AUTHOR_EMAIL="+sig.Email, 220 "GIT_AUTHOR_DATE="+commitTimeStr, 221 "GIT_COMMITTER_DATE="+commitTimeStr, 222 ) 223 committerName := sig.Name 224 committerEmail := sig.Email 225 226 if stdout, _, err := git.NewCommand(ctx, "add", "--all"). 227 SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)). 228 RunStdString(&git.RunOpts{Dir: tmpPath}); err != nil { 229 log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err) 230 return fmt.Errorf("git add --all: %w", err) 231 } 232 233 cmd := git.NewCommand(ctx, "commit", "--message=Initial commit"). 234 AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email) 235 236 sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u) 237 if sign { 238 cmd.AddOptionFormat("-S%s", keyID) 239 240 if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { 241 // need to set the committer to the KeyID owner 242 committerName = signer.Name 243 committerEmail = signer.Email 244 } 245 } else { 246 cmd.AddArguments("--no-gpg-sign") 247 } 248 249 env = append(env, 250 "GIT_COMMITTER_NAME="+committerName, 251 "GIT_COMMITTER_EMAIL="+committerEmail, 252 ) 253 254 if stdout, _, err := cmd. 255 SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)). 256 RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil { 257 log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err) 258 return fmt.Errorf("git commit: %w", err) 259 } 260 261 if len(defaultBranch) == 0 { 262 defaultBranch = setting.Repository.DefaultBranch 263 } 264 265 if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch). 266 SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)). 267 RunStdString(&git.RunOpts{Dir: tmpPath, Env: InternalPushingEnvironment(u, repo)}); err != nil { 268 log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err) 269 return fmt.Errorf("git push: %w", err) 270 } 271 272 return nil 273 } 274 275 func checkInitRepository(ctx context.Context, owner, name string) (err error) { 276 // Somehow the directory could exist. 277 repoPath := repo_model.RepoPath(owner, name) 278 isExist, err := util.IsExist(repoPath) 279 if err != nil { 280 log.Error("Unable to check if %s exists. Error: %v", repoPath, err) 281 return err 282 } 283 if isExist { 284 return repo_model.ErrRepoFilesAlreadyExist{ 285 Uname: owner, 286 Name: name, 287 } 288 } 289 290 // Init git bare new repository. 291 if err = git.InitRepository(ctx, repoPath, true); err != nil { 292 return fmt.Errorf("git.InitRepository: %w", err) 293 } else if err = createDelegateHooks(repoPath); err != nil { 294 return fmt.Errorf("createDelegateHooks: %w", err) 295 } 296 return nil 297 } 298 299 // InitRepository initializes README and .gitignore if needed. 300 func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) { 301 if err = checkInitRepository(ctx, repo.OwnerName, repo.Name); err != nil { 302 return err 303 } 304 305 // Initialize repository according to user's choice. 306 if opts.AutoInit { 307 tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name) 308 if err != nil { 309 return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err) 310 } 311 defer func() { 312 if err := util.RemoveAll(tmpDir); err != nil { 313 log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err) 314 } 315 }() 316 317 if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil { 318 return fmt.Errorf("prepareRepoCommit: %w", err) 319 } 320 321 // Apply changes and commit. 322 if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil { 323 return fmt.Errorf("initRepoCommit: %w", err) 324 } 325 } 326 327 // Re-fetch the repository from database before updating it (else it would 328 // override changes that were done earlier with sql) 329 if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { 330 return fmt.Errorf("getRepositoryByID: %w", err) 331 } 332 333 if !opts.AutoInit { 334 repo.IsEmpty = true 335 } 336 337 repo.DefaultBranch = setting.Repository.DefaultBranch 338 339 if len(opts.DefaultBranch) > 0 { 340 repo.DefaultBranch = opts.DefaultBranch 341 gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) 342 if err != nil { 343 return fmt.Errorf("openRepository: %w", err) 344 } 345 defer gitRepo.Close() 346 if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { 347 return fmt.Errorf("setDefaultBranch: %w", err) 348 } 349 } 350 351 if err = UpdateRepository(ctx, repo, false); err != nil { 352 return fmt.Errorf("updateRepository: %w", err) 353 } 354 355 return nil 356 } 357 358 // InitializeLabels adds a label set to a repository using a template 359 func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error { 360 list, err := LoadTemplateLabelsByDisplayName(labelTemplate) 361 if err != nil { 362 return err 363 } 364 365 labels := make([]*issues_model.Label, len(list)) 366 for i := 0; i < len(list); i++ { 367 labels[i] = &issues_model.Label{ 368 Name: list[i].Name, 369 Exclusive: list[i].Exclusive, 370 Description: list[i].Description, 371 Color: list[i].Color, 372 } 373 if isOrg { 374 labels[i].OrgID = id 375 } else { 376 labels[i].RepoID = id 377 } 378 } 379 for _, label := range labels { 380 if err = issues_model.NewLabel(ctx, label); err != nil { 381 return err 382 } 383 } 384 return nil 385 } 386 387 // LoadTemplateLabelsByDisplayName loads a label template by its display name 388 func LoadTemplateLabelsByDisplayName(displayName string) ([]*label.Label, error) { 389 if fileName, ok := labelTemplateFileMap[displayName]; ok { 390 return label.LoadTemplateFile(fileName) 391 } 392 return nil, label.ErrTemplateLoad{TemplateFile: displayName, OriginalError: fmt.Errorf("label template %q not found", displayName)} 393 }