code.gitea.io/gitea@v1.22.3/services/wiki/wiki.go (about) 1 // Copyright 2015 The Gogs Authors. All rights reserved. 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package wiki 6 7 import ( 8 "context" 9 "errors" 10 "fmt" 11 "os" 12 "strings" 13 14 "code.gitea.io/gitea/models/db" 15 repo_model "code.gitea.io/gitea/models/repo" 16 system_model "code.gitea.io/gitea/models/system" 17 "code.gitea.io/gitea/models/unit" 18 user_model "code.gitea.io/gitea/models/user" 19 "code.gitea.io/gitea/modules/git" 20 "code.gitea.io/gitea/modules/gitrepo" 21 "code.gitea.io/gitea/modules/log" 22 repo_module "code.gitea.io/gitea/modules/repository" 23 "code.gitea.io/gitea/modules/sync" 24 "code.gitea.io/gitea/modules/util" 25 asymkey_service "code.gitea.io/gitea/services/asymkey" 26 repo_service "code.gitea.io/gitea/services/repository" 27 ) 28 29 // TODO: use clustered lock (unique queue? or *abuse* cache) 30 var wikiWorkingPool = sync.NewExclusivePool() 31 32 const DefaultRemote = "origin" 33 34 // InitWiki initializes a wiki for repository, 35 // it does nothing when repository already has wiki. 36 func InitWiki(ctx context.Context, repo *repo_model.Repository) error { 37 if repo.HasWiki() { 38 return nil 39 } 40 41 if err := git.InitRepository(ctx, repo.WikiPath(), true, repo.ObjectFormatName); err != nil { 42 return fmt.Errorf("InitRepository: %w", err) 43 } else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil { 44 return fmt.Errorf("createDelegateHooks: %w", err) 45 } else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix + repo.DefaultWikiBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil { 46 return fmt.Errorf("unable to set default wiki branch to %q: %w", repo.DefaultWikiBranch, err) 47 } 48 return nil 49 } 50 51 // prepareGitPath try to find a suitable file path with file name by the given raw wiki name. 52 // return: existence, prepared file path with name, error 53 func prepareGitPath(gitRepo *git.Repository, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) { 54 unescaped := string(wikiPath) + ".md" 55 gitPath := WebPathToGitPath(wikiPath) 56 57 // Look for both files 58 filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath) 59 if err != nil { 60 if strings.Contains(err.Error(), "Not a valid object name") { 61 return false, gitPath, nil // branch doesn't exist 62 } 63 log.Error("Wiki LsTree failed, err: %v", err) 64 return false, gitPath, err 65 } 66 67 foundEscaped := false 68 for _, filename := range filesInIndex { 69 switch filename { 70 case unescaped: 71 // if we find the unescaped file return it 72 return true, unescaped, nil 73 case gitPath: 74 foundEscaped = true 75 } 76 } 77 78 // If not return whether the escaped file exists, and the escaped filename to keep backwards compatibility. 79 return foundEscaped, gitPath, nil 80 } 81 82 // updateWikiPage adds a new page or edits an existing page in repository wiki. 83 func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName WebPath, content, message string, isNew bool) (err error) { 84 err = repo.MustNotBeArchived() 85 if err != nil { 86 return err 87 } 88 89 if err = validateWebPath(newWikiName); err != nil { 90 return err 91 } 92 wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID)) 93 defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID)) 94 95 if err = InitWiki(ctx, repo); err != nil { 96 return fmt.Errorf("InitWiki: %w", err) 97 } 98 99 hasDefaultBranch := git.IsBranchExist(ctx, repo.WikiPath(), repo.DefaultWikiBranch) 100 101 basePath, err := repo_module.CreateTemporaryPath("update-wiki") 102 if err != nil { 103 return err 104 } 105 defer func() { 106 if err := repo_module.RemoveTemporaryPath(basePath); err != nil { 107 log.Error("Merge: RemoveTemporaryPath: %s", err) 108 } 109 }() 110 111 cloneOpts := git.CloneRepoOptions{ 112 Bare: true, 113 Shared: true, 114 } 115 116 if hasDefaultBranch { 117 cloneOpts.Branch = repo.DefaultWikiBranch 118 } 119 120 if err := git.Clone(ctx, repo.WikiPath(), basePath, cloneOpts); err != nil { 121 log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err) 122 return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err) 123 } 124 125 gitRepo, err := git.OpenRepository(ctx, basePath) 126 if err != nil { 127 log.Error("Unable to open temporary repository: %s (%v)", basePath, err) 128 return fmt.Errorf("failed to open new temporary repository in: %s %w", basePath, err) 129 } 130 defer gitRepo.Close() 131 132 if hasDefaultBranch { 133 if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil { 134 log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err) 135 return fmt.Errorf("fnable to read HEAD tree to index in: %s %w", basePath, err) 136 } 137 } 138 139 isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, newWikiName) 140 if err != nil { 141 return err 142 } 143 144 if isNew { 145 if isWikiExist { 146 return repo_model.ErrWikiAlreadyExist{ 147 Title: newWikiPath, 148 } 149 } 150 } else { 151 // avoid check existence again if wiki name is not changed since gitRepo.LsFiles(...) is not free. 152 isOldWikiExist := true 153 oldWikiPath := newWikiPath 154 if oldWikiName != newWikiName { 155 isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, repo.DefaultWikiBranch, oldWikiName) 156 if err != nil { 157 return err 158 } 159 } 160 161 if isOldWikiExist { 162 err := gitRepo.RemoveFilesFromIndex(oldWikiPath) 163 if err != nil { 164 log.Error("RemoveFilesFromIndex failed: %v", err) 165 return err 166 } 167 } 168 } 169 170 // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here 171 172 objectHash, err := gitRepo.HashObject(strings.NewReader(content)) 173 if err != nil { 174 log.Error("HashObject failed: %v", err) 175 return err 176 } 177 178 if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil { 179 log.Error("AddObjectToIndex failed: %v", err) 180 return err 181 } 182 183 tree, err := gitRepo.WriteTree() 184 if err != nil { 185 log.Error("WriteTree failed: %v", err) 186 return err 187 } 188 189 commitTreeOpts := git.CommitTreeOpts{ 190 Message: message, 191 } 192 193 committer := doer.NewGitSig() 194 195 sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) 196 if sign { 197 commitTreeOpts.KeyID = signingKey 198 if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { 199 committer = signer 200 } 201 } else { 202 commitTreeOpts.NoGPGSign = true 203 } 204 if hasDefaultBranch { 205 commitTreeOpts.Parents = []string{"HEAD"} 206 } 207 208 commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) 209 if err != nil { 210 log.Error("CommitTree failed: %v", err) 211 return err 212 } 213 214 if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{ 215 Remote: DefaultRemote, 216 Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch), 217 Env: repo_module.FullPushingEnvironment( 218 doer, 219 doer, 220 repo, 221 repo.Name+".wiki", 222 0, 223 ), 224 }); err != nil { 225 log.Error("Push failed: %v", err) 226 if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { 227 return err 228 } 229 return fmt.Errorf("failed to push: %w", err) 230 } 231 232 return nil 233 } 234 235 // AddWikiPage adds a new wiki page with a given wikiPath. 236 func AddWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName WebPath, content, message string) error { 237 return updateWikiPage(ctx, doer, repo, "", wikiName, content, message, true) 238 } 239 240 // EditWikiPage updates a wiki page identified by its wikiPath, 241 // optionally also changing wikiPath. 242 func EditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName WebPath, content, message string) error { 243 return updateWikiPage(ctx, doer, repo, oldWikiName, newWikiName, content, message, false) 244 } 245 246 // DeleteWikiPage deletes a wiki page identified by its path. 247 func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName WebPath) (err error) { 248 err = repo.MustNotBeArchived() 249 if err != nil { 250 return err 251 } 252 253 wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID)) 254 defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID)) 255 256 if err = InitWiki(ctx, repo); err != nil { 257 return fmt.Errorf("InitWiki: %w", err) 258 } 259 260 basePath, err := repo_module.CreateTemporaryPath("update-wiki") 261 if err != nil { 262 return err 263 } 264 defer func() { 265 if err := repo_module.RemoveTemporaryPath(basePath); err != nil { 266 log.Error("Merge: RemoveTemporaryPath: %s", err) 267 } 268 }() 269 270 if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{ 271 Bare: true, 272 Shared: true, 273 Branch: repo.DefaultWikiBranch, 274 }); err != nil { 275 log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err) 276 return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err) 277 } 278 279 gitRepo, err := git.OpenRepository(ctx, basePath) 280 if err != nil { 281 log.Error("Unable to open temporary repository: %s (%v)", basePath, err) 282 return fmt.Errorf("failed to open new temporary repository in: %s %w", basePath, err) 283 } 284 defer gitRepo.Close() 285 286 if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil { 287 log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err) 288 return fmt.Errorf("unable to read HEAD tree to index in: %s %w", basePath, err) 289 } 290 291 found, wikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, wikiName) 292 if err != nil { 293 return err 294 } 295 if found { 296 err := gitRepo.RemoveFilesFromIndex(wikiPath) 297 if err != nil { 298 return err 299 } 300 } else { 301 return os.ErrNotExist 302 } 303 304 // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here 305 306 tree, err := gitRepo.WriteTree() 307 if err != nil { 308 return err 309 } 310 message := fmt.Sprintf("Delete page %q", wikiName) 311 commitTreeOpts := git.CommitTreeOpts{ 312 Message: message, 313 Parents: []string{"HEAD"}, 314 } 315 316 committer := doer.NewGitSig() 317 318 sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) 319 if sign { 320 commitTreeOpts.KeyID = signingKey 321 if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { 322 committer = signer 323 } 324 } else { 325 commitTreeOpts.NoGPGSign = true 326 } 327 328 commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) 329 if err != nil { 330 return err 331 } 332 333 if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{ 334 Remote: DefaultRemote, 335 Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch), 336 Env: repo_module.FullPushingEnvironment( 337 doer, 338 doer, 339 repo, 340 repo.Name+".wiki", 341 0, 342 ), 343 }); err != nil { 344 if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { 345 return err 346 } 347 return fmt.Errorf("Push: %w", err) 348 } 349 350 return nil 351 } 352 353 // DeleteWiki removes the actual and local copy of repository wiki. 354 func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error { 355 if err := repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit.Type{unit.TypeWiki}); err != nil { 356 return err 357 } 358 359 system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath()) 360 return nil 361 } 362 363 func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, newBranch string) error { 364 if !git.IsValidRefPattern(newBranch) { 365 return fmt.Errorf("invalid branch name: %s", newBranch) 366 } 367 return db.WithTx(ctx, func(ctx context.Context) error { 368 repo.DefaultWikiBranch = newBranch 369 if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_wiki_branch"); err != nil { 370 return fmt.Errorf("unable to update database: %w", err) 371 } 372 373 if !repo.HasWiki() { 374 return nil 375 } 376 377 oldDefBranch, err := gitrepo.GetWikiDefaultBranch(ctx, repo) 378 if err != nil { 379 return fmt.Errorf("unable to get default branch: %w", err) 380 } 381 if oldDefBranch == newBranch { 382 return nil 383 } 384 385 gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo) 386 if errors.Is(err, util.ErrNotExist) { 387 return nil // no git repo on storage, no need to do anything else 388 } else if err != nil { 389 return fmt.Errorf("unable to open repository: %w", err) 390 } 391 defer gitRepo.Close() 392 393 err = gitRepo.RenameBranch(oldDefBranch, newBranch) 394 if err != nil { 395 return fmt.Errorf("unable to rename default branch: %w", err) 396 } 397 return nil 398 }) 399 }