diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a97e2e2b3b86d..f91a5edfe0faf 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1615,6 +1615,8 @@ pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull re pulls.delete.title = Delete this pull request? pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) +pulls.refresh = Refresh + milestones.new = New Milestone milestones.closed = Closed %s milestones.update_ago = Updated %s ago diff --git a/routers/private/event.go b/routers/private/event.go new file mode 100644 index 0000000000000..2de56682a99b6 --- /dev/null +++ b/routers/private/event.go @@ -0,0 +1,67 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. +package private + +import ( + access_model "code.gitea.io/gitea/models/perm/access" + user_model "code.gitea.io/gitea/models/user" + gitea_context "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/eventsource" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/web" +) + +type BranchUpdateEvent struct { + CommitID string + Branch string + BranchDeleted bool + Owner string + RefFullName string + Repository string +} + +func SendBranchUpdateEvent(readers []*user_model.User, commitID, branchName, repoName, refFullName, ownerName string) { + manager := eventsource.GetManager() + + for _, reader := range readers { + manager.SendMessage(reader.ID, &eventsource.Event{ + Name: "branch-update", + Data: BranchUpdateEvent{ + CommitID: commitID, + Branch: branchName, + BranchDeleted: commitID == git.EmptySHA, + Repository: repoName, + RefFullName: refFullName, + Owner: ownerName, + }, + }) + } +} + +func SendContextBranchUpdateEvents(ctx *gitea_context.PrivateContext) error { + opts := web.GetForm(ctx).(*private.HookOptions) + + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + + repo := loadRepository(ctx, ownerName, repoName) + + readers, err := access_model.GetRepoReaders(repo) + if err != nil { + return err + } + + for i := range opts.OldCommitIDs { + branch := git.RefEndName(opts.RefFullNames[i]) + commitID := opts.NewCommitIDs[i] + refFullName := opts.RefFullNames[i] + + SendBranchUpdateEvent(readers, commitID, branch, repoName, refFullName, ownerName) + } + + return nil +} diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 93aa450f9c3b7..7799310b6c855 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -233,6 +233,19 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { } } } + + // Notify users with read access via eventsource that a change + // has been made + if err := SendContextBranchUpdateEvents(ctx); err != nil { + log.Error("Failed to Send Branch Update Events: %s/%s Error: %v", ownerName, repoName, err) + + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Failed to Send Branch Update Events: %s/%s Error: %v", ownerName, repoName, err), + }) + + return + } + ctx.JSON(http.StatusOK, private.HookPostReceiveResult{ Results: results, RepoWasEmpty: wasEmpty, diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index e6f9529e31e81..846901704b01e 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1171,6 +1171,7 @@ func ViewIssue(ctx *context.Context) { } ctx.Data["PageIsPullList"] = true ctx.Data["PageIsPullConversation"] = true + ctx.Data["Reponame"] = ctx.Repo.Repository.Name } else { MustEnableIssues(ctx) if ctx.Written() { diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index 456515af33100..3f330e772e4f3 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -5,6 +5,9 @@
{{.locale.Tr "repo.issues.edit"}}
{{end}} +
+ +

{{RenderIssueTitle $.Context .Issue.Title $.RepoLink $.Repository.ComposeMetas}} #{{.Issue.Index}} diff --git a/web_src/js/features/eventsource.sharedworker.js b/web_src/js/features/eventsource.sharedworker.js index 824ccfea79f84..48c2eda4d9e00 100644 --- a/web_src/js/features/eventsource.sharedworker.js +++ b/web_src/js/features/eventsource.sharedworker.js @@ -13,6 +13,7 @@ class Source { this.listen('notification-count'); this.listen('stopwatches'); this.listen('error'); + this.listen('branch-update'); } register(port) { diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js index 36df196cac2d8..aa7676c3e0a2c 100644 --- a/web_src/js/features/notification.js +++ b/web_src/js/features/notification.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import getMemoizedSharedWorker from './shared-worker.js'; const {appSubUrl, csrfToken, notificationSettings} = window.config; let notificationSequenceNumber = 0; @@ -49,19 +50,10 @@ export function initNotificationCount() { return; } - if (notificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource && window.SharedWorker) { + let worker; + + if (notificationSettings.EventSourceUpdateTime > 0 && (worker = getMemoizedSharedWorker())) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js`, 'notification-worker'); - worker.addEventListener('error', (event) => { - console.error(event); - }); - worker.port.addEventListener('messageerror', () => { - console.error('Unable to deserialize message'); - }); - worker.port.postMessage({ - type: 'start', - url: `${window.location.origin}${appSubUrl}/user/events`, - }); worker.port.addEventListener('message', (event) => { if (!event.data || !event.data.type) { console.error(event); @@ -69,34 +61,8 @@ export function initNotificationCount() { } if (event.data.type === 'notification-count') { const _promise = receiveUpdateCount(event.data); - } else if (event.data.type === 'error') { - console.error(event.data); - } else if (event.data.type === 'logout') { - if (event.data.data !== 'here') { - return; - } - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - window.location.href = appSubUrl; - } else if (event.data.type === 'close') { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); } }); - worker.port.addEventListener('error', (e) => { - console.error(e); - }); - worker.port.start(); - window.addEventListener('beforeunload', () => { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - }); return; } diff --git a/web_src/js/features/repo-refresh-pr.js b/web_src/js/features/repo-refresh-pr.js new file mode 100644 index 0000000000000..19d57c68b2612 --- /dev/null +++ b/web_src/js/features/repo-refresh-pr.js @@ -0,0 +1,56 @@ +import $ from 'jquery'; +import getMemoizedSharedWorker from './shared-worker.js'; + +async function receiveBranchUpdated(event) { + try { + const data = JSON.parse(event.data); + + const refreshPullRequest = document.querySelector('.refresh-pull-request'); + + if (!refreshPullRequest) { + return; + } + + const baseTarget = $(refreshPullRequest).data('baseTarget'); + const headTarget = $(refreshPullRequest).data('headTarget'); + const ownerName = $(refreshPullRequest).data('ownerName'); + const repositoryName = $(refreshPullRequest).data('repositoryName'); + + if ( + [baseTarget, headTarget].includes(data.Branch) && + data.Owner === ownerName && + data.Repository === repositoryName + ) { + refreshPullRequest.classList.add('active'); + } + } catch (error) { + console.error(error, event); + } +} + +export function initRepoRefreshPullRequest() { + const staleBranchAlert = $('.refresh-pull-request'); + + if (!staleBranchAlert.length) { + return; + } + + $(staleBranchAlert).on('click', () => { + window.location.reload(); + }); + + const worker = getMemoizedSharedWorker(); + + if (worker) { + worker.port.addEventListener('message', (event) => { + if (!event.data || !event.data.type) { + return; + } + if (event.data.type === 'branch-update') { + receiveBranchUpdated(event.data); + } + }); + } else { + console.error('Service workers not available'); + } +} diff --git a/web_src/js/features/shared-worker.js b/web_src/js/features/shared-worker.js new file mode 100644 index 0000000000000..4ea51cdce26ca --- /dev/null +++ b/web_src/js/features/shared-worker.js @@ -0,0 +1,70 @@ +const {appSubUrl} = window.config; + +let worker; + +function getMemoizedSharedWorker() { + if (!window.EventSource || !window.SharedWorker) { + return null; + } + + if (!worker) { + worker = new SharedWorker( + `${__webpack_public_path__}js/eventsource.sharedworker.js`, + 'notification-worker' + ); + + worker.addEventListener('error', (event) => { + console.error(event); + }); + + worker.port.addEventListener('messageerror', () => { + console.error('Unable to deserialize message'); + }); + + worker.port.postMessage({ + type: 'start', + url: `${window.location.origin}${appSubUrl}/user/events`, + }); + + worker.port.addEventListener('message', (event) => { + if (!event.data || !event.data.type) { + console.error(event); + return; + } + if (event.data.type === 'error') { + console.error(event.data); + } else if (event.data.type === 'logout') { + if (event.data.data !== 'here') { + return; + } + worker.port.postMessage({ + type: 'close', + }); + worker.port.close(); + window.location.href = appSubUrl; + } else if (event.data.type === 'close') { + worker.port.postMessage({ + type: 'close', + }); + worker.port.close(); + } + }); + + worker.port.addEventListener('error', (e) => { + console.error(e); + }); + + worker.port.start(); + + window.addEventListener('beforeunload', () => { + worker.port.postMessage({ + type: 'close', + }); + worker.port.close(); + }); + } + + return worker; +} + +export default getMemoizedSharedWorker; diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js index d63da4155af27..e0a2f8dd1fe98 100644 --- a/web_src/js/features/stopwatch.js +++ b/web_src/js/features/stopwatch.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import prettyMilliseconds from 'pretty-ms'; +import getMemoizedSharedWorker from './shared-worker.js'; const {appSubUrl, csrfToken, notificationSettings, enableTimeTracking} = window.config; let updateTimeInterval = null; // holds setInterval id when active @@ -26,54 +27,18 @@ export function initStopwatch() { $(this).parent().trigger('submit'); }); - if (notificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource && window.SharedWorker) { + let worker; + + if (notificationSettings.EventSourceUpdateTime > 0 && (worker = getMemoizedSharedWorker())) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js`, 'notification-worker'); - worker.addEventListener('error', (event) => { - console.error(event); - }); - worker.port.addEventListener('messageerror', () => { - console.error('Unable to deserialize message'); - }); - worker.port.postMessage({ - type: 'start', - url: `${window.location.origin}${appSubUrl}/user/events`, - }); worker.port.addEventListener('message', (event) => { if (!event.data || !event.data.type) { - console.error(event); return; } if (event.data.type === 'stopwatches') { updateStopwatchData(JSON.parse(event.data.data)); - } else if (event.data.type === 'error') { - console.error(event.data); - } else if (event.data.type === 'logout') { - if (event.data.data !== 'here') { - return; - } - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - window.location.href = appSubUrl; - } else if (event.data.type === 'close') { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); } }); - worker.port.addEventListener('error', (e) => { - console.error(e); - }); - worker.port.start(); - window.addEventListener('beforeunload', () => { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - }); return; } diff --git a/web_src/js/index.js b/web_src/js/index.js index 6f872b5353378..24685cc4512b1 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -82,6 +82,7 @@ import {initInstall} from './features/install.js'; import {initCompWebHookEditor} from './features/comp/WebHookEditor.js'; import {initCommonIssue} from './features/common-issue.js'; import {initRepoBranchButton} from './features/repo-branch.js'; +import {initRepoRefreshPullRequest} from './features/repo-refresh-pr.js'; import {initCommonOrganization} from './features/common-organization.js'; import {initRepoWikiForm} from './features/repo-wiki.js'; import {initRepoCommentForm, initRepository} from './features/repo-legacy.js'; @@ -176,6 +177,7 @@ $(document).ready(() => { initRepoSettingGitHook(); initRepoSettingSearchTeamBox(); initRepoSettingsCollaboration(); + initRepoRefreshPullRequest(); initRepoTemplateSearch(); initRepoTopicBar(); initRepoWikiForm(); diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 5aed4dcf72358..6f6ea750d9b98 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -2572,6 +2572,14 @@ display: none; } +.refresh-pull-request { + display: none; + + &.active { + display: inherit; + } +} + .ui.menu .item > img:not(.ui) { width: auto; }