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