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;
}