Files
gerrit/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
Sven Selberg 6b24cc03d4 PG: Make commitlink selection configurable and consistent
Previously the selection of which Weblink that should be used for
inline commitlinks (like patchset, parent) was determined by a number
of assumptions:
(Known weblinks that links to codebrowsers are Gitweb and Gitiles)

* It was assumed that the name of the weblink to gitiles would be
  'gitiles'.

  The default name has since changed to 'browse', however many
  installations have configured the name to be the old default
  'gitiles and therefore we keep this name as an identifier of a
  weblink that links to a codebrowser.

* It was assumed that there would never be more than one weblink
  available to any of these code-browsers. If weblinks for both
  Gitiles and Gitweb was available PG would always select Gitweb
  for inline commitlinks.

  Use ServerInfo.gerrit.primaryWeblinkName setting to select
  codebrowser weblink for inline commit links.

  If primaryWeblinkName is not set try to find a codebrowser weblink
  by searching for known codebrowser weblinks in the order
  (Gitiles first):

    1. weblink.name == 'gitiles' - Old default Gitiles name
    2. weblink.name == 'browse'  - Current default Gitiles name
    3. weblink.name == 'gitweb'

* None of the known codebrowser weblinks were visible in
  changeMetadata.links. Since it was assumed that there would never
  be more than one weblink available that linked to a codebrowser,
  all known weblinks linking to codebrowsers were excluded from this
  collection, even if they were not selected for inline commitlinks.

  Since selection of the weblink that is used for inline commitlinks
  is now well defined we can safely remove exactly that one from
  the available weblinks when populating changeMetadata.links which
  will enable for having weblinks to both Gitweb and Gitiles (when
  named 'gitiles') being displayed on the Changescreen. The primary
  as inline commitlinks and all other in changeMetadata.links.

Bug: Issue 10355
Change-Id: If3aaa629d06ff77f1faff0dff15f68fe1adad469
2019-02-18 09:07:46 +00:00

480 lines
13 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 Defs = {};
/**
* @typedef {{
* message: string,
* icon: string,
* class: string,
* }}
*/
Defs.PushCertificateValidation;
const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
const SubmitTypeLabel = {
FAST_FORWARD_ONLY: 'Fast Forward Only',
MERGE_IF_NECESSARY: 'Merge if Necessary',
REBASE_IF_NECESSARY: 'Rebase if Necessary',
MERGE_ALWAYS: 'Always Merge',
REBASE_ALWAYS: 'Rebase Always',
CHERRY_PICK: 'Cherry Pick',
};
const NOT_CURRENT_MESSAGE = 'Not current - rebase possible';
/**
* @enum {string}
*/
const CertificateStatus = {
/**
* This certificate status is bad.
*/
BAD: 'BAD',
/**
* This certificate status is OK.
*/
OK: 'OK',
/**
* This certificate status is TRUSTED.
*/
TRUSTED: 'TRUSTED',
};
Polymer({
is: 'gr-change-metadata',
/**
* Fired when the change topic is changed.
*
* @event topic-changed
*/
properties: {
/** @type {?} */
change: Object,
labels: {
type: Object,
notify: true,
},
account: Object,
/** @type {?} */
revision: Object,
commitInfo: Object,
_mutable: {
type: Boolean,
computed: '_computeIsMutable(account)',
},
/**
* @type {{ note_db_enabled: string }}
*/
serverConfig: Object,
parentIsCurrent: Boolean,
_notCurrentMessage: {
type: String,
value: NOT_CURRENT_MESSAGE,
readOnly: true,
},
_topicReadOnly: {
type: Boolean,
computed: '_computeTopicReadOnly(_mutable, change)',
},
_hashtagReadOnly: {
type: Boolean,
computed: '_computeHashtagReadOnly(_mutable, change)',
},
_showReviewersByState: {
type: Boolean,
computed: '_computeShowReviewersByState(serverConfig)',
},
/**
* @type {Defs.PushCertificateValidation}
*/
_pushCertificateValidation: {
type: Object,
computed: '_computePushCertificateValidation(serverConfig, change)',
},
_showRequirements: {
type: Boolean,
computed: '_computeShowRequirements(change)',
},
_assignee: Array,
_isWip: {
type: Boolean,
computed: '_computeIsWip(change)',
},
_newHashtag: String,
_settingTopic: {
type: Boolean,
value: false,
},
_currentParents: {
type: Array,
computed: '_computeParents(change)',
},
/** @type {?} */
_CHANGE_ROLE: {
type: Object,
readOnly: true,
value: {
OWNER: 'owner',
UPLOADER: 'uploader',
AUTHOR: 'author',
COMMITTER: 'committer',
},
},
},
behaviors: [
Gerrit.RESTClientBehavior,
],
observers: [
'_changeChanged(change)',
'_labelsChanged(change.labels)',
'_assigneeChanged(_assignee.*)',
],
_labelsChanged(labels) {
this.labels = Object.assign({}, labels) || null;
},
_changeChanged(change) {
this._assignee = change.assignee ? [change.assignee] : [];
},
_assigneeChanged(assigneeRecord) {
if (!this.change) { return; }
const assignee = assigneeRecord.base;
if (assignee.length) {
const acct = assignee[0];
if (this.change.assignee &&
acct._account_id === this.change.assignee._account_id) { return; }
this.set(['change', 'assignee'], acct);
this.$.restAPI.setAssignee(this.change._number, acct._account_id);
} else {
if (!this.change.assignee) { return; }
this.set(['change', 'assignee'], undefined);
this.$.restAPI.deleteAssignee(this.change._number);
}
},
_computeHideStrategy(change) {
return !this.changeIsOpen(change.status);
},
/**
* @param {Object} commitInfo
* @return {?Array} If array is empty, returns null instead so
* an existential check can be used to hide or show the webLinks
* section.
*/
_computeWebLinks(commitInfo, serverConfig) {
if (!commitInfo) { return null; }
const weblinks = Gerrit.Nav.getChangeWeblinks(
this.change ? this.change.repo : '',
commitInfo.commit,
{
weblinks: commitInfo.web_links,
config: serverConfig,
});
return weblinks.length ? weblinks : null;
},
_computeStrategy(change) {
return SubmitTypeLabel[change.submit_type];
},
_computeLabelNames(labels) {
return Object.keys(labels).sort();
},
_handleTopicChanged(e, topic) {
const lastTopic = this.change.topic;
if (!topic.length) { topic = null; }
this._settingTopic = true;
this.$.restAPI.setChangeTopic(this.change._number, topic)
.then(newTopic => {
this._settingTopic = false;
this.set(['change', 'topic'], newTopic);
if (newTopic !== lastTopic) {
this.dispatchEvent(
new CustomEvent('topic-changed', {bubbles: true}));
}
});
},
_showAddTopic(changeRecord, settingTopic) {
const hasTopic = !!changeRecord && !!changeRecord.base.topic;
return !hasTopic && !settingTopic;
},
_showTopicChip(changeRecord, settingTopic) {
const hasTopic = !!changeRecord && !!changeRecord.base.topic;
return hasTopic && !settingTopic;
},
_handleHashtagChanged(e) {
const lastHashtag = this.change.hashtag;
if (!this._newHashtag.length) { return; }
const newHashtag = this._newHashtag;
this._newHashtag = '';
this.$.restAPI.setChangeHashtag(
this.change._number, {add: [newHashtag]}).then(newHashtag => {
this.set(['change', 'hashtags'], newHashtag);
if (newHashtag !== lastHashtag) {
this.dispatchEvent(
new CustomEvent('hashtag-changed', {bubbles: true}));
}
});
},
_computeTopicReadOnly(mutable, change) {
return !mutable ||
!change.actions ||
!change.actions.topic ||
!change.actions.topic.enabled;
},
_computeHashtagReadOnly(mutable, change) {
return !mutable ||
!change.actions ||
!change.actions.hashtags ||
!change.actions.hashtags.enabled;
},
_computeAssigneeReadOnly(mutable, change) {
return !mutable ||
!change.actions ||
!change.actions.assignee ||
!change.actions.assignee.enabled;
},
_computeTopicPlaceholder(_topicReadOnly) {
// Action items in Material Design are uppercase -- placeholder label text
// is sentence case.
return _topicReadOnly ? 'No topic' : 'ADD TOPIC';
},
_computeHashtagPlaceholder(_hashtagReadOnly) {
return _hashtagReadOnly ? '' : HASHTAG_ADD_MESSAGE;
},
_computeShowReviewersByState(serverConfig) {
return !!serverConfig.note_db_enabled;
},
_computeShowRequirements(change) {
if (change.status !== this.ChangeStatus.NEW) {
// TODO(maximeg) change this to display the stored
// requirements, once it is implemented server-side.
return false;
}
const hasRequirements = !!change.requirements &&
Object.keys(change.requirements).length > 0;
const hasLabels = !!change.labels &&
Object.keys(change.labels).length > 0;
return hasRequirements || hasLabels || !!change.work_in_progress;
},
/**
* @return {?Defs.PushCertificateValidation} object representing data for
* the push validation.
*/
_computePushCertificateValidation(serverConfig, change) {
if (!serverConfig || !serverConfig.receive ||
!serverConfig.receive.enable_signed_push) {
return null;
}
const rev = change.revisions[change.current_revision];
if (!rev.push_certificate || !rev.push_certificate.key) {
return {
class: 'help',
icon: 'gr-icons:help',
message: 'This patch set was created without a push certificate',
};
}
const key = rev.push_certificate.key;
switch (key.status) {
case CertificateStatus.BAD:
return {
class: 'invalid',
icon: 'gr-icons:close',
message: this._problems('Push certificate is invalid', key),
};
case CertificateStatus.OK:
return {
class: 'notTrusted',
icon: 'gr-icons:info',
message: this._problems(
'Push certificate is valid, but key is not trusted', key),
};
case CertificateStatus.TRUSTED:
return {
class: 'trusted',
icon: 'gr-icons:check',
message: this._problems(
'Push certificate is valid and key is trusted', key),
};
default:
throw new Error(`unknown certificate status: ${key.status}`);
}
},
_problems(msg, key) {
if (!key || !key.problems || key.problems.length === 0) {
return msg;
}
return [msg + ':'].concat(key.problems).join('\n');
},
_computeProjectURL(project) {
return Gerrit.Nav.getUrlForProjectChanges(project);
},
_computeBranchURL(project, branch) {
return Gerrit.Nav.getUrlForBranch(branch, project,
this.change.status == this.ChangeStatus.NEW ? 'open' :
this.change.status.toLowerCase());
},
_computeTopicURL(topic) {
return Gerrit.Nav.getUrlForTopic(topic);
},
_computeHashtagURL(hashtag) {
return Gerrit.Nav.getUrlForHashtag(hashtag);
},
_handleTopicRemoved(e) {
const target = Polymer.dom(e).rootTarget;
target.disabled = true;
this.$.restAPI.setChangeTopic(this.change._number, null).then(() => {
target.disabled = false;
this.set(['change', 'topic'], '');
this.dispatchEvent(
new CustomEvent('topic-changed', {bubbles: true}));
}).catch(err => {
target.disabled = false;
return;
});
},
_handleHashtagRemoved(e) {
e.preventDefault();
const target = Polymer.dom(e).rootTarget;
target.disabled = true;
this.$.restAPI.setChangeHashtag(this.change._number,
{remove: [target.text]})
.then(newHashtag => {
target.disabled = false;
this.set(['change', 'hashtags'], newHashtag);
}).catch(err => {
target.disabled = false;
return;
});
},
_computeIsWip(change) {
return !!change.work_in_progress;
},
_computeShowRoleClass(change, role) {
return this._getNonOwnerRole(change, role) ? '' : 'hideDisplay';
},
/**
* Get the user with the specified role on the change. Returns null if the
* user with that role is the same as the owner.
* @param {!Object} change
* @param {string} role One of the values from _CHANGE_ROLE
* @return {Object|null} either an accound or null.
*/
_getNonOwnerRole(change, role) {
if (!change.current_revision ||
!change.revisions[change.current_revision]) {
return null;
}
const rev = change.revisions[change.current_revision];
if (!rev) { return null; }
if (role === this._CHANGE_ROLE.UPLOADER &&
rev.uploader &&
change.owner._account_id !== rev.uploader._account_id) {
return rev.uploader;
}
if (role === this._CHANGE_ROLE.AUTHOR &&
rev.commit && rev.commit.author &&
change.owner.email !== rev.commit.author.email) {
return rev.commit.author;
}
if (role === this._CHANGE_ROLE.COMMITTER &&
rev.commit && rev.commit.committer &&
change.owner.email !== rev.commit.committer.email) {
return rev.commit.committer;
}
return null;
},
_computeParents(change) {
if (!change.current_revision ||
!change.revisions[change.current_revision] ||
!change.revisions[change.current_revision].commit) {
return undefined;
}
return change.revisions[change.current_revision].commit.parents;
},
_computeParentsLabel(parents) {
return parents.length > 1 ? 'Parents' : 'Parent';
},
_computeParentListClass(parents, parentIsCurrent) {
return [
'parentList',
parents.length > 1 ? 'merge' : 'nonMerge',
parentIsCurrent ? 'current' : 'notCurrent',
].join(' ');
},
_computeIsMutable(account) {
return !!Object.keys(account).length;
},
editTopic() {
if (this._topicReadOnly || this.change.topic) { return; }
// Cannot use `this.$.ID` syntax because the element exists inside of a
// dom-if.
this.$$('.topicEditableLabel').open();
},
});
})();