
* stable-3.0: Update git submodules Update git submodules Update git submodules Fix documentation of UI selection Set version to 2.16.12-SNAPSHOT Set version to 2.16.11 Update git submodules Revert "GerritServer: Silence non-critical logs from JGit's FileSnapshot" Revert "GerritServer: Silence non-critical logs from JGit's FS" Revert "Upgrade JGit to 5.1.10.201908230655-r" Revert "Stop using deprecated DirCacheEntry#setLastModified(long)" Update git submodules Update git submodules Update git submodules Update git submodules Update git submodules Update git submodules Set version to 2.15.17-SNAPSHOT Update git submodules Update git submodules Revert "Upgrade JGit to 5.3.4.201908231101-r" Set version to 2.15.16 Revert "CreateAccount: Fail early when invalid SSH key is given" Update git submodules tools/eclipse/project.py: Fix typo of bazelisk Set XSRF on '/' under PolyGerrit Migrate from old-style legacy .java provider to the new JavaInfo. Remove deleted rules for the new added section Support bazelisk or bazel in tools/eclipse/project.py Rework imports in project.py Update project.py to use argparse AccountIT: Test that account is not created with invalid email CreateAccount: Fail early when invalid SSH key is given Update git submodules GerritServer: Silence non-critical logs from JGit's FS GerritServer: Silence non-critical logs from JGit's FileSnapshot Stop using deprecated DirCacheEntry#setLastModified(long) ProjectState: Fix 'invalid type qualification' javadoc warning Upgrade JGit to 5.3.4.201908231101-r Documentation: refresh IntelliJ IDEA developer documentation Documentation: update external links to Bazel Use base url for commentlink Update git submodules Update git submodules Don't store LabelTypes in ProjectState Upgrade highlight.js to latest master revision Update git submodules Update git submodules Update git submodules StarredChangesUtil: Stop using deprecated RefDatabase.getRef Suppress warnings about deprecated BaseReceivePack DeleteRef: Stop using deprecated RefDatabase.getRef Upgrade elasticsearch-rest-client to 7.3.1 Add .gitreview file Remove vestigal GWT plugin loading hooks Upgrade JGit to 5.1.10.201908230655-r DeleteDraftComments: Don't update change modified timestamp ChangeIT: set submittableAfterLosingPermissions private Rebase: Don't swallow caught exception Output NoteDb migration progress to Flogger Update git submodules Remove AccountPatchReview data when change gets auto-abandoned DefaultChangeReportFormatter: Make constructor and urlFormatter visible StarredChangesUtil: Fix NPE when ref to be deleted doesn't exist StarredChangesUtil: Throw LockFailureException on LOCK_FAILURE Add test for creating a change on a non-existing base change Rebase: Do not fail with 500 ISE if non-existing change is given as base Fix detecting changes of parent trees when computing change kind for merge commit Remove duplicate descriptions of fields in Requirement JSON entity Allow to set content type in the plugin REST API interface InternalAccountQuery: Add back the oneByExternalId method Set version to 2.16.11-SNAPSHOT Revert "Migrate from old-style legacy .java provider to the new JavaInfo." Catch all exceptions for reporting on Schema_130 migration Update git submodules Migrate from old-style legacy .java provider to the new JavaInfo. Update git submodules Update git submodules AuthRequest: Fix Javadoc for return values ChangeApi: Add methods to get change comments/drafts as list Update git submodules ListChangeComments: Extend ListChangeDrafts Upgrade Go Bazel rules to the latest version detach -> detached Fix anchor tag for settings page Add support for Elasticsearch version 7.3.* PrologEnvironment: Reduce "setting reductionLimit" log spam ElasticContainer: Upgrade to 6.8.2 image for V6_8 tests Fix typo: program Fix email token routing Clarify usage of 'parent' option in list files API Remove unused Skylark patch file Files: Use Gerrit API to get revision parents Fix broken link for rest-api-projects.html#commentlink-info Add support for Elasticsearch version 6.8.x Upgrade elasticsearch-rest-client to 7.2.1 Add support for "Link Another Identity" in gr-identities Update git submodules CommitApi: Add method to get commit info Consolidate all CommitApi tests into a single class Files: Validate parent option to prevent internal server error RevisionIT: Assert that files(base) only works for patch set revisions Fix and expand documentation of REST API to get revision files RevisionIT#files: Simplify assertion Update git submodules Update git submodules Update git submodules Update git submodules Remove default bug tracker from _feedbackUrl PG: Add shortcuts for dashboard and watched changes PG: Allow empty label values Remove token param from getCapabilities Add an extension point to show a small banner next to the search bar Fix gr-group-audit-log to use tbody Change-Id: Iaf877b737f115d4fe95af74f8284eec19112a21d
1521 lines
47 KiB
JavaScript
1521 lines
47 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright (C) 2016 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
const RoutePattern = {
|
|
ROOT: '/',
|
|
|
|
DASHBOARD: /^\/dashboard\/(.+)$/,
|
|
CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
|
|
PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
|
|
|
|
AGREEMENTS: /^\/settings\/agreements\/?/,
|
|
NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/,
|
|
REGISTER: /^\/register(\/.*)?$/,
|
|
|
|
// Pattern for login and logout URLs intended to be passed-through. May
|
|
// include a return URL.
|
|
LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/,
|
|
|
|
// Pattern for a catchall route when no other pattern is matched.
|
|
DEFAULT: /.*/,
|
|
|
|
// Matches /admin/groups/[uuid-]<group>
|
|
GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/,
|
|
|
|
// Redirects /groups/self to /settings/#Groups for GWT compatibility
|
|
GROUP_SELF: /^\/groups\/self/,
|
|
|
|
// Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui)
|
|
// Redirects to /admin/groups/[uuid-]<group>
|
|
GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/,
|
|
|
|
// Matches /admin/groups/<group>,audit-log
|
|
GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/,
|
|
|
|
// Matches /admin/groups/[uuid-]<group>,members
|
|
GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/,
|
|
|
|
// Matches /admin/groups[,<offset>][/].
|
|
GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/,
|
|
GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
|
|
GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
|
|
|
|
// Matches /admin/create-project
|
|
LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/,
|
|
|
|
// Matches /admin/create-project
|
|
LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/,
|
|
|
|
PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/,
|
|
|
|
// Matches /admin/repos/<repo>
|
|
REPO: /^\/admin\/repos\/([^,]+)$/,
|
|
|
|
// Matches /admin/repos/<repo>,commands.
|
|
REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/,
|
|
|
|
// Matches /admin/repos/<repos>,access.
|
|
REPO_ACCESS: /^\/admin\/repos\/(.+),access$/,
|
|
|
|
// Matches /admin/repos/<repos>,access.
|
|
REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
|
|
|
|
// Matches /admin/repos[,<offset>][/].
|
|
REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/,
|
|
REPO_LIST_FILTER: '/admin/repos/q/filter::filter',
|
|
REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset',
|
|
|
|
// Matches /admin/repos/<repo>,branches[,<offset>].
|
|
BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/,
|
|
BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter',
|
|
BRANCH_LIST_FILTER_OFFSET:
|
|
'/admin/repos/:repo,branches/q/filter::filter,:offset',
|
|
|
|
// Matches /admin/repos/<repo>,tags[,<offset>].
|
|
TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/,
|
|
TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter',
|
|
TAG_LIST_FILTER_OFFSET:
|
|
'/admin/repos/:repo,tags/q/filter::filter,:offset',
|
|
|
|
PLUGINS: /^\/plugins\/(.+)$/,
|
|
|
|
PLUGIN_LIST: /^\/admin\/plugins(\/)?$/,
|
|
|
|
// Matches /admin/plugins[,<offset>][/].
|
|
PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/,
|
|
PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter',
|
|
PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset',
|
|
|
|
QUERY: /^\/q\/([^,]+)(,(\d+))?$/,
|
|
|
|
/**
|
|
* Support vestigial params from GWT UI.
|
|
* @see Issue 7673.
|
|
* @type {!RegExp}
|
|
*/
|
|
QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/,
|
|
|
|
// Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
|
|
CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
|
|
CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
|
|
|
|
// Matches
|
|
// /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>].
|
|
// TODO(kaspern): Migrate completely to project based URLs, with backwards
|
|
// compatibility for change-only.
|
|
CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
|
|
|
|
// Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit
|
|
CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/,
|
|
|
|
// Matches
|
|
// /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>.
|
|
// TODO(kaspern): Migrate completely to project based URLs, with backwards
|
|
// compatibility for change-only.
|
|
// eslint-disable-next-line max-len
|
|
DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
|
|
|
|
// Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit
|
|
DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit$/,
|
|
|
|
// Matches non-project-relative
|
|
// /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
|
|
DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/,
|
|
|
|
// Matches diff routes using @\d+ to specify a file name (whether or not
|
|
// the project name is included).
|
|
// eslint-disable-next-line max-len
|
|
DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/,
|
|
|
|
SETTINGS: /^\/settings\/?/,
|
|
SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/,
|
|
|
|
// Matches /c/<changeNum>/ /<URL tail>
|
|
// Catches improperly encoded URLs (context: Issue 7100)
|
|
IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
|
|
|
|
PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
|
|
|
|
DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter',
|
|
DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/,
|
|
DOCUMENTATION: /^\/Documentation(\/)?(.+)?/,
|
|
};
|
|
|
|
/**
|
|
* Pattern to recognize and parse the diff line locations as they appear in
|
|
* the hash of diff URLs. In this format, a number on its own indicates that
|
|
* line number in the revision of the diff. A number prefixed by either an 'a'
|
|
* or a 'b' indicates that line number of the base of the diff.
|
|
* @type {RegExp}
|
|
*/
|
|
const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/;
|
|
|
|
/**
|
|
* Pattern to recognize '+' in url-encoded strings for replacement with ' '.
|
|
*/
|
|
const PLUS_PATTERN = /\+/g;
|
|
|
|
/**
|
|
* Pattern to recognize leading '?' in window.location.search, for stripping.
|
|
*/
|
|
const QUESTION_PATTERN = /^\?*/;
|
|
|
|
/**
|
|
* GWT UI would use @\d+ at the end of a path to indicate linenum.
|
|
*/
|
|
const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/;
|
|
|
|
const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/;
|
|
|
|
const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g;
|
|
|
|
// Polymer makes `app` intrinsically defined on the window by virtue of the
|
|
// custom element having the id "app", but it is made explicit here.
|
|
const app = document.querySelector('#app');
|
|
if (!app) {
|
|
console.log('No gr-app found (running tests)');
|
|
}
|
|
|
|
// Setup listeners outside of the router component initialization.
|
|
(function() {
|
|
const reporting = document.createElement('gr-reporting');
|
|
|
|
window.addEventListener('load', () => {
|
|
setTimeout(() => {
|
|
reporting.pageLoaded();
|
|
}, 0);
|
|
});
|
|
|
|
window.addEventListener('WebComponentsReady', () => {
|
|
reporting.timeEnd('WebComponentsReady');
|
|
});
|
|
})();
|
|
|
|
Polymer({
|
|
is: 'gr-router',
|
|
_legacyUndefinedCheck: true,
|
|
|
|
properties: {
|
|
_app: {
|
|
type: Object,
|
|
value: app,
|
|
},
|
|
_isRedirecting: Boolean,
|
|
// This variable is to differentiate between internal navigation (false)
|
|
// and for first navigation in app after loaded from server (true).
|
|
_isInitialLoad: {
|
|
type: Boolean,
|
|
value: true,
|
|
},
|
|
},
|
|
|
|
behaviors: [
|
|
Gerrit.BaseUrlBehavior,
|
|
Gerrit.FireBehavior,
|
|
Gerrit.PatchSetBehavior,
|
|
Gerrit.URLEncodingBehavior,
|
|
],
|
|
|
|
start() {
|
|
if (!this._app) { return; }
|
|
this._startRouter();
|
|
},
|
|
|
|
_setParams(params) {
|
|
this._appElement().params = params;
|
|
},
|
|
|
|
_appElement() {
|
|
// In Polymer2 you have to reach through the shadow root of the app
|
|
// element. This obviously breaks encapsulation.
|
|
// TODO(brohlfs): Make this more elegant, e.g. by exposing app-element
|
|
// explicitly in app, or by delegating to it.
|
|
return document.getElementById('app-element') ||
|
|
document.getElementById('app').shadowRoot.getElementById(
|
|
'app-element');
|
|
},
|
|
|
|
_redirect(url) {
|
|
this._isRedirecting = true;
|
|
page.redirect(url);
|
|
},
|
|
|
|
/**
|
|
* @param {!Object} params
|
|
* @return {string}
|
|
*/
|
|
_generateUrl(params) {
|
|
const base = this.getBaseUrl();
|
|
let url = '';
|
|
const Views = Gerrit.Nav.View;
|
|
|
|
if (params.view === Views.SEARCH) {
|
|
url = this._generateSearchUrl(params);
|
|
} else if (params.view === Views.CHANGE) {
|
|
url = this._generateChangeUrl(params);
|
|
} else if (params.view === Views.DASHBOARD) {
|
|
url = this._generateDashboardUrl(params);
|
|
} else if (params.view === Views.DIFF || params.view === Views.EDIT) {
|
|
url = this._generateDiffOrEditUrl(params);
|
|
} else if (params.view === Views.GROUP) {
|
|
url = this._generateGroupUrl(params);
|
|
} else if (params.view === Views.REPO) {
|
|
url = this._generateRepoUrl(params);
|
|
} else if (params.view === Views.ROOT) {
|
|
url = '/';
|
|
} else if (params.view === Views.SETTINGS) {
|
|
url = this._generateSettingsUrl(params);
|
|
} else {
|
|
throw new Error('Can\'t generate');
|
|
}
|
|
|
|
return base + url;
|
|
},
|
|
|
|
_generateWeblinks(params) {
|
|
const type = params.type;
|
|
switch (type) {
|
|
case Gerrit.Nav.WeblinkType.FILE:
|
|
return this._getFileWebLinks(params);
|
|
case Gerrit.Nav.WeblinkType.CHANGE:
|
|
return this._getChangeWeblinks(params);
|
|
case Gerrit.Nav.WeblinkType.PATCHSET:
|
|
return this._getPatchSetWeblink(params);
|
|
default:
|
|
console.warn(`Unsupported weblink ${type}!`);
|
|
}
|
|
},
|
|
|
|
_getPatchSetWeblink(params) {
|
|
const {commit, options} = params;
|
|
const {weblinks, config} = options || {};
|
|
const name = commit && commit.slice(0, 7);
|
|
const weblink = this._getBrowseCommitWeblink(weblinks, config);
|
|
if (!weblink || !weblink.url) {
|
|
return {name};
|
|
} else {
|
|
return {name, url: weblink.url};
|
|
}
|
|
},
|
|
|
|
_firstCodeBrowserWeblink(weblinks) {
|
|
// This is an ordered whitelist of web link types that provide direct
|
|
// links to the commit in the url property.
|
|
const codeBrowserLinks = ['gitiles', 'browse', 'gitweb'];
|
|
for (let i = 0; i < codeBrowserLinks.length; i++) {
|
|
const weblink =
|
|
weblinks.find(weblink => weblink.name === codeBrowserLinks[i]);
|
|
if (weblink) { return weblink; }
|
|
}
|
|
return null;
|
|
},
|
|
|
|
|
|
_getBrowseCommitWeblink(weblinks, config) {
|
|
if (!weblinks) { return null; }
|
|
let weblink;
|
|
// Use primary weblink if configured and exists.
|
|
if (config && config.gerrit && config.gerrit.primary_weblink_name) {
|
|
weblink = weblinks.find(
|
|
weblink => weblink.name === config.gerrit.primary_weblink_name
|
|
);
|
|
}
|
|
if (!weblink) {
|
|
weblink = this._firstCodeBrowserWeblink(weblinks);
|
|
}
|
|
if (!weblink) { return null; }
|
|
return weblink;
|
|
},
|
|
|
|
_getChangeWeblinks({repo, commit, options: {weblinks, config}}) {
|
|
if (!weblinks || !weblinks.length) return [];
|
|
const commitWeblink = this._getBrowseCommitWeblink(weblinks, config);
|
|
return weblinks.filter(weblink =>
|
|
!commitWeblink ||
|
|
!commitWeblink.name ||
|
|
weblink.name !== commitWeblink.name);
|
|
},
|
|
|
|
_getFileWebLinks({repo, commit, file, options: {weblinks}}) {
|
|
return weblinks;
|
|
},
|
|
|
|
/**
|
|
* @param {!Object} params
|
|
* @return {string}
|
|
*/
|
|
_generateSearchUrl(params) {
|
|
let offsetExpr = '';
|
|
if (params.offset && params.offset > 0) {
|
|
offsetExpr = ',' + params.offset;
|
|
}
|
|
|
|
if (params.query) {
|
|
return '/q/' + this.encodeURL(params.query, true) + offsetExpr;
|
|
}
|
|
|
|
const operators = [];
|
|
if (params.owner) {
|
|
operators.push('owner:' + this.encodeURL(params.owner, false));
|
|
}
|
|
if (params.project) {
|
|
operators.push('project:' + this.encodeURL(params.project, false));
|
|
}
|
|
if (params.branch) {
|
|
operators.push('branch:' + this.encodeURL(params.branch, false));
|
|
}
|
|
if (params.topic) {
|
|
operators.push('topic:"' + this.encodeURL(params.topic, false) + '"');
|
|
}
|
|
if (params.hashtag) {
|
|
operators.push('hashtag:"' +
|
|
this.encodeURL(params.hashtag.toLowerCase(), false) + '"');
|
|
}
|
|
if (params.statuses) {
|
|
if (params.statuses.length === 1) {
|
|
operators.push(
|
|
'status:' + this.encodeURL(params.statuses[0], false));
|
|
} else if (params.statuses.length > 1) {
|
|
operators.push(
|
|
'(' +
|
|
params.statuses.map(s => `status:${this.encodeURL(s, false)}`)
|
|
.join(' OR ') +
|
|
')');
|
|
}
|
|
}
|
|
|
|
return '/q/' + operators.join('+') + offsetExpr;
|
|
},
|
|
|
|
/**
|
|
* @param {!Object} params
|
|
* @return {string}
|
|
*/
|
|
_generateChangeUrl(params) {
|
|
let range = this._getPatchRangeExpression(params);
|
|
if (range.length) { range = '/' + range; }
|
|
let suffix = `${range}`;
|
|
if (params.querystring) {
|
|
suffix += '?' + params.querystring;
|
|
} else if (params.edit) {
|
|
suffix += ',edit';
|
|
}
|
|
if (params.messageHash) {
|
|
suffix += params.messageHash;
|
|
}
|
|
if (params.project) {
|
|
const encodedProject = this.encodeURL(params.project, true);
|
|
return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
|
|
} else {
|
|
return `/c/${params.changeNum}${suffix}`;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {!Object} params
|
|
* @return {string}
|
|
*/
|
|
_generateDashboardUrl(params) {
|
|
const repoName = params.repo || params.project || null;
|
|
if (params.sections) {
|
|
// Custom dashboard.
|
|
const queryParams = this._sectionsToEncodedParams(params.sections,
|
|
repoName);
|
|
if (params.title) {
|
|
queryParams.push('title=' + encodeURIComponent(params.title));
|
|
}
|
|
const user = params.user ? params.user : '';
|
|
return `/dashboard/${user}?${queryParams.join('&')}`;
|
|
} else if (repoName) {
|
|
// Project dashboard.
|
|
const encodedRepo = this.encodeURL(repoName, true);
|
|
return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`;
|
|
} else {
|
|
// User dashboard.
|
|
return `/dashboard/${params.user || 'self'}`;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {!Array<!{name: string, query: string}>} sections
|
|
* @param {string=} opt_repoName
|
|
* @return {!Array<string>}
|
|
*/
|
|
_sectionsToEncodedParams(sections, opt_repoName) {
|
|
return sections.map(section => {
|
|
// If there is a repo name provided, make sure to substitute it into the
|
|
// ${repo} (or legacy ${project}) query tokens.
|
|
const query = opt_repoName ?
|
|
section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) :
|
|
section.query;
|
|
return encodeURIComponent(section.name) + '=' +
|
|
encodeURIComponent(query);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @param {!Object} params
|
|
* @return {string}
|
|
*/
|
|
_generateDiffOrEditUrl(params) {
|
|
let range = this._getPatchRangeExpression(params);
|
|
if (range.length) { range = '/' + range; }
|
|
|
|
let suffix = `${range}/${this.encodeURL(params.path, true)}`;
|
|
|
|
if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; }
|
|
|
|
if (params.lineNum) {
|
|
suffix += '#';
|
|
if (params.leftSide) { suffix += 'b'; }
|
|
suffix += params.lineNum;
|
|
}
|
|
|
|
if (params.project) {
|
|
const encodedProject = this.encodeURL(params.project, true);
|
|
return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
|
|
} else {
|
|
return `/c/${params.changeNum}${suffix}`;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {!Object} params
|
|
* @return {string}
|
|
*/
|
|
_generateGroupUrl(params) {
|
|
let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`;
|
|
if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) {
|
|
url += ',members';
|
|
} else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) {
|
|
url += ',audit-log';
|
|
}
|
|
return url;
|
|
},
|
|
|
|
/**
|
|
* @param {!Object} params
|
|
* @return {string}
|
|
*/
|
|
_generateRepoUrl(params) {
|
|
let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`;
|
|
if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) {
|
|
url += ',access';
|
|
} else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) {
|
|
url += ',branches';
|
|
} else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) {
|
|
url += ',tags';
|
|
} else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) {
|
|
url += ',commands';
|
|
} else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) {
|
|
url += ',dashboards';
|
|
}
|
|
return url;
|
|
},
|
|
|
|
/**
|
|
* @param {!Object} params
|
|
* @return {string}
|
|
*/
|
|
_generateSettingsUrl(params) {
|
|
return '/settings';
|
|
},
|
|
|
|
/**
|
|
* Given an object of parameters, potentially including a `patchNum` or a
|
|
* `basePatchNum` or both, return a string representation of that range. If
|
|
* no range is indicated in the params, the empty string is returned.
|
|
* @param {!Object} params
|
|
* @return {string}
|
|
*/
|
|
_getPatchRangeExpression(params) {
|
|
let range = '';
|
|
if (params.patchNum) { range = '' + params.patchNum; }
|
|
if (params.basePatchNum) { range = params.basePatchNum + '..' + range; }
|
|
return range;
|
|
},
|
|
|
|
/**
|
|
* Given a set of params without a project, gets the project from the rest
|
|
* API project lookup and then sets the app params.
|
|
*
|
|
* @param {?Object} params
|
|
*/
|
|
_normalizeLegacyRouteParams(params) {
|
|
if (!params.changeNum) { return Promise.resolve(); }
|
|
|
|
return this.$.restAPI.getFromProjectLookup(params.changeNum)
|
|
.then(project => {
|
|
// Show a 404 and terminate if the lookup request failed. Attempting
|
|
// to redirect after failing to get the project loops infinitely.
|
|
if (!project) {
|
|
this._show404();
|
|
return;
|
|
}
|
|
|
|
params.project = project;
|
|
this._normalizePatchRangeParams(params);
|
|
this._redirect(this._generateUrl(params));
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Normalizes the params object, and determines if the URL needs to be
|
|
* modified to fit the proper schema.
|
|
*
|
|
* @param {*} params
|
|
* @return {boolean} whether or not the URL needs to be upgraded.
|
|
*/
|
|
_normalizePatchRangeParams(params) {
|
|
const hasBasePatchNum = params.basePatchNum !== null &&
|
|
params.basePatchNum !== undefined;
|
|
const hasPatchNum = params.patchNum !== null &&
|
|
params.patchNum !== undefined;
|
|
let needsRedirect = false;
|
|
|
|
// Diffing a patch against itself is invalid, so if the base and revision
|
|
// patches are equal clear the base.
|
|
if (hasBasePatchNum &&
|
|
this.patchNumEquals(params.basePatchNum, params.patchNum)) {
|
|
needsRedirect = true;
|
|
params.basePatchNum = null;
|
|
} else if (hasBasePatchNum && !hasPatchNum) {
|
|
// Regexes set basePatchNum instead of patchNum when only one is
|
|
// specified. Redirect is not needed in this case.
|
|
params.patchNum = params.basePatchNum;
|
|
params.basePatchNum = null;
|
|
}
|
|
// In GWTUI, edits are represented in URLs with either 0 or 'edit'.
|
|
// TODO(kaspern): Remove this normalization when GWT UI is gone.
|
|
if (this.patchNumEquals(params.basePatchNum, 0)) {
|
|
params.basePatchNum = this.EDIT_NAME;
|
|
needsRedirect = true;
|
|
}
|
|
if (this.patchNumEquals(params.patchNum, 0)) {
|
|
params.patchNum = this.EDIT_NAME;
|
|
needsRedirect = true;
|
|
}
|
|
return needsRedirect;
|
|
},
|
|
|
|
/**
|
|
* Redirect the user to login using the given return-URL for redirection
|
|
* after authentication success.
|
|
* @param {string} returnUrl
|
|
*/
|
|
_redirectToLogin(returnUrl) {
|
|
const basePath = this.getBaseUrl() || '';
|
|
page(
|
|
'/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
|
|
},
|
|
|
|
/**
|
|
* Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c"
|
|
* is parsed to have a hash of "b" rather than "b#c". Instead, this method
|
|
* parses hashes correctly. Will return an empty string if there is no hash.
|
|
* @param {!string} canonicalPath
|
|
* @return {!string} Everything after the first '#' ("a#b#c" -> "b#c").
|
|
*/
|
|
_getHashFromCanonicalPath(canonicalPath) {
|
|
return canonicalPath.split('#').slice(1).join('#');
|
|
},
|
|
|
|
_parseLineAddress(hash) {
|
|
const match = hash.match(LINE_ADDRESS_PATTERN);
|
|
if (!match) { return null; }
|
|
return {
|
|
leftSide: !!match[1],
|
|
lineNum: parseInt(match[2], 10),
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Check to see if the user is logged in and return a promise that only
|
|
* resolves if the user is logged in. If the user us not logged in, the
|
|
* promise is rejected and the page is redirected to the login flow.
|
|
* @param {!Object} data The parsed route data.
|
|
* @return {!Promise<!Object>} A promise yielding the original route data
|
|
* (if it resolves).
|
|
*/
|
|
_redirectIfNotLoggedIn(data) {
|
|
return this.$.restAPI.getLoggedIn().then(loggedIn => {
|
|
if (loggedIn) {
|
|
return Promise.resolve();
|
|
} else {
|
|
this._redirectToLogin(data.canonicalPath);
|
|
return Promise.reject(new Error());
|
|
}
|
|
});
|
|
},
|
|
|
|
/** Page.js middleware that warms the REST API's logged-in cache line. */
|
|
_loadUserMiddleware(ctx, next) {
|
|
this.$.restAPI.getLoggedIn().then(() => { next(); });
|
|
},
|
|
|
|
/**
|
|
* Map a route to a method on the router.
|
|
*
|
|
* @param {!string|!RegExp} pattern The page.js pattern for the route.
|
|
* @param {!string} handlerName The method name for the handler. If the
|
|
* route is matched, the handler will be executed with `this` referring
|
|
* to the component. Its return value will be discarded so that it does
|
|
* not interfere with page.js.
|
|
* @param {?boolean=} opt_authRedirect If true, then auth is checked before
|
|
* executing the handler. If the user is not logged in, it will redirect
|
|
* to the login flow and the handler will not be executed. The login
|
|
* redirect specifies the matched URL to be used after successfull auth.
|
|
*/
|
|
_mapRoute(pattern, handlerName, opt_authRedirect) {
|
|
if (!this[handlerName]) {
|
|
console.error('Attempted to map route to unknown method: ',
|
|
handlerName);
|
|
return;
|
|
}
|
|
page(pattern, this._loadUserMiddleware.bind(this), data => {
|
|
this.$.reporting.locationChanged(handlerName);
|
|
const promise = opt_authRedirect ?
|
|
this._redirectIfNotLoggedIn(data) : Promise.resolve();
|
|
promise.then(() => { this[handlerName](data); });
|
|
});
|
|
},
|
|
|
|
_startRouter() {
|
|
const base = this.getBaseUrl();
|
|
if (base) {
|
|
page.base(base);
|
|
}
|
|
|
|
Gerrit.Nav.setup(
|
|
url => { page.show(url); },
|
|
this._generateUrl.bind(this),
|
|
params => this._generateWeblinks(params),
|
|
x => x
|
|
);
|
|
|
|
page.exit('*', (ctx, next) => {
|
|
if (!this._isRedirecting) {
|
|
this.$.reporting.beforeLocationChanged();
|
|
}
|
|
this._isRedirecting = false;
|
|
this._isInitialLoad = false;
|
|
next();
|
|
});
|
|
|
|
// Middleware
|
|
page((ctx, next) => {
|
|
document.body.scrollTop = 0;
|
|
|
|
if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
|
|
// Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen
|
|
// This is needed to allow plugins to add basic #/x/ screen links to
|
|
// any location.
|
|
this._redirect(ctx.hash);
|
|
return;
|
|
}
|
|
|
|
// Fire asynchronously so that the URL is changed by the time the event
|
|
// is processed.
|
|
this.async(() => {
|
|
this.fire('location-change', {
|
|
hash: window.location.hash,
|
|
pathname: window.location.pathname,
|
|
});
|
|
}, 1);
|
|
next();
|
|
});
|
|
|
|
this._mapRoute(RoutePattern.ROOT, '_handleRootRoute');
|
|
|
|
this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute');
|
|
|
|
this._mapRoute(RoutePattern.CUSTOM_DASHBOARD,
|
|
'_handleCustomDashboardRoute');
|
|
|
|
this._mapRoute(RoutePattern.PROJECT_DASHBOARD,
|
|
'_handleProjectDashboardRoute');
|
|
|
|
this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute',
|
|
true);
|
|
|
|
this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute',
|
|
true);
|
|
|
|
this._mapRoute(RoutePattern.GROUP_LIST_OFFSET,
|
|
'_handleGroupListOffsetRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET,
|
|
'_handleGroupListFilterOffsetRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.GROUP_LIST_FILTER,
|
|
'_handleGroupListFilterRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute',
|
|
true);
|
|
|
|
this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.PROJECT_OLD,
|
|
'_handleProjectsOldRoute');
|
|
|
|
this._mapRoute(RoutePattern.REPO_COMMANDS,
|
|
'_handleRepoCommandsRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.REPO_ACCESS,
|
|
'_handleRepoAccessRoute');
|
|
|
|
this._mapRoute(RoutePattern.REPO_DASHBOARDS,
|
|
'_handleRepoDashboardsRoute');
|
|
|
|
this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET,
|
|
'_handleBranchListOffsetRoute');
|
|
|
|
this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET,
|
|
'_handleBranchListFilterOffsetRoute');
|
|
|
|
this._mapRoute(RoutePattern.BRANCH_LIST_FILTER,
|
|
'_handleBranchListFilterRoute');
|
|
|
|
this._mapRoute(RoutePattern.TAG_LIST_OFFSET,
|
|
'_handleTagListOffsetRoute');
|
|
|
|
this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET,
|
|
'_handleTagListFilterOffsetRoute');
|
|
|
|
this._mapRoute(RoutePattern.TAG_LIST_FILTER,
|
|
'_handleTagListFilterRoute');
|
|
|
|
this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP,
|
|
'_handleCreateGroupRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT,
|
|
'_handleCreateProjectRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.REPO_LIST_OFFSET,
|
|
'_handleRepoListOffsetRoute');
|
|
|
|
this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET,
|
|
'_handleRepoListFilterOffsetRoute');
|
|
|
|
this._mapRoute(RoutePattern.REPO_LIST_FILTER,
|
|
'_handleRepoListFilterRoute');
|
|
|
|
this._mapRoute(RoutePattern.REPO, '_handleRepoRoute');
|
|
|
|
this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
|
|
|
|
this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET,
|
|
'_handlePluginListOffsetRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET,
|
|
'_handlePluginListFilterOffsetRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER,
|
|
'_handlePluginListFilterRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX,
|
|
'_handleQueryLegacySuffixRoute');
|
|
|
|
this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
|
|
|
|
this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum');
|
|
|
|
this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY,
|
|
'_handleChangeNumberLegacyRoute');
|
|
|
|
this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute');
|
|
|
|
this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute');
|
|
|
|
this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
|
|
|
|
this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute');
|
|
|
|
this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute',
|
|
true);
|
|
|
|
this._mapRoute(RoutePattern.SETTINGS_LEGACY,
|
|
'_handleSettingsLegacyRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true);
|
|
|
|
this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute');
|
|
|
|
this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute');
|
|
|
|
this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS,
|
|
'_handleImproperlyEncodedPlusRoute');
|
|
|
|
this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
|
|
|
|
this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER,
|
|
'_handleDocumentationSearchRoute');
|
|
|
|
// redirects /Documentation/q/* to /Documentation/q/filter:*
|
|
this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH,
|
|
'_handleDocumentationSearchRedirectRoute');
|
|
|
|
// Makes sure /Documentation/* links work (doin't return 404)
|
|
this._mapRoute(RoutePattern.DOCUMENTATION,
|
|
'_handleDocumentationRedirectRoute');
|
|
|
|
// Note: this route should appear last so it only catches URLs unmatched
|
|
// by other patterns.
|
|
this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
|
|
|
|
page.start();
|
|
},
|
|
|
|
/**
|
|
* @param {!Object} data
|
|
* @return {Promise|null} if handling the route involves asynchrony, then a
|
|
* promise is returned. Otherwise, synchronous handling returns null.
|
|
*/
|
|
_handleRootRoute(data) {
|
|
if (data.querystring.match(/^closeAfterLogin/)) {
|
|
// Close child window on redirect after login.
|
|
window.close();
|
|
return null;
|
|
}
|
|
let hash = this._getHashFromCanonicalPath(data.canonicalPath);
|
|
// For backward compatibility with GWT links.
|
|
if (hash) {
|
|
// In certain login flows the server may redirect to a hash without
|
|
// a leading slash, which page.js doesn't handle correctly.
|
|
if (hash[0] !== '/') {
|
|
hash = '/' + hash;
|
|
}
|
|
if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) {
|
|
// Path decodes all '+' to ' ' -- this breaks project-based URLs.
|
|
// See Issue 6888.
|
|
hash = hash.replace('/ /', '/+/');
|
|
}
|
|
const base = this.getBaseUrl();
|
|
let newUrl = base + hash;
|
|
if (hash.startsWith('/VE/')) {
|
|
newUrl = base + '/settings' + hash;
|
|
}
|
|
this._redirect(newUrl);
|
|
return null;
|
|
}
|
|
return this.$.restAPI.getLoggedIn().then(loggedIn => {
|
|
if (loggedIn) {
|
|
this._redirect('/dashboard/self');
|
|
} else {
|
|
this._redirect('/q/status:open');
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Decode an application/x-www-form-urlencoded string.
|
|
*
|
|
* @param {string} qs The application/x-www-form-urlencoded string.
|
|
* @return {string} The decoded string.
|
|
*/
|
|
_decodeQueryString(qs) {
|
|
return decodeURIComponent(qs.replace(PLUS_PATTERN, ' '));
|
|
},
|
|
|
|
/**
|
|
* Parse a query string (e.g. window.location.search) into an array of
|
|
* name/value pairs.
|
|
*
|
|
* @param {string} qs The application/x-www-form-urlencoded query string.
|
|
* @return {!Array<!Array<string>>} An array of name/value pairs, where each
|
|
* element is a 2-element array.
|
|
*/
|
|
_parseQueryString(qs) {
|
|
qs = qs.replace(QUESTION_PATTERN, '');
|
|
if (!qs) {
|
|
return [];
|
|
}
|
|
const params = [];
|
|
qs.split('&').forEach(param => {
|
|
const idx = param.indexOf('=');
|
|
let name;
|
|
let value;
|
|
if (idx < 0) {
|
|
name = this._decodeQueryString(param);
|
|
value = '';
|
|
} else {
|
|
name = this._decodeQueryString(param.substring(0, idx));
|
|
value = this._decodeQueryString(param.substring(idx + 1));
|
|
}
|
|
if (name) {
|
|
params.push([name, value]);
|
|
}
|
|
});
|
|
return params;
|
|
},
|
|
|
|
/**
|
|
* Handle dashboard routes. These may be user, or project dashboards.
|
|
*
|
|
* @param {!Object} data The parsed route data.
|
|
*/
|
|
_handleDashboardRoute(data) {
|
|
// User dashboard. We require viewing user to be logged in, else we
|
|
// redirect to login for self dashboard or simple owner search for
|
|
// other user dashboard.
|
|
return this.$.restAPI.getLoggedIn().then(loggedIn => {
|
|
if (!loggedIn) {
|
|
if (data.params[0].toLowerCase() === 'self') {
|
|
this._redirectToLogin(data.canonicalPath);
|
|
} else {
|
|
this._redirect('/q/owner:' + encodeURIComponent(data.params[0]));
|
|
}
|
|
} else {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.DASHBOARD,
|
|
user: data.params[0],
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Handle custom dashboard routes.
|
|
*
|
|
* @param {!Object} data The parsed route data.
|
|
* @param {string=} opt_qs Optional query string associated with the route.
|
|
* If not given, window.location.search is used. (Used by tests).
|
|
*/
|
|
_handleCustomDashboardRoute(data, opt_qs) {
|
|
// opt_qs may be provided by a test, and it may have a falsy value
|
|
const qs = opt_qs !== undefined ? opt_qs : window.location.search;
|
|
const queryParams = this._parseQueryString(qs);
|
|
let title = 'Custom Dashboard';
|
|
const titleParam = queryParams.find(
|
|
elem => elem[0].toLowerCase() === 'title');
|
|
if (titleParam) {
|
|
title = titleParam[1];
|
|
}
|
|
// Dashboards support a foreach param which adds a base query to any
|
|
// additional query.
|
|
const forEachParam = queryParams.find(
|
|
elem => elem[0].toLowerCase() === 'foreach');
|
|
let forEachQuery = null;
|
|
if (forEachParam) {
|
|
forEachQuery = forEachParam[1];
|
|
}
|
|
const sectionParams = queryParams.filter(
|
|
elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title'
|
|
&& elem[0].toLowerCase() !== 'foreach');
|
|
const sections = sectionParams.map(elem => {
|
|
const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1];
|
|
return {
|
|
name: elem[0],
|
|
query,
|
|
};
|
|
});
|
|
|
|
if (sections.length > 0) {
|
|
// Custom dashboard view.
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.DASHBOARD,
|
|
user: 'self',
|
|
sections,
|
|
title,
|
|
});
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// Redirect /dashboard/ -> /dashboard/self.
|
|
this._redirect('/dashboard/self');
|
|
return Promise.resolve();
|
|
},
|
|
|
|
_handleProjectDashboardRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.DASHBOARD,
|
|
project: data.params[0],
|
|
dashboard: decodeURIComponent(data.params[1]),
|
|
});
|
|
},
|
|
|
|
_handleGroupInfoRoute(data) {
|
|
this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
|
|
},
|
|
|
|
_handleGroupSelfRedirectRoute(data) {
|
|
this._redirect('/settings/#Groups');
|
|
},
|
|
|
|
_handleGroupRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.GROUP,
|
|
groupId: data.params[0],
|
|
});
|
|
},
|
|
|
|
_handleGroupAuditLogRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.GROUP,
|
|
detail: Gerrit.Nav.GroupDetailView.LOG,
|
|
groupId: data.params[0],
|
|
});
|
|
},
|
|
|
|
_handleGroupMembersRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.GROUP,
|
|
detail: Gerrit.Nav.GroupDetailView.MEMBERS,
|
|
groupId: data.params[0],
|
|
});
|
|
},
|
|
|
|
_handleGroupListOffsetRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.ADMIN,
|
|
adminView: 'gr-admin-group-list',
|
|
offset: data.params[1] || 0,
|
|
filter: null,
|
|
openCreateModal: data.hash === 'create',
|
|
});
|
|
},
|
|
|
|
_handleGroupListFilterOffsetRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.ADMIN,
|
|
adminView: 'gr-admin-group-list',
|
|
offset: data.params.offset,
|
|
filter: data.params.filter,
|
|
});
|
|
},
|
|
|
|
_handleGroupListFilterRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.ADMIN,
|
|
adminView: 'gr-admin-group-list',
|
|
filter: data.params.filter || null,
|
|
});
|
|
},
|
|
|
|
_handleProjectsOldRoute(data) {
|
|
let params = '';
|
|
if (data.params[1]) {
|
|
params = encodeURIComponent(data.params[1]);
|
|
if (data.params[1].includes(',')) {
|
|
params =
|
|
encodeURIComponent(data.params[1]).replace('%2C', ',');
|
|
}
|
|
}
|
|
|
|
this._redirect(`/admin/repos/${params}`);
|
|
},
|
|
|
|
_handleRepoCommandsRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.REPO,
|
|
detail: Gerrit.Nav.RepoDetailView.COMMANDS,
|
|
repo: data.params[0],
|
|
});
|
|
},
|
|
|
|
_handleRepoAccessRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.REPO,
|
|
detail: Gerrit.Nav.RepoDetailView.ACCESS,
|
|
repo: data.params[0],
|
|
});
|
|
},
|
|
|
|
_handleRepoDashboardsRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.REPO,
|
|
detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
|
|
repo: data.params[0],
|
|
});
|
|
},
|
|
|
|
_handleBranchListOffsetRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.REPO,
|
|
detail: Gerrit.Nav.RepoDetailView.BRANCHES,
|
|
repo: data.params[0],
|
|
offset: data.params[2] || 0,
|
|
filter: null,
|
|
});
|
|
},
|
|
|
|
_handleBranchListFilterOffsetRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.REPO,
|
|
detail: Gerrit.Nav.RepoDetailView.BRANCHES,
|
|
repo: data.params.repo,
|
|
offset: data.params.offset,
|
|
filter: data.params.filter,
|
|
});
|
|
},
|
|
|
|
_handleBranchListFilterRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.REPO,
|
|
detail: Gerrit.Nav.RepoDetailView.BRANCHES,
|
|
repo: data.params.repo,
|
|
filter: data.params.filter || null,
|
|
});
|
|
},
|
|
|
|
_handleTagListOffsetRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.REPO,
|
|
detail: Gerrit.Nav.RepoDetailView.TAGS,
|
|
repo: data.params[0],
|
|
offset: data.params[2] || 0,
|
|
filter: null,
|
|
});
|
|
},
|
|
|
|
_handleTagListFilterOffsetRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.REPO,
|
|
detail: Gerrit.Nav.RepoDetailView.TAGS,
|
|
repo: data.params.repo,
|
|
offset: data.params.offset,
|
|
filter: data.params.filter,
|
|
});
|
|
},
|
|
|
|
_handleTagListFilterRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.REPO,
|
|
detail: Gerrit.Nav.RepoDetailView.TAGS,
|
|
repo: data.params.repo,
|
|
filter: data.params.filter || null,
|
|
});
|
|
},
|
|
|
|
_handleRepoListOffsetRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.ADMIN,
|
|
adminView: 'gr-repo-list',
|
|
offset: data.params[1] || 0,
|
|
filter: null,
|
|
openCreateModal: data.hash === 'create',
|
|
});
|
|
},
|
|
|
|
_handleRepoListFilterOffsetRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.ADMIN,
|
|
adminView: 'gr-repo-list',
|
|
offset: data.params.offset,
|
|
filter: data.params.filter,
|
|
});
|
|
},
|
|
|
|
_handleRepoListFilterRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.ADMIN,
|
|
adminView: 'gr-repo-list',
|
|
filter: data.params.filter || null,
|
|
});
|
|
},
|
|
|
|
_handleCreateProjectRoute(data) {
|
|
// Redirects the legacy route to the new route, which displays the project
|
|
// list with a hash 'create'.
|
|
this._redirect('/admin/repos#create');
|
|
},
|
|
|
|
_handleCreateGroupRoute(data) {
|
|
// Redirects the legacy route to the new route, which displays the group
|
|
// list with a hash 'create'.
|
|
this._redirect('/admin/groups#create');
|
|
},
|
|
|
|
_handleRepoRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.REPO,
|
|
repo: data.params[0],
|
|
});
|
|
},
|
|
|
|
_handlePluginListOffsetRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.ADMIN,
|
|
adminView: 'gr-plugin-list',
|
|
offset: data.params[1] || 0,
|
|
filter: null,
|
|
});
|
|
},
|
|
|
|
_handlePluginListFilterOffsetRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.ADMIN,
|
|
adminView: 'gr-plugin-list',
|
|
offset: data.params.offset,
|
|
filter: data.params.filter,
|
|
});
|
|
},
|
|
|
|
_handlePluginListFilterRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.ADMIN,
|
|
adminView: 'gr-plugin-list',
|
|
filter: data.params.filter || null,
|
|
});
|
|
},
|
|
|
|
_handlePluginListRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.ADMIN,
|
|
adminView: 'gr-plugin-list',
|
|
});
|
|
},
|
|
|
|
_handleQueryRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.SEARCH,
|
|
query: data.params[0],
|
|
offset: data.params[2],
|
|
});
|
|
},
|
|
|
|
_handleQueryLegacySuffixRoute(ctx) {
|
|
this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
|
|
},
|
|
|
|
_handleChangeNumberLegacyRoute(ctx) {
|
|
this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
|
|
},
|
|
|
|
_handleChangeRoute(ctx) {
|
|
// Parameter order is based on the regex group number matched.
|
|
const params = {
|
|
project: ctx.params[0],
|
|
changeNum: ctx.params[1],
|
|
basePatchNum: ctx.params[4],
|
|
patchNum: ctx.params[6],
|
|
view: Gerrit.Nav.View.CHANGE,
|
|
};
|
|
|
|
this._redirectOrNavigate(params);
|
|
},
|
|
|
|
_handleDiffRoute(ctx) {
|
|
// Parameter order is based on the regex group number matched.
|
|
const params = {
|
|
project: ctx.params[0],
|
|
changeNum: ctx.params[1],
|
|
basePatchNum: ctx.params[4],
|
|
patchNum: ctx.params[6],
|
|
path: ctx.params[8],
|
|
view: Gerrit.Nav.View.DIFF,
|
|
};
|
|
|
|
const address = this._parseLineAddress(ctx.hash);
|
|
if (address) {
|
|
params.leftSide = address.leftSide;
|
|
params.lineNum = address.lineNum;
|
|
}
|
|
|
|
this._redirectOrNavigate(params);
|
|
},
|
|
|
|
_handleChangeLegacyRoute(ctx) {
|
|
// Parameter order is based on the regex group number matched.
|
|
const params = {
|
|
changeNum: ctx.params[0],
|
|
basePatchNum: ctx.params[3],
|
|
patchNum: ctx.params[5],
|
|
view: Gerrit.Nav.View.CHANGE,
|
|
querystring: ctx.querystring,
|
|
};
|
|
|
|
this._normalizeLegacyRouteParams(params);
|
|
},
|
|
|
|
_handleLegacyLinenum(ctx) {
|
|
this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
|
|
},
|
|
|
|
_handleDiffLegacyRoute(ctx) {
|
|
// Parameter order is based on the regex group number matched.
|
|
const params = {
|
|
changeNum: ctx.params[0],
|
|
basePatchNum: ctx.params[2],
|
|
patchNum: ctx.params[4],
|
|
path: ctx.params[5],
|
|
view: Gerrit.Nav.View.DIFF,
|
|
};
|
|
|
|
const address = this._parseLineAddress(ctx.hash);
|
|
if (address) {
|
|
params.leftSide = address.leftSide;
|
|
params.lineNum = address.lineNum;
|
|
}
|
|
|
|
this._normalizeLegacyRouteParams(params);
|
|
},
|
|
|
|
_handleDiffEditRoute(ctx) {
|
|
// Parameter order is based on the regex group number matched.
|
|
this._redirectOrNavigate({
|
|
project: ctx.params[0],
|
|
changeNum: ctx.params[1],
|
|
patchNum: ctx.params[2],
|
|
path: ctx.params[3],
|
|
view: Gerrit.Nav.View.EDIT,
|
|
});
|
|
},
|
|
|
|
_handleChangeEditRoute(ctx) {
|
|
// Parameter order is based on the regex group number matched.
|
|
this._redirectOrNavigate({
|
|
project: ctx.params[0],
|
|
changeNum: ctx.params[1],
|
|
patchNum: ctx.params[3],
|
|
view: Gerrit.Nav.View.CHANGE,
|
|
edit: true,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Normalize the patch range params for a the change or diff view and
|
|
* redirect if URL upgrade is needed.
|
|
*/
|
|
_redirectOrNavigate(params) {
|
|
const needsRedirect = this._normalizePatchRangeParams(params);
|
|
if (needsRedirect) {
|
|
this._redirect(this._generateUrl(params));
|
|
} else {
|
|
this._setParams(params);
|
|
}
|
|
},
|
|
|
|
// TODO fix this so it properly redirects
|
|
// to /settings#Agreements (Scrolls down)
|
|
_handleAgreementsRoute(data) {
|
|
this._redirect('/settings/#Agreements');
|
|
},
|
|
|
|
_handleNewAgreementsRoute(data) {
|
|
data.params.view = Gerrit.Nav.View.AGREEMENTS;
|
|
this._setParams(data.params);
|
|
},
|
|
|
|
_handleSettingsLegacyRoute(data) {
|
|
// email tokens may contain '+' but no space.
|
|
// The parameter parsing replaces all '+' with a space,
|
|
// undo that to have valid tokens.
|
|
const token = data.params[0].replace(/ /g, '+');
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.SETTINGS,
|
|
emailToken: token,
|
|
});
|
|
},
|
|
|
|
_handleSettingsRoute(data) {
|
|
this._setParams({view: Gerrit.Nav.View.SETTINGS});
|
|
},
|
|
|
|
_handleRegisterRoute(ctx) {
|
|
this._setParams({justRegistered: true});
|
|
let path = ctx.params[0] || '/';
|
|
|
|
// Prevent redirect looping.
|
|
if (path.startsWith('/register')) { path = '/'; }
|
|
|
|
if (path[0] !== '/') { return; }
|
|
this._redirect(this.getBaseUrl() + path);
|
|
},
|
|
|
|
/**
|
|
* Handler for routes that should pass through the router and not be caught
|
|
* by the catchall _handleDefaultRoute handler.
|
|
*/
|
|
_handlePassThroughRoute() {
|
|
location.reload();
|
|
},
|
|
|
|
|
|
/**
|
|
* URL may sometimes have /+/ encoded to / /.
|
|
* Context: Issue 6888, Issue 7100
|
|
*/
|
|
_handleImproperlyEncodedPlusRoute(ctx) {
|
|
let hash = this._getHashFromCanonicalPath(ctx.canonicalPath);
|
|
if (hash.length) { hash = '#' + hash; }
|
|
this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
|
|
},
|
|
|
|
_handlePluginScreen(ctx) {
|
|
const view = Gerrit.Nav.View.PLUGIN_SCREEN;
|
|
const plugin = ctx.params[0];
|
|
const screen = ctx.params[1];
|
|
this._setParams({view, plugin, screen});
|
|
},
|
|
|
|
_handleDocumentationSearchRoute(data) {
|
|
this._setParams({
|
|
view: Gerrit.Nav.View.DOCUMENTATION_SEARCH,
|
|
filter: data.params.filter || null,
|
|
});
|
|
},
|
|
|
|
_handleDocumentationSearchRedirectRoute(data) {
|
|
this._redirect('/Documentation/q/filter:' +
|
|
encodeURIComponent(data.params[0]));
|
|
},
|
|
|
|
_handleDocumentationRedirectRoute(data) {
|
|
if (data.params[1]) {
|
|
location.reload();
|
|
} else {
|
|
// Redirect /Documentation to /Documentation/index.html
|
|
this._redirect('/Documentation/index.html');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Catchall route for when no other route is matched.
|
|
*/
|
|
_handleDefaultRoute() {
|
|
if (this._isInitialLoad) {
|
|
// Server recognized this route as polygerrit, so we show 404.
|
|
this._show404();
|
|
} else {
|
|
// Route can be recognized by server, so we pass it to server.
|
|
this._handlePassThroughRoute();
|
|
}
|
|
},
|
|
|
|
_show404() {
|
|
// Note: the app's 404 display is tightly-coupled with catching 404
|
|
// network responses, so we simulate a 404 response status to display it.
|
|
// TODO: Decouple the gr-app error view from network responses.
|
|
this._appElement().dispatchEvent(new CustomEvent('page-error',
|
|
{detail: {response: {status: 404}}}));
|
|
},
|
|
});
|
|
})();
|