diff --git a/models/repo/release.go b/models/repo/release.go index 1c2e4a48e3..7cf8760f31 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -298,6 +298,26 @@ func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) { return tags, sess.Find(&tags) } +// GetTagMappingsByRepoID returns a mapping from tag name to commit SHA by repo id +func GetTagMappingsByRepoID(ctx context.Context, repoID int64) (map[string]string, error) { + mapping := make(map[string]string) + rels := make([]*Release, 0) + if err := db.GetEngine(ctx). + Desc("created_unix"). + Find(&rels, Release{RepoID: repoID}); err != nil { + return mapping, err + } + for _, r := range rels { + mapping[r.TagName] = r.Sha1 + } + return mapping, nil +} + +// CountReleasesByRepoID returns a number of releases matching FindReleaseOptions and RepoID. +func CountReleasesByRepoID(ctx context.Context, repoID int64, opts FindReleasesOptions) (int64, error) { + return db.GetEngine(ctx).Where(opts.ToConds()).Count(new(Release)) +} + // GetLatestReleaseByRepoID returns the latest release for a repository func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, error) { cond := builder.NewCond(). diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 41f6c5055d..71c974085c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2679,6 +2679,12 @@ release.add_tag_msg = Use the title and content of release as tag message. release.add_tag = Create Tag Only release.releases_for = Releases for %s release.tags_for = Tags for %s +release.existing_tag_header = New release with an old tag +release.existing_tag_draft_header = Draft release with an old tag +release.existing_tag = You are about to create a new release with an <b>existing tag</b>. Make sure that the tag is pointing to the right commit. +release.existing_draft_tag = You are about to create a draft release with an <b>existing tag</b>. Make sure that the tag is pointing to the right commit. +release.create_confirmation = Do you want to continue with the release creation? +release.create_draft_confirmation = Do you want to continue with the release draft creation? branch.name = Branch Name branch.already_exists = A branch named "%s" already exists. diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 284fd27abf..e84af21798 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -330,9 +330,9 @@ func newReleaseCommon(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.release.new_release") ctx.Data["PageIsReleaseList"] = true - tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) + tags, err := repo_model.GetTagMappingsByRepoID(ctx, ctx.Repo.Repository.ID) if err != nil { - ctx.ServerError("GetTagNamesByRepoID", err) + ctx.ServerError("GetTagMappingsByRepoID", err) return } ctx.Data["Tags"] = tags diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl index 8b6aa252af..6766e2a3a0 100644 --- a/templates/repo/release/new.tmpl +++ b/templates/repo/release/new.tmpl @@ -38,6 +38,9 @@ </div> </div> </div> + <div id="tag-warning" class="gt-dib gt-hidden" data-commit-url-stub="{{.RepoLink}}/commit"> + {{svg "octicon-alert-fill" 16}} Existing tag referencing <span class="gt-px-3">{{svg "octicon-git-commit" 16}} <a target="_blank" rel="noopener noreferrer" class="tag-warning-detail"></a></span> + </div> <div> <span id="tag-helper" class="help tw-mt-2 tw-pb-0">{{ctx.Locale.Tr "repo.release.tag_helper"}}</span> </div> @@ -118,8 +121,8 @@ {{if .ShowCreateTagOnlyButton}} <button class="ui small button" name="tag_only" value="1">{{ctx.Locale.Tr "repo.release.add_tag"}}</button> {{end}} - <button class="ui small button" name="draft" value="1">{{ctx.Locale.Tr "repo.release.save_draft"}}</button> - <button class="ui small primary button">{{ctx.Locale.Tr "repo.release.publish"}}</button> + <button class="ui small button tag-confirm tag-draft" name="draft" value="1">{{ctx.Locale.Tr "repo.release.save_draft"}}</button> + <button class="ui small primary button tag-confirm">{{ctx.Locale.Tr "repo.release.publish"}}</button> {{end}} </div> </div> @@ -128,6 +131,29 @@ </div> </div> +<div id="tag-confirm-modal" class="ui g-modal-confirm modal"> + <div class="header"> + {{ctx.Locale.Tr "repo.release.existing_tag_header"}} + </div> + <div class="content"> + <p>{{svg "octicon-alert-fill" 16}} Existing tag referencing <span class="gt-px-3">{{svg "octicon-git-commit" 16}} <a target="_blank" rel="noopener noreferrer" class="tag-warning-detail"></a></span></p> + <p>{{ctx.Locale.Tr "repo.release.existing_tag"}}</p> + <p>{{ctx.Locale.Tr "repo.release.create_confirmation"}}</p> + </div> + {{template "base/modal_actions_confirm" .}} +</div> +<div id="tag-confirm-draft-modal" class="ui g-modal-confirm modal"> + <div class="header"> + {{ctx.Locale.Tr "repo.release.existing_tag_draft_header"}} + </div> + <div class="content"> + <p>{{svg "octicon-alert-fill" 16}} Existing tag referencing <span class="gt-px-3">{{svg "octicon-git-commit" 16}} <a target="_blank" rel="noopener noreferrer" class="tag-warning-detail"></a></span></p> + <p>{{ctx.Locale.Tr "repo.release.existing_draft_tag"}}</p> + <p>{{ctx.Locale.Tr "repo.release.create_draft_confirmation"}}</p> + </div> + {{template "base/modal_actions_confirm" .}} +</div> + {{if .PageIsEditRelease}} <div class="ui g-modal-confirm delete modal"> <div class="header"> diff --git a/web_src/js/features/repo-release.ts b/web_src/js/features/repo-release.ts index dfff090ba9..f8a35e8f50 100644 --- a/web_src/js/features/repo-release.ts +++ b/web_src/js/features/repo-release.ts @@ -1,4 +1,5 @@ import {hideElem, showElem, type DOMEvent} from '../utils/dom.ts'; +import {fomanticQuery} from "../modules/fomantic/base"; export function initRepoRelease() { document.addEventListener('click', (e: DOMEvent<MouseEvent>) => { @@ -21,24 +22,65 @@ function initTagNameEditor() { const el = document.querySelector('#tag-name-editor'); if (!el) return; + const tagWarning = document.querySelector('#tag-warning'); + const tagWarningDetailLinks = Array.from(document.getElementsByClassName('tag-warning-detail')); const existingTags = JSON.parse(el.getAttribute('data-existing-tags')); if (!Array.isArray(existingTags)) return; const defaultTagHelperText = el.getAttribute('data-tag-helper'); const newTagHelperText = el.getAttribute('data-tag-helper-new'); const existingTagHelperText = el.getAttribute('data-tag-helper-existing'); + const tagURLStub = tagWarning.getAttribute('data-commit-url-stub'); + const tagConfirmDraftModal = document.querySelector('#tag-confirm-draft-modal'); + const tagConfirmModal = document.querySelector('#tag-confirm-modal'); + + // show the confirmation modal if release is using an existing tag + let requiresConfirmation = false; + $('.tag-confirm').on('click', (event) => { + if (requiresConfirmation) { + event.preventDefault(); + if ($(event.target).hasClass('tag-draft')) { + fomanticQuery(tagConfirmDraftModal).modal({ + onApprove() { + // need to add hidden input with draft form value + // (triggering form submission doesn't include the button data) + $('<input>').attr({ + type: 'hidden', + name: 'draft', + value: '1' + }).appendTo(event.target.form); + $(event.target.form).trigger('submit'); + }, + }).modal('show'); + } else { + fomanticQuery(tagConfirmModal).modal({ + onApprove() { + $(event.target.form).trigger('submit'); + }, + }).modal('show'); + } + } + }); const tagNameInput = document.querySelector<HTMLInputElement>('#tag-name'); const hideTargetInput = function(tagNameInput: HTMLInputElement) { const value = tagNameInput.value; const tagHelper = document.querySelector('#tag-helper'); - if (existingTags.includes(value)) { + if (value in existingTags) { // If the tag already exists, hide the target branch selector. hideElem('#tag-target-selector'); tagHelper.textContent = existingTagHelperText; + showElem('#tag-warning'); + for (const detail of tagWarningDetailLinks) { + detail.href = `${tagURLStub}/${existingTags[value]}`; + detail.textContent = existingTags[value].substring(0, 10); + } + requiresConfirmation = true; } else { showElem('#tag-target-selector'); tagHelper.textContent = value ? newTagHelperText : defaultTagHelperText; + hideElem('#tag-warning'); + requiresConfirmation = false; } }; hideTargetInput(tagNameInput); // update on page load because the input may have a value