Browse Source

Sync releases table with tags on push and for mirrors (#2459)

* Sync releases table with tags on push and for mirrors

* Code style fixes

* Fix api to return only releases

* Optimize release creation and update
Minimize posibility of race conditions

* Fix release lower tag name updating

* handle tag reference update by addionally comparing commit id
Lauris BH 2 years ago
parent
commit
7a0297819d

+ 2 - 0
models/migrations/migrations.go

@@ -132,6 +132,8 @@ var migrations = []Migration{
132 132
 	NewMigration("migrate protected branch struct", migrateProtectedBranchStruct),
133 133
 	// v41 -> v42
134 134
 	NewMigration("add default value to user prohibit_login", addDefaultValueToUserProhibitLogin),
135
+	// v42 -> v43
136
+	NewMigration("add tags to releases and sync existing repositories", releaseAddColumnIsTagAndSyncTags),
135 137
 }
136 138
 
137 139
 // Migrate database to current version

+ 57 - 0
models/migrations/v42.go

@@ -0,0 +1,57 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package migrations
6
+
7
+import (
8
+	"fmt"
9
+
10
+	"code.gitea.io/git"
11
+	"code.gitea.io/gitea/models"
12
+	"code.gitea.io/gitea/modules/log"
13
+
14
+	"github.com/go-xorm/xorm"
15
+)
16
+
17
+// ReleaseV39 describes the added field for Release
18
+type ReleaseV39 struct {
19
+	IsTag bool `xorm:"NOT NULL DEFAULT false"`
20
+}
21
+
22
+// TableName will be invoked by XORM to customrize the table name
23
+func (*ReleaseV39) TableName() string {
24
+	return "release"
25
+}
26
+
27
+func releaseAddColumnIsTagAndSyncTags(x *xorm.Engine) error {
28
+	if err := x.Sync2(new(ReleaseV39)); err != nil {
29
+		return fmt.Errorf("Sync2: %v", err)
30
+	}
31
+
32
+	// For the sake of SQLite3, we can't use x.Iterate here.
33
+	offset := 0
34
+	pageSize := 20
35
+	for {
36
+		repos := make([]*models.Repository, 0, pageSize)
37
+		if err := x.Table("repository").Asc("id").Limit(pageSize, offset).Find(&repos); err != nil {
38
+			return fmt.Errorf("select repos [offset: %d]: %v", offset, err)
39
+		}
40
+		for _, repo := range repos {
41
+			gitRepo, err := git.OpenRepository(repo.RepoPath())
42
+			if err != nil {
43
+				log.Warn("OpenRepository: %v", err)
44
+				continue
45
+			}
46
+
47
+			if err = models.SyncReleasesWithTags(repo, gitRepo); err != nil {
48
+				log.Warn("SyncReleasesWithTags: %v", err)
49
+			}
50
+		}
51
+		if len(repos) < pageSize {
52
+			break
53
+		}
54
+		offset += pageSize
55
+	}
56
+	return nil
57
+}

+ 75 - 14
models/release.go

@@ -34,7 +34,8 @@ type Release struct {
34 34
 	NumCommitsBehind int64  `xorm:"-"`
35 35
 	Note             string `xorm:"TEXT"`
36 36
 	IsDraft          bool   `xorm:"NOT NULL DEFAULT false"`
37
-	IsPrerelease     bool
37
+	IsPrerelease     bool   `xorm:"NOT NULL DEFAULT false"`
38
+	IsTag            bool   `xorm:"NOT NULL DEFAULT false"`
38 39
 
39 40
 	Attachments []*Attachment `xorm:"-"`
40 41
 
@@ -139,17 +140,18 @@ func createTag(gitRepo *git.Repository, rel *Release) error {
139 140
 				}
140 141
 				return err
141 142
 			}
142
-		} else {
143
-			commit, err := gitRepo.GetTagCommit(rel.TagName)
144
-			if err != nil {
145
-				return fmt.Errorf("GetTagCommit: %v", err)
146
-			}
143
+			rel.LowerTagName = strings.ToLower(rel.TagName)
144
+		}
145
+		commit, err := gitRepo.GetTagCommit(rel.TagName)
146
+		if err != nil {
147
+			return fmt.Errorf("GetTagCommit: %v", err)
148
+		}
147 149
 
148
-			rel.Sha1 = commit.ID.String()
149
-			rel.NumCommits, err = commit.CommitsCount()
150
-			if err != nil {
151
-				return fmt.Errorf("CommitsCount: %v", err)
152
-			}
150
+		rel.Sha1 = commit.ID.String()
151
+		rel.CreatedUnix = commit.Author.When.Unix()
152
+		rel.NumCommits, err = commit.CommitsCount()
153
+		if err != nil {
154
+			return fmt.Errorf("CommitsCount: %v", err)
153 155
 		}
154 156
 	}
155 157
 	return nil
@@ -236,6 +238,7 @@ func GetReleaseByID(id int64) (*Release, error) {
236 238
 // FindReleasesOptions describes the conditions to Find releases
237 239
 type FindReleasesOptions struct {
238 240
 	IncludeDrafts bool
241
+	IncludeTags   bool
239 242
 	TagNames      []string
240 243
 }
241 244
 
@@ -246,6 +249,9 @@ func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond {
246 249
 	if !opts.IncludeDrafts {
247 250
 		cond = cond.And(builder.Eq{"is_draft": false})
248 251
 	}
252
+	if !opts.IncludeTags {
253
+		cond = cond.And(builder.Eq{"is_tag": false})
254
+	}
249 255
 	if len(opts.TagNames) > 0 {
250 256
 		cond = cond.And(builder.In("tag_name", opts.TagNames))
251 257
 	}
@@ -361,6 +367,8 @@ func UpdateRelease(gitRepo *git.Repository, rel *Release, attachmentUUIDs []stri
361 367
 	if err = createTag(gitRepo, rel); err != nil {
362 368
 		return err
363 369
 	}
370
+	rel.LowerTagName = strings.ToLower(rel.TagName)
371
+
364 372
 	_, err = x.Id(rel.ID).AllCols().Update(rel)
365 373
 	if err != nil {
366 374
 		return err
@@ -397,11 +405,64 @@ func DeleteReleaseByID(id int64, u *User, delTag bool) error {
397 405
 		if err != nil && !strings.Contains(stderr, "not found") {
398 406
 			return fmt.Errorf("git tag -d: %v - %s", err, stderr)
399 407
 		}
400
-	}
401 408
 
402
-	if _, err = x.Id(rel.ID).Delete(new(Release)); err != nil {
403
-		return fmt.Errorf("Delete: %v", err)
409
+		if _, err = x.Id(rel.ID).Delete(new(Release)); err != nil {
410
+			return fmt.Errorf("Delete: %v", err)
411
+		}
412
+	} else {
413
+		rel.IsTag = true
414
+		rel.IsDraft = false
415
+		rel.IsPrerelease = false
416
+		rel.Title = ""
417
+		rel.Note = ""
418
+
419
+		if _, err = x.Id(rel.ID).AllCols().Update(rel); err != nil {
420
+			return fmt.Errorf("Update: %v", err)
421
+		}
404 422
 	}
405 423
 
406 424
 	return nil
407 425
 }
426
+
427
+// SyncReleasesWithTags synchronizes release table with repository tags
428
+func SyncReleasesWithTags(repo *Repository, gitRepo *git.Repository) error {
429
+	existingRelTags := make(map[string]struct{})
430
+	opts := FindReleasesOptions{IncludeDrafts: true, IncludeTags: true}
431
+	for page := 1; ; page++ {
432
+		rels, err := GetReleasesByRepoID(repo.ID, opts, page, 100)
433
+		if err != nil {
434
+			return fmt.Errorf("GetReleasesByRepoID: %v", err)
435
+		}
436
+		if len(rels) == 0 {
437
+			break
438
+		}
439
+		for _, rel := range rels {
440
+			if rel.IsDraft {
441
+				continue
442
+			}
443
+			commitID, err := gitRepo.GetTagCommitID(rel.TagName)
444
+			if err != nil {
445
+				return fmt.Errorf("GetTagCommitID: %v", err)
446
+			}
447
+			if !gitRepo.IsTagExist(rel.TagName) || commitID != rel.Sha1 {
448
+				if err := pushUpdateDeleteTag(repo, gitRepo, rel.TagName); err != nil {
449
+					return fmt.Errorf("pushUpdateDeleteTag: %v", err)
450
+				}
451
+			} else {
452
+				existingRelTags[strings.ToLower(rel.TagName)] = struct{}{}
453
+			}
454
+		}
455
+	}
456
+	tags, err := gitRepo.GetTags()
457
+	if err != nil {
458
+		return fmt.Errorf("GetTags: %v", err)
459
+	}
460
+	for _, tagName := range tags {
461
+		if _, ok := existingRelTags[strings.ToLower(tagName)]; !ok {
462
+			if err := pushUpdateAddTag(repo, gitRepo, tagName); err != nil {
463
+				return fmt.Errorf("pushUpdateAddTag: %v", err)
464
+			}
465
+		}
466
+	}
467
+	return nil
468
+}

+ 4 - 0
models/repo.go

@@ -940,6 +940,10 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err
940 940
 		if headBranch != nil {
941 941
 			repo.DefaultBranch = headBranch.Name
942 942
 		}
943
+
944
+		if err = SyncReleasesWithTags(repo, gitRepo); err != nil {
945
+			log.Error(4, "Failed to synchronize tags to releases for repository: %v", err)
946
+		}
943 947
 	}
944 948
 
945 949
 	if err = repo.UpdateSize(); err != nil {

+ 10 - 0
models/repo_mirror.go

@@ -13,6 +13,7 @@ import (
13 13
 	"github.com/go-xorm/xorm"
14 14
 	"gopkg.in/ini.v1"
15 15
 
16
+	"code.gitea.io/git"
16 17
 	"code.gitea.io/gitea/modules/log"
17 18
 	"code.gitea.io/gitea/modules/process"
18 19
 	"code.gitea.io/gitea/modules/setting"
@@ -156,6 +157,15 @@ func (m *Mirror) runSync() bool {
156 157
 		return false
157 158
 	}
158 159
 
160
+	gitRepo, err := git.OpenRepository(repoPath)
161
+	if err != nil {
162
+		log.Error(4, "OpenRepository: %v", err)
163
+		return false
164
+	}
165
+	if err = SyncReleasesWithTags(m.Repo, gitRepo); err != nil {
166
+		log.Error(4, "Failed to synchronize tags to releases for repository: %v", err)
167
+	}
168
+
159 169
 	if err := m.Repo.UpdateSize(); err != nil {
160 170
 		log.Error(4, "Failed to update size for mirror repository: %v", err)
161 171
 	}

+ 100 - 5
models/update.go

@@ -81,6 +81,93 @@ func PushUpdate(branch string, opt PushUpdateOptions) error {
81 81
 	return nil
82 82
 }
83 83
 
84
+func pushUpdateDeleteTag(repo *Repository, gitRepo *git.Repository, tagName string) error {
85
+	rel, err := GetRelease(repo.ID, tagName)
86
+	if err != nil {
87
+		if IsErrReleaseNotExist(err) {
88
+			return nil
89
+		}
90
+		return fmt.Errorf("GetRelease: %v", err)
91
+	}
92
+	if rel.IsTag {
93
+		if _, err = x.Id(rel.ID).Delete(new(Release)); err != nil {
94
+			return fmt.Errorf("Delete: %v", err)
95
+		}
96
+	} else {
97
+		rel.IsDraft = true
98
+		rel.NumCommits = 0
99
+		rel.Sha1 = ""
100
+		if _, err = x.Id(rel.ID).AllCols().Update(rel); err != nil {
101
+			return fmt.Errorf("Update: %v", err)
102
+		}
103
+	}
104
+
105
+	return nil
106
+}
107
+
108
+func pushUpdateAddTag(repo *Repository, gitRepo *git.Repository, tagName string) error {
109
+	rel, err := GetRelease(repo.ID, tagName)
110
+	if err != nil && !IsErrReleaseNotExist(err) {
111
+		return fmt.Errorf("GetRelease: %v", err)
112
+	}
113
+
114
+	tag, err := gitRepo.GetTag(tagName)
115
+	if err != nil {
116
+		return fmt.Errorf("GetTag: %v", err)
117
+	}
118
+	commit, err := tag.Commit()
119
+	if err != nil {
120
+		return fmt.Errorf("Commit: %v", err)
121
+	}
122
+	tagCreatedUnix := commit.Author.When.Unix()
123
+
124
+	author, err := GetUserByEmail(commit.Author.Email)
125
+	if err != nil && !IsErrUserNotExist(err) {
126
+		return fmt.Errorf("GetUserByEmail: %v", err)
127
+	}
128
+
129
+	commitsCount, err := commit.CommitsCount()
130
+	if err != nil {
131
+		return fmt.Errorf("CommitsCount: %v", err)
132
+	}
133
+
134
+	if rel == nil {
135
+		rel = &Release{
136
+			RepoID:       repo.ID,
137
+			Title:        "",
138
+			TagName:      tagName,
139
+			LowerTagName: strings.ToLower(tagName),
140
+			Target:       "",
141
+			Sha1:         commit.ID.String(),
142
+			NumCommits:   commitsCount,
143
+			Note:         "",
144
+			IsDraft:      false,
145
+			IsPrerelease: false,
146
+			IsTag:        true,
147
+			CreatedUnix:  tagCreatedUnix,
148
+		}
149
+		if author != nil {
150
+			rel.PublisherID = author.ID
151
+		}
152
+
153
+		if _, err = x.InsertOne(rel); err != nil {
154
+			return fmt.Errorf("InsertOne: %v", err)
155
+		}
156
+	} else {
157
+		rel.Sha1 = commit.ID.String()
158
+		rel.CreatedUnix = tagCreatedUnix
159
+		rel.NumCommits = commitsCount
160
+		rel.IsDraft = false
161
+		if rel.IsTag && author != nil {
162
+			rel.PublisherID = author.ID
163
+		}
164
+		if _, err = x.Id(rel.ID).AllCols().Update(rel); err != nil {
165
+			return fmt.Errorf("Update: %v", err)
166
+		}
167
+	}
168
+	return nil
169
+}
170
+
84 171
 func pushUpdate(opts PushUpdateOptions) (repo *Repository, err error) {
85 172
 	isNewRef := opts.OldCommitID == git.EmptySHA
86 173
 	isDelRef := opts.NewCommitID == git.EmptySHA
@@ -106,23 +193,31 @@ func pushUpdate(opts PushUpdateOptions) (repo *Repository, err error) {
106 193
 		return nil, fmt.Errorf("GetRepositoryByName: %v", err)
107 194
 	}
108 195
 
196
+	gitRepo, err := git.OpenRepository(repoPath)
197
+	if err != nil {
198
+		return nil, fmt.Errorf("OpenRepository: %v", err)
199
+	}
200
+
109 201
 	if isDelRef {
202
+		// Tag has been deleted
203
+		if strings.HasPrefix(opts.RefFullName, git.TagPrefix) {
204
+			err = pushUpdateDeleteTag(repo, gitRepo, opts.RefFullName[len(git.TagPrefix):])
205
+			if err != nil {
206
+				return nil, fmt.Errorf("pushUpdateDeleteTag: %v", err)
207
+			}
208
+		}
110 209
 		log.GitLogger.Info("Reference '%s' has been deleted from '%s/%s' by %s",
111 210
 			opts.RefFullName, opts.RepoUserName, opts.RepoName, opts.PusherName)
112 211
 		return repo, nil
113 212
 	}
114 213
 
115
-	gitRepo, err := git.OpenRepository(repoPath)
116
-	if err != nil {
117
-		return nil, fmt.Errorf("OpenRepository: %v", err)
118
-	}
119
-
120 214
 	if err = repo.UpdateSize(); err != nil {
121 215
 		log.Error(4, "Failed to update size for repository: %v", err)
122 216
 	}
123 217
 
124 218
 	// Push tags.
125 219
 	if strings.HasPrefix(opts.RefFullName, git.TagPrefix) {
220
+		pushUpdateAddTag(repo, gitRepo, opts.RefFullName[len(git.TagPrefix):])
126 221
 		if err := CommitRepoAction(CommitRepoActionOptions{
127 222
 			PusherName:  opts.PusherName,
128 223
 			RepoOwnerID: owner.ID,

+ 1 - 0
modules/context/repo.go

@@ -357,6 +357,7 @@ func RepoAssignment() macaron.Handler {
357 357
 
358 358
 		count, err := models.GetReleaseCountByRepoID(ctx.Repo.Repository.ID, models.FindReleasesOptions{
359 359
 			IncludeDrafts: false,
360
+			IncludeTags:   true,
360 361
 		})
361 362
 		if err != nil {
362 363
 			ctx.Handle(500, "GetReleaseCountByRepoID", err)

+ 48 - 41
routers/api/v1/repo/release.go

@@ -5,8 +5,6 @@
5 5
 package repo
6 6
 
7 7
 import (
8
-	"strings"
9
-
10 8
 	api "code.gitea.io/sdk/gitea"
11 9
 
12 10
 	"code.gitea.io/gitea/models"
@@ -36,6 +34,7 @@ func GetRelease(ctx *context.APIContext) {
36 34
 func ListReleases(ctx *context.APIContext) {
37 35
 	releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, models.FindReleasesOptions{
38 36
 		IncludeDrafts: ctx.Repo.AccessMode >= models.AccessModeWrite,
37
+		IncludeTags:   false,
39 38
 	}, 1, 2147483647)
40 39
 	if err != nil {
41 40
 		ctx.Error(500, "GetReleasesByRepoID", err)
@@ -62,43 +61,49 @@ func CreateRelease(ctx *context.APIContext, form api.CreateReleaseOption) {
62 61
 		ctx.Status(404)
63 62
 		return
64 63
 	}
65
-	tag, err := ctx.Repo.GitRepo.GetTag(form.TagName)
66
-	if err != nil {
67
-		ctx.Error(500, "GetTag", err)
68
-		return
69
-	}
70
-	commit, err := tag.Commit()
71
-	if err != nil {
72
-		ctx.Error(500, "Commit", err)
73
-		return
74
-	}
75
-	commitsCount, err := commit.CommitsCount()
64
+	rel, err := models.GetRelease(ctx.Repo.Repository.ID, form.TagName)
76 65
 	if err != nil {
77
-		ctx.Error(500, "CommitsCount", err)
78
-		return
79
-	}
80
-	rel := &models.Release{
81
-		RepoID:       ctx.Repo.Repository.ID,
82
-		PublisherID:  ctx.User.ID,
83
-		Publisher:    ctx.User,
84
-		TagName:      form.TagName,
85
-		LowerTagName: strings.ToLower(form.TagName),
86
-		Target:       form.Target,
87
-		Title:        form.Title,
88
-		Sha1:         commit.ID.String(),
89
-		NumCommits:   commitsCount,
90
-		Note:         form.Note,
91
-		IsDraft:      form.IsDraft,
92
-		IsPrerelease: form.IsPrerelease,
93
-		CreatedUnix:  commit.Author.When.Unix(),
94
-	}
95
-	if err := models.CreateRelease(ctx.Repo.GitRepo, rel, nil); err != nil {
96
-		if models.IsErrReleaseAlreadyExist(err) {
66
+		if !models.IsErrReleaseNotExist(err) {
67
+			ctx.Handle(500, "GetRelease", err)
68
+			return
69
+		}
70
+		rel = &models.Release{
71
+			RepoID:       ctx.Repo.Repository.ID,
72
+			PublisherID:  ctx.User.ID,
73
+			Publisher:    ctx.User,
74
+			TagName:      form.TagName,
75
+			Target:       form.Target,
76
+			Title:        form.Title,
77
+			Note:         form.Note,
78
+			IsDraft:      form.IsDraft,
79
+			IsPrerelease: form.IsPrerelease,
80
+			IsTag:        false,
81
+		}
82
+		if err := models.CreateRelease(ctx.Repo.GitRepo, rel, nil); err != nil {
83
+			if models.IsErrReleaseAlreadyExist(err) {
84
+				ctx.Status(409)
85
+			} else {
86
+				ctx.Error(500, "CreateRelease", err)
87
+			}
88
+			return
89
+		}
90
+	} else {
91
+		if !rel.IsTag {
97 92
 			ctx.Status(409)
98
-		} else {
99
-			ctx.Error(500, "CreateRelease", err)
93
+			return
94
+		}
95
+
96
+		rel.Title = form.Title
97
+		rel.Note = form.Note
98
+		rel.IsDraft = form.IsDraft
99
+		rel.IsPrerelease = form.IsPrerelease
100
+		rel.PublisherID = ctx.User.ID
101
+		rel.IsTag = false
102
+
103
+		if err = models.UpdateRelease(ctx.Repo.GitRepo, rel, nil); err != nil {
104
+			ctx.Handle(500, "UpdateRelease", err)
105
+			return
100 106
 		}
101
-		return
102 107
 	}
103 108
 	ctx.JSON(201, rel.APIFormat())
104 109
 }
@@ -111,11 +116,12 @@ func EditRelease(ctx *context.APIContext, form api.EditReleaseOption) {
111 116
 	}
112 117
 	id := ctx.ParamsInt64(":id")
113 118
 	rel, err := models.GetReleaseByID(id)
114
-	if err != nil {
119
+	if err != nil && !models.IsErrReleaseNotExist(err) {
115 120
 		ctx.Error(500, "GetReleaseByID", err)
116 121
 		return
117 122
 	}
118
-	if rel.RepoID != ctx.Repo.Repository.ID {
123
+	if err != nil && models.IsErrReleaseNotExist(err) ||
124
+		rel.IsTag || rel.RepoID != ctx.Repo.Repository.ID {
119 125
 		ctx.Status(404)
120 126
 		return
121 127
 	}
@@ -162,12 +168,13 @@ func DeleteRelease(ctx *context.APIContext) {
162 168
 		return
163 169
 	}
164 170
 	id := ctx.ParamsInt64(":id")
165
-	release, err := models.GetReleaseByID(id)
166
-	if err != nil {
171
+	rel, err := models.GetReleaseByID(id)
172
+	if err != nil && !models.IsErrReleaseNotExist(err) {
167 173
 		ctx.Error(500, "GetReleaseByID", err)
168 174
 		return
169 175
 	}
170
-	if release.RepoID != ctx.Repo.Repository.ID {
176
+	if err != nil && models.IsErrReleaseNotExist(err) ||
177
+		rel.IsTag || rel.RepoID != ctx.Repo.Repository.ID {
171 178
 		ctx.Status(404)
172 179
 		return
173 180
 	}

+ 53 - 45
routers/repo/release.go

@@ -67,6 +67,7 @@ func Releases(ctx *context.Context) {
67 67
 
68 68
 	opts := models.FindReleasesOptions{
69 69
 		IncludeDrafts: ctx.Repo.IsWriter(),
70
+		IncludeTags:   true,
70 71
 	}
71 72
 
72 73
 	releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, opts, page, limit)
@@ -145,57 +146,61 @@ func NewReleasePost(ctx *context.Context, form auth.NewReleaseForm) {
145 146
 		return
146 147
 	}
147 148
 
148
-	var tagCreatedUnix int64
149
-	tag, err := ctx.Repo.GitRepo.GetTag(form.TagName)
150
-	if err == nil {
151
-		commit, err := tag.Commit()
152
-		if err == nil {
153
-			tagCreatedUnix = commit.Author.When.Unix()
154
-		}
149
+	var attachmentUUIDs []string
150
+	if setting.AttachmentEnabled {
151
+		attachmentUUIDs = form.Files
155 152
 	}
156 153
 
157
-	commit, err := ctx.Repo.GitRepo.GetBranchCommit(form.Target)
154
+	rel, err := models.GetRelease(ctx.Repo.Repository.ID, form.TagName)
158 155
 	if err != nil {
159
-		ctx.Handle(500, "GetBranchCommit", err)
160
-		return
161
-	}
156
+		if !models.IsErrReleaseNotExist(err) {
157
+			ctx.Handle(500, "GetRelease", err)
158
+			return
159
+		}
162 160
 
163
-	commitsCount, err := commit.CommitsCount()
164
-	if err != nil {
165
-		ctx.Handle(500, "CommitsCount", err)
166
-		return
167
-	}
161
+		rel := &models.Release{
162
+			RepoID:       ctx.Repo.Repository.ID,
163
+			PublisherID:  ctx.User.ID,
164
+			Title:        form.Title,
165
+			TagName:      form.TagName,
166
+			Target:       form.Target,
167
+			Note:         form.Content,
168
+			IsDraft:      len(form.Draft) > 0,
169
+			IsPrerelease: form.Prerelease,
170
+			IsTag:        false,
171
+		}
168 172
 
169
-	rel := &models.Release{
170
-		RepoID:       ctx.Repo.Repository.ID,
171
-		PublisherID:  ctx.User.ID,
172
-		Title:        form.Title,
173
-		TagName:      form.TagName,
174
-		Target:       form.Target,
175
-		Sha1:         commit.ID.String(),
176
-		NumCommits:   commitsCount,
177
-		Note:         form.Content,
178
-		IsDraft:      len(form.Draft) > 0,
179
-		IsPrerelease: form.Prerelease,
180
-		CreatedUnix:  tagCreatedUnix,
181
-	}
173
+		if err = models.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs); err != nil {
174
+			ctx.Data["Err_TagName"] = true
175
+			switch {
176
+			case models.IsErrReleaseAlreadyExist(err):
177
+				ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
178
+			case models.IsErrInvalidTagName(err):
179
+				ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
180
+			default:
181
+				ctx.Handle(500, "CreateRelease", err)
182
+			}
183
+			return
184
+		}
185
+	} else {
186
+		if !rel.IsTag {
187
+			ctx.Data["Err_TagName"] = true
188
+			ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
189
+			return
190
+		}
182 191
 
183
-	var attachmentUUIDs []string
184
-	if setting.AttachmentEnabled {
185
-		attachmentUUIDs = form.Files
186
-	}
192
+		rel.Title = form.Title
193
+		rel.Note = form.Content
194
+		rel.IsDraft = len(form.Draft) > 0
195
+		rel.IsPrerelease = form.Prerelease
196
+		rel.PublisherID = ctx.User.ID
197
+		rel.IsTag = false
187 198
 
188
-	if err = models.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs); err != nil {
189
-		ctx.Data["Err_TagName"] = true
190
-		switch {
191
-		case models.IsErrReleaseAlreadyExist(err):
192
-			ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
193
-		case models.IsErrInvalidTagName(err):
194
-			ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
195
-		default:
196
-			ctx.Handle(500, "CreateRelease", err)
199
+		if err = models.UpdateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs); err != nil {
200
+			ctx.Data["Err_TagName"] = true
201
+			ctx.Handle(500, "UpdateRelease", err)
202
+			return
197 203
 		}
198
-		return
199 204
 	}
200 205
 	log.Trace("Release created: %s/%s:%s", ctx.User.LowerName, ctx.Repo.Repository.Name, form.TagName)
201 206
 
@@ -246,6 +251,10 @@ func EditReleasePost(ctx *context.Context, form auth.EditReleaseForm) {
246 251
 		}
247 252
 		return
248 253
 	}
254
+	if rel.IsTag {
255
+		ctx.Handle(404, "GetRelease", err)
256
+		return
257
+	}
249 258
 	ctx.Data["tag_name"] = rel.TagName
250 259
 	ctx.Data["tag_target"] = rel.Target
251 260
 	ctx.Data["title"] = rel.Title
@@ -275,8 +284,7 @@ func EditReleasePost(ctx *context.Context, form auth.EditReleaseForm) {
275 284
 
276 285
 // DeleteRelease delete a release
277 286
 func DeleteRelease(ctx *context.Context) {
278
-	delTag := ctx.QueryBool("delTag")
279
-	if err := models.DeleteReleaseByID(ctx.QueryInt64("id"), ctx.User, delTag); err != nil {
287
+	if err := models.DeleteReleaseByID(ctx.QueryInt64("id"), ctx.User, true); err != nil {
280 288
 		ctx.Flash.Error("DeleteReleaseByID: " + err.Error())
281 289
 	} else {
282 290
 		ctx.Flash.Success(ctx.Tr("repo.release.deletion_success"))

+ 16 - 13
templates/repo/release/list.tmpl

@@ -17,7 +17,9 @@
17 17
 			{{range .Releases}}
18 18
 				<li class="ui grid">
19 19
 					<div class="ui four wide column meta">
20
-						{{if .PublisherID}}
20
+						{{if .IsTag}}
21
+							{{if .Created}}<span class="time">{{TimeSince .Created $.Lang}}</span>{{end}}
22
+						{{else}}
21 23
 							{{if .IsDraft}}
22 24
 								<span class="ui yellow label">{{$.i18n.Tr "repo.release.draft"}}</span>
23 25
 							{{else if .IsPrerelease}}
@@ -28,13 +30,22 @@
28 30
 							<span class="tag text blue">
29 31
 								<a href="{{$.RepoLink}}/src/{{.TagName}}" rel="nofollow"><i class="tag icon"></i> {{.TagName}}</a>
30 32
 							</span>
33
+							<span class="commit">
34
+								<a href="{{$.RepoLink}}/src/{{.Sha1}}" rel="nofollow"><i class="code icon"></i> {{ShortSha .Sha1}}</a>
35
+							</span>
31 36
 						{{end}}
32
-						<span class="commit">
33
-							<a href="{{$.RepoLink}}/src/{{.Sha1}}" rel="nofollow"><i class="code icon"></i> {{ShortSha .Sha1}}</a>
34
-						</span>
35 37
 					</div>
36 38
 					<div class="ui twelve wide column detail">
37
-						{{if .PublisherID}}
39
+						{{if .IsTag}}
40
+							<h4>
41
+								<a href="{{$.RepoLink}}/src/{{.TagName}}" rel="nofollow"><i class="tag icon"></i> {{.TagName}}</a>
42
+							</h4>
43
+							<div class="download">
44
+								<a href="{{$.RepoLink}}/src/{{.Sha1}}" rel="nofollow"><i class="code icon"></i> {{ShortSha .Sha1}}</a>
45
+								<a href="{{$.RepoLink}}/archive/{{.TagName}}.zip" rel="nofollow"><i class="octicon octicon-file-zip"></i> ZIP</a>
46
+								<a href="{{$.RepoLink}}/archive/{{.TagName}}.tar.gz"><i class="octicon octicon-file-zip"></i> TAR.GZ</a>
47
+							</div>
48
+						{{else}}
38 49
 							<h3>
39 50
 								<a href="{{$.RepoLink}}/src/{{.TagName}}">{{.Title}}</a>
40 51
 								{{if $.IsRepositoryWriter}}<small>(<a href="{{$.RepoLink}}/releases/edit/{{.TagName}}" rel="nofollow">{{$.i18n.Tr "repo.release.edit"}}</a>)</small>{{end}}
@@ -70,14 +81,6 @@
70 81
 									{{end}}
71 82
 								</ul>
72 83
 							</div>
73
-						{{else}}
74
-							<h4>
75
-								<a href="{{$.RepoLink}}/src/{{.TagName}}" rel="nofollow"><i class="tag icon"></i> {{.TagName}}</a>
76
-							</h4>
77
-							<div class="download">
78
-								<a href="{{$.RepoLink}}/archive/{{.TagName}}.zip" rel="nofollow"><i class="octicon octicon-file-zip"></i> ZIP</a>
79
-								<a href="{{$.RepoLink}}/archive/{{.TagName}}.tar.gz"><i class="octicon octicon-file-zip"></i> TAR.GZ</a>
80
-							</div>
81 84
 						{{end}}
82 85
 						<span class="dot">&nbsp;</span>
83 86
 					</div>