code.gitea.io/gitea@v1.22.3/services/release/release.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package release 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "strings" 11 12 "code.gitea.io/gitea/models" 13 "code.gitea.io/gitea/models/db" 14 git_model "code.gitea.io/gitea/models/git" 15 repo_model "code.gitea.io/gitea/models/repo" 16 user_model "code.gitea.io/gitea/models/user" 17 "code.gitea.io/gitea/modules/container" 18 "code.gitea.io/gitea/modules/git" 19 "code.gitea.io/gitea/modules/gitrepo" 20 "code.gitea.io/gitea/modules/graceful" 21 "code.gitea.io/gitea/modules/log" 22 "code.gitea.io/gitea/modules/repository" 23 "code.gitea.io/gitea/modules/storage" 24 "code.gitea.io/gitea/modules/timeutil" 25 "code.gitea.io/gitea/modules/util" 26 notify_service "code.gitea.io/gitea/services/notify" 27 ) 28 29 func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Release, msg string) (bool, error) { 30 err := rel.LoadAttributes(ctx) 31 if err != nil { 32 return false, err 33 } 34 35 err = rel.Repo.MustNotBeArchived() 36 if err != nil { 37 return false, err 38 } 39 40 var created bool 41 // Only actual create when publish. 42 if !rel.IsDraft { 43 if !gitRepo.IsTagExist(rel.TagName) { 44 if err := rel.LoadAttributes(ctx); err != nil { 45 log.Error("LoadAttributes: %v", err) 46 return false, err 47 } 48 49 protectedTags, err := git_model.GetProtectedTags(ctx, rel.Repo.ID) 50 if err != nil { 51 return false, fmt.Errorf("GetProtectedTags: %w", err) 52 } 53 54 // Trim '--' prefix to prevent command line argument vulnerability. 55 rel.TagName = strings.TrimPrefix(rel.TagName, "--") 56 isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID) 57 if err != nil { 58 return false, err 59 } 60 if !isAllowed { 61 return false, models.ErrProtectedTagName{ 62 TagName: rel.TagName, 63 } 64 } 65 66 commit, err := gitRepo.GetCommit(rel.Target) 67 if err != nil { 68 return false, err 69 } 70 71 if len(msg) > 0 { 72 if err = gitRepo.CreateAnnotatedTag(rel.TagName, msg, commit.ID.String()); err != nil { 73 if strings.Contains(err.Error(), "is not a valid tag name") { 74 return false, models.ErrInvalidTagName{ 75 TagName: rel.TagName, 76 } 77 } 78 return false, err 79 } 80 } else if err = gitRepo.CreateTag(rel.TagName, commit.ID.String()); err != nil { 81 if strings.Contains(err.Error(), "is not a valid tag name") { 82 return false, models.ErrInvalidTagName{ 83 TagName: rel.TagName, 84 } 85 } 86 return false, err 87 } 88 created = true 89 rel.LowerTagName = strings.ToLower(rel.TagName) 90 91 objectFormat := git.ObjectFormatFromName(rel.Repo.ObjectFormatName) 92 commits := repository.NewPushCommits() 93 commits.HeadCommit = repository.CommitToPushCommit(commit) 94 commits.CompareURL = rel.Repo.ComposeCompareURL(objectFormat.EmptyObjectID().String(), commit.ID.String()) 95 96 refFullName := git.RefNameFromTag(rel.TagName) 97 notify_service.PushCommits( 98 ctx, rel.Publisher, rel.Repo, 99 &repository.PushUpdateOptions{ 100 RefFullName: refFullName, 101 OldCommitID: objectFormat.EmptyObjectID().String(), 102 NewCommitID: commit.ID.String(), 103 }, commits) 104 notify_service.CreateRef(ctx, rel.Publisher, rel.Repo, refFullName, commit.ID.String()) 105 rel.CreatedUnix = timeutil.TimeStampNow() 106 } 107 commit, err := gitRepo.GetTagCommit(rel.TagName) 108 if err != nil { 109 return false, fmt.Errorf("GetTagCommit: %w", err) 110 } 111 112 rel.Sha1 = commit.ID.String() 113 rel.NumCommits, err = commit.CommitsCount() 114 if err != nil { 115 return false, fmt.Errorf("CommitsCount: %w", err) 116 } 117 118 if rel.PublisherID <= 0 { 119 u, err := user_model.GetUserByEmail(ctx, commit.Author.Email) 120 if err == nil { 121 rel.PublisherID = u.ID 122 } 123 } 124 } else { 125 rel.CreatedUnix = timeutil.TimeStampNow() 126 } 127 return created, nil 128 } 129 130 // CreateRelease creates a new release of repository. 131 func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentUUIDs []string, msg string) error { 132 has, err := repo_model.IsReleaseExist(gitRepo.Ctx, rel.RepoID, rel.TagName) 133 if err != nil { 134 return err 135 } else if has { 136 return repo_model.ErrReleaseAlreadyExist{ 137 TagName: rel.TagName, 138 } 139 } 140 141 if _, err = createTag(gitRepo.Ctx, gitRepo, rel, msg); err != nil { 142 return err 143 } 144 145 rel.LowerTagName = strings.ToLower(rel.TagName) 146 if err = db.Insert(gitRepo.Ctx, rel); err != nil { 147 return err 148 } 149 150 if err = repo_model.AddReleaseAttachments(gitRepo.Ctx, rel.ID, attachmentUUIDs); err != nil { 151 return err 152 } 153 154 if !rel.IsDraft { 155 notify_service.NewRelease(gitRepo.Ctx, rel) 156 } 157 158 return nil 159 } 160 161 // CreateNewTag creates a new repository tag 162 func CreateNewTag(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commit, tagName, msg string) error { 163 has, err := repo_model.IsReleaseExist(ctx, repo.ID, tagName) 164 if err != nil { 165 return err 166 } else if has { 167 return models.ErrTagAlreadyExists{ 168 TagName: tagName, 169 } 170 } 171 172 gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) 173 if err != nil { 174 return err 175 } 176 defer closer.Close() 177 178 rel := &repo_model.Release{ 179 RepoID: repo.ID, 180 Repo: repo, 181 PublisherID: doer.ID, 182 Publisher: doer, 183 TagName: tagName, 184 Target: commit, 185 IsDraft: false, 186 IsPrerelease: false, 187 IsTag: true, 188 } 189 190 if _, err = createTag(ctx, gitRepo, rel, msg); err != nil { 191 return err 192 } 193 194 return db.Insert(ctx, rel) 195 } 196 197 // UpdateRelease updates information, attachments of a release and will create tag if it's not a draft and tag not exist. 198 // addAttachmentUUIDs accept a slice of new created attachments' uuids which will be reassigned release_id as the created release 199 // delAttachmentUUIDs accept a slice of attachments' uuids which will be deleted from the release 200 // editAttachments accept a map of attachment uuid to new attachment name which will be updated with attachments. 201 func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, rel *repo_model.Release, 202 addAttachmentUUIDs, delAttachmentUUIDs []string, editAttachments map[string]string, 203 ) error { 204 if rel.ID == 0 { 205 return errors.New("UpdateRelease only accepts an exist release") 206 } 207 isTagCreated, err := createTag(gitRepo.Ctx, gitRepo, rel, "") 208 if err != nil { 209 return err 210 } 211 rel.LowerTagName = strings.ToLower(rel.TagName) 212 213 ctx, committer, err := db.TxContext(ctx) 214 if err != nil { 215 return err 216 } 217 defer committer.Close() 218 219 oldRelease, err := repo_model.GetReleaseByID(ctx, rel.ID) 220 if err != nil { 221 return err 222 } 223 isConvertedFromTag := oldRelease.IsTag && !rel.IsTag 224 225 if err = repo_model.UpdateRelease(ctx, rel); err != nil { 226 return err 227 } 228 229 if err = repo_model.AddReleaseAttachments(ctx, rel.ID, addAttachmentUUIDs); err != nil { 230 return fmt.Errorf("AddReleaseAttachments: %w", err) 231 } 232 233 deletedUUIDs := make(container.Set[string]) 234 if len(delAttachmentUUIDs) > 0 { 235 // Check attachments 236 attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, delAttachmentUUIDs) 237 if err != nil { 238 return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", delAttachmentUUIDs, err) 239 } 240 for _, attach := range attachments { 241 if attach.ReleaseID != rel.ID { 242 return util.SilentWrap{ 243 Message: "delete attachment of release permission denied", 244 Err: util.ErrPermissionDenied, 245 } 246 } 247 deletedUUIDs.Add(attach.UUID) 248 } 249 250 if _, err := repo_model.DeleteAttachments(ctx, attachments, true); err != nil { 251 return fmt.Errorf("DeleteAttachments [uuids: %v]: %w", delAttachmentUUIDs, err) 252 } 253 } 254 255 if len(editAttachments) > 0 { 256 updateAttachmentsList := make([]string, 0, len(editAttachments)) 257 for k := range editAttachments { 258 updateAttachmentsList = append(updateAttachmentsList, k) 259 } 260 // Check attachments 261 attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, updateAttachmentsList) 262 if err != nil { 263 return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", updateAttachmentsList, err) 264 } 265 for _, attach := range attachments { 266 if attach.ReleaseID != rel.ID { 267 return util.SilentWrap{ 268 Message: "update attachment of release permission denied", 269 Err: util.ErrPermissionDenied, 270 } 271 } 272 } 273 274 for uuid, newName := range editAttachments { 275 if !deletedUUIDs.Contains(uuid) { 276 if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{ 277 UUID: uuid, 278 Name: newName, 279 }, "name"); err != nil { 280 return err 281 } 282 } 283 } 284 } 285 286 if err := committer.Commit(); err != nil { 287 return err 288 } 289 290 for _, uuid := range delAttachmentUUIDs { 291 if err := storage.Attachments.Delete(repo_model.AttachmentRelativePath(uuid)); err != nil { 292 // Even delete files failed, but the attachments has been removed from database, so we 293 // should not return error but only record the error on logs. 294 // users have to delete this attachments manually or we should have a 295 // synchronize between database attachment table and attachment storage 296 log.Error("delete attachment[uuid: %s] failed: %v", uuid, err) 297 } 298 } 299 300 if !rel.IsDraft { 301 if !isTagCreated && !isConvertedFromTag { 302 notify_service.UpdateRelease(gitRepo.Ctx, doer, rel) 303 return nil 304 } 305 notify_service.NewRelease(gitRepo.Ctx, rel) 306 } 307 return nil 308 } 309 310 // DeleteReleaseByID deletes a release and corresponding Git tag by given ID. 311 func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *repo_model.Release, doer *user_model.User, delTag bool) error { 312 if delTag { 313 protectedTags, err := git_model.GetProtectedTags(ctx, rel.RepoID) 314 if err != nil { 315 return fmt.Errorf("GetProtectedTags: %w", err) 316 } 317 isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID) 318 if err != nil { 319 return err 320 } 321 if !isAllowed { 322 return models.ErrProtectedTagName{ 323 TagName: rel.TagName, 324 } 325 } 326 327 if stdout, _, err := git.NewCommand(ctx, "tag", "-d").AddDashesAndList(rel.TagName). 328 SetDescription(fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID)). 329 RunStdString(&git.RunOpts{Dir: repo.RepoPath()}); err != nil && !strings.Contains(err.Error(), "not found") { 330 log.Error("DeleteReleaseByID (git tag -d): %d in %v Failed:\nStdout: %s\nError: %v", rel.ID, repo, stdout, err) 331 return fmt.Errorf("git tag -d: %w", err) 332 } 333 334 refName := git.RefNameFromTag(rel.TagName) 335 objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) 336 notify_service.PushCommits( 337 ctx, doer, repo, 338 &repository.PushUpdateOptions{ 339 RefFullName: refName, 340 OldCommitID: rel.Sha1, 341 NewCommitID: objectFormat.EmptyObjectID().String(), 342 }, repository.NewPushCommits()) 343 notify_service.DeleteRef(ctx, doer, repo, refName) 344 345 if _, err := db.DeleteByID[repo_model.Release](ctx, rel.ID); err != nil { 346 return fmt.Errorf("DeleteReleaseByID: %w", err) 347 } 348 } else { 349 rel.IsTag = true 350 351 if err := repo_model.UpdateRelease(ctx, rel); err != nil { 352 return fmt.Errorf("Update: %w", err) 353 } 354 } 355 356 rel.Repo = repo 357 if err := rel.LoadAttributes(ctx); err != nil { 358 return fmt.Errorf("LoadAttributes: %w", err) 359 } 360 361 if err := repo_model.DeleteAttachmentsByRelease(ctx, rel.ID); err != nil { 362 return fmt.Errorf("DeleteAttachments: %w", err) 363 } 364 365 for i := range rel.Attachments { 366 attachment := rel.Attachments[i] 367 if err := storage.Attachments.Delete(attachment.RelativePath()); err != nil { 368 log.Error("Delete attachment %s of release %s failed: %v", attachment.UUID, rel.ID, err) 369 } 370 } 371 372 if !rel.IsDraft { 373 notify_service.DeleteRelease(ctx, doer, rel) 374 } 375 return nil 376 } 377 378 // Init start release service 379 func Init() error { 380 return initTagSyncQueue(graceful.GetManager().ShutdownContext()) 381 }