Commit 7942d863 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into 'dm-copy-mr-source-branch-as-gfm'

# Conflicts:
#   app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
parents 24902315 9b0309db
...@@ -408,9 +408,6 @@ rake gitlab:assets:compile: ...@@ -408,9 +408,6 @@ rake gitlab:assets:compile:
- webpack-report/ - webpack-report/
rake karma: rake karma:
cache:
paths:
- vendor/ruby
stage: test stage: test
<<: *use-pg <<: *use-pg
<<: *dedicated-runner <<: *dedicated-runner
......
...@@ -33,7 +33,7 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({ ...@@ -33,7 +33,7 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
<span> <span>
{{ __('FirstPushedBy|First') }} {{ __('FirstPushedBy|First') }}
<span class="commit-icon">${iconCommit}</span> <span class="commit-icon">${iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a> <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
{{ __('FirstPushedBy|pushed by') }} {{ __('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link"> <a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }} {{ commit.author.name }}
......
...@@ -26,9 +26,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ ...@@ -26,9 +26,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
<h5 class="item-title"> <h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a> <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i> <i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span> <span class="icon-branch">${iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5> </h5>
<span> <span>
<a :href="build.url" class="build-date">{{ build.date }}</a> <a :href="build.url" class="build-date">{{ build.date }}</a>
......
...@@ -29,9 +29,9 @@ global.cycleAnalytics.StageTestComponent = Vue.extend({ ...@@ -29,9 +29,9 @@ global.cycleAnalytics.StageTestComponent = Vue.extend({
&middot; &middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a> <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i> <i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span> <span class="icon-branch">${iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5> </h5>
<span> <span>
<a :href="build.url" class="issue-date"> <a :href="build.url" class="issue-date">
......
...@@ -14,7 +14,6 @@ ...@@ -14,7 +14,6 @@
/* global NotificationsForm */ /* global NotificationsForm */
/* global TreeView */ /* global TreeView */
/* global NotificationsDropdown */ /* global NotificationsDropdown */
/* global UsersSelect */
/* global GroupAvatar */ /* global GroupAvatar */
/* global LineHighlighter */ /* global LineHighlighter */
/* global ProjectFork */ /* global ProjectFork */
...@@ -52,6 +51,7 @@ import ShortcutsWiki from './shortcuts_wiki'; ...@@ -52,6 +51,7 @@ import ShortcutsWiki from './shortcuts_wiki';
import Pipelines from './pipelines'; import Pipelines from './pipelines';
import BlobViewer from './blob/viewer/index'; import BlobViewer from './blob/viewer/index';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import UsersSelect from './users_select';
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
...@@ -113,6 +113,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -113,6 +113,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:boards:show': case 'projects:boards:show':
case 'projects:boards:index': case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break; break;
case 'projects:builds:show': case 'projects:builds:show':
new Build(); new Build();
...@@ -127,6 +128,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -127,6 +128,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
}); });
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break; break;
case 'projects:issues:show': case 'projects:issues:show':
new Issue(); new Issue();
...@@ -139,6 +141,10 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -139,6 +141,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
new Milestone(); new Milestone();
new Sidebar(); new Sidebar();
break; break;
case 'groups:issues':
case 'groups:merge_requests':
new UsersSelect();
break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new gl.Todos(); new gl.Todos();
break; break;
...@@ -223,6 +229,10 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -223,6 +229,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'dashboard:activity': case 'dashboard:activity':
new gl.Activities(); new gl.Activities();
break; break;
case 'dashboard:issues':
case 'dashboard:merge_requests':
new UsersSelect();
break;
case 'projects:commit:show': case 'projects:commit:show':
new Commit(); new Commit();
new gl.Diff(); new gl.Diff();
...@@ -377,6 +387,9 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -377,6 +387,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
new LineHighlighter(); new LineHighlighter();
new BlobViewer(); new BlobViewer();
break; break;
case 'import:fogbugz:new_user_map':
new UsersSelect();
break;
} }
switch (path.first()) { switch (path.first()) {
case 'sessions': case 'sessions':
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
/* global UsersSelect */
/* global bp */ /* global bp */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import UsersSelect from './users_select';
(function() { (function() {
this.IssuableContext = (function() { this.IssuableContext = (function() {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */ /* global GitLab */
/* global UsersSelect */
/* global ZenMode */ /* global ZenMode */
/* global Autosave */ /* global Autosave */
/* global dateFormat */ /* global dateFormat */
/* global Pikaday */ /* global Pikaday */
import UsersSelect from './users_select';
(function() { (function() {
this.IssuableForm = (function() { this.IssuableForm = (function() {
IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?'; IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
......
export default (newStateData, tasks) => {
const $tasks = $('#task_status');
const $tasksShort = $('#task_status_short');
const $issueableHeader = $('.issuable-header');
const tasksStates = { newState: null, currentState: null };
if ($tasks.length === 0) {
if (!(newStateData.task_status.indexOf('0 of 0') === 0)) {
$issueableHeader.append(`<span id="task_status">${newStateData.task_status}</span>`);
} else {
$issueableHeader.append('<span id="task_status"></span>');
}
} else {
tasksStates.newState = newStateData.task_status.indexOf('0 of 0') === 0;
tasksStates.currentState = tasks.indexOf('0 of 0') === 0;
}
if ($tasks.length !== 0 && !tasksStates.newState) {
$tasks.text(newStateData.task_status);
$tasksShort.text(newStateData.task_status);
} else if (tasksStates.currentState) {
$issueableHeader.append(`<span id="task_status">${newStateData.task_status}</span>`);
} else if (tasksStates.newState) {
$tasks.remove();
$tasksShort.remove();
}
};
<script>
import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
export default {
props: {
endpoint: {
required: true,
type: String,
},
canUpdate: {
required: true,
type: Boolean,
},
issuableRef: {
type: String,
required: true,
},
initialTitle: {
type: String,
required: true,
},
initialDescriptionHtml: {
type: String,
required: false,
default: '',
},
initialDescriptionText: {
type: String,
required: false,
default: '',
},
},
data() {
const store = new Store({
titleHtml: this.initialTitle,
descriptionHtml: this.initialDescriptionHtml,
descriptionText: this.initialDescriptionText,
});
return {
store,
state: store.state,
};
},
components: {
descriptionComponent,
titleComponent,
},
created() {
const resource = new Service(this.endpoint);
const poll = new Poll({
resource,
method: 'getData',
successCallback: (res) => {
this.store.updateState(res.json());
},
errorCallback(err) {
throw new Error(err);
},
});
if (!Visibility.hidden()) {
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
},
};
</script>
<template>
<div>
<title-component
:issuable-ref="issuableRef"
:title-html="state.titleHtml"
:title-text="state.titleText" />
<description-component
v-if="state.descriptionHtml"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
:task-status="state.taskStatus" />
</div>
</template>
<script>
import animateMixin from '../mixins/animate';
export default {
mixins: [animateMixin],
props: {
canUpdate: {
type: Boolean,
required: true,
},
descriptionHtml: {
type: String,
required: true,
},
descriptionText: {
type: String,
required: true,
},
updatedAt: {
type: String,
required: true,
},
taskStatus: {
type: String,
required: true,
},
},
data() {
return {
preAnimation: false,
pulseAnimation: false,
timeAgoEl: $('.js-issue-edited-ago'),
};
},
watch: {
descriptionHtml() {
this.animateChange();
this.$nextTick(() => {
const toolTipTime = gl.utils.formatDate(this.updatedAt);
this.timeAgoEl.attr('datetime', this.updatedAt)
.attr('title', toolTipTime)
.tooltip('fixTitle');
this.renderGFM();
});
},
taskStatus() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/);
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
} else {
$tasks.text('');
$tasksShort.text('');
}
},
},
methods: {
renderGFM() {
$(this.$refs['gfm-entry-content']).renderGFM();
if (this.canUpdate) {
// eslint-disable-next-line no-new
new gl.TaskList({
dataType: 'issue',
fieldName: 'description',
selector: '.detail-page-description',
});
}
},
},
mounted() {
this.renderGFM();
},
};
</script>
<template>
<div
class="description"
:class="{
'js-task-list-container': canUpdate
}">
<div
class="wiki"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="descriptionHtml"
ref="gfm-content">
</div>
<textarea
class="hidden js-task-list-field"
v-if="descriptionText"
v-model="descriptionText">
</textarea>
</div>
</template>
<script>
import animateMixin from '../mixins/animate';
export default {
mixins: [animateMixin],
data() {
return {
preAnimation: false,
pulseAnimation: false,
titleEl: document.querySelector('title'),
};
},
props: {
issuableRef: {
type: String,
required: true,
},
titleHtml: {
type: String,
required: true,
},
titleText: {
type: String,
required: true,
},
},
watch: {
titleHtml() {
this.setPageTitle();
this.animateChange();
},
},
methods: {
setPageTitle() {
const currentPageTitleScope = this.titleEl.innerText.split('·');
currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
this.titleEl.textContent = currentPageTitleScope.join('·');
},
},
};
</script>
<template>
<h2
class="title"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="titleHtml"
>
</h2>
</template>
import Vue from 'vue'; import Vue from 'vue';
import IssueTitle from './issue_title_description.vue'; import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor'; import '../vue_shared/vue_resource_interceptor';
(() => { document.addEventListener('DOMContentLoaded', () => new Vue({
const issueTitleData = document.querySelector('.issue-title-data').dataset; el: document.getElementById('js-issuable-app'),
const { canUpdateTasksClass, endpoint } = issueTitleData; components: {
issuableApp,
},
data() {
const issuableElement = this.$options.el;
const issuableTitleElement = issuableElement.querySelector('.title');
const issuableDescriptionElement = issuableElement.querySelector('.wiki');
const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field');
const {
canUpdate,
endpoint,
issuableRef,
} = issuableElement.dataset;
const vm = new Vue({ return {
el: '.issue-title-entrypoint', canUpdate: gl.utils.convertPermissionToBoolean(canUpdate),
render: createElement => createElement(IssueTitle, { endpoint,
issuableRef,
initialTitle: issuableTitleElement.innerHTML,
initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
};
},
render(createElement) {
return createElement('issuable-app', {
props: { props: {
canUpdateTasksClass, canUpdate: this.canUpdate,
endpoint, endpoint: this.endpoint,
issuableRef: this.issuableRef,
initialTitle: this.initialTitle,
initialDescriptionHtml: this.initialDescriptionHtml,
initialDescriptionText: this.initialDescriptionText,
}, },
}), });
}); },
}));
return vm;
})();
<script>
import Visibility from 'visibilityjs';
import Poll from './../lib/utils/poll';
import Service from './services/index';
import tasks from './actions/tasks';
export default {
props: {
endpoint: {
required: true,
type: String,
},
canUpdateTasksClass: {
required: true,
type: String,
},
},
data() {
const resource = new Service(this.$http, this.endpoint);
const poll = new Poll({
resource,
method: 'getTitle',
successCallback: (res) => {
this.renderResponse(res);
},
errorCallback: (err) => {
throw new Error(err);
},
});
return {
poll,
apiData: {},
tasks: '0 of 0',
title: null,
titleText: '',
titleFlag: {
pre: true,
pulse: false,
},
description: null,
descriptionText: '',
descriptionChange: false,
descriptionFlag: {
pre: true,
pulse: false,
},
timeAgoEl: $('.issue_edited_ago'),
titleEl: document.querySelector('title'),
};
},
methods: {
updateFlag(key, toggle) {
this[key].pre = toggle;
this[key].pulse = !toggle;
},
renderResponse(res) {
this.apiData = res.json();
this.triggerAnimation();
},
updateTaskHTML() {
tasks(this.apiData, this.tasks);
},
elementsToVisualize(noTitleChange, noDescriptionChange) {
if (!noTitleChange) {
this.titleText = this.apiData.title_text;
this.updateFlag('titleFlag', true);
}
if (!noDescriptionChange) {
// only change to true when we need to bind TaskLists the html of description
this.descriptionChange = true;
this.updateTaskHTML();
this.tasks = this.apiData.task_status;
this.updateFlag('descriptionFlag', true);
}
},
setTabTitle() {
const currentTabTitleScope = this.titleEl.innerText.split('·');
currentTabTitleScope[0] = `${this.titleText} (#${this.apiData.issue_number}) `;
this.titleEl.innerText = currentTabTitleScope.join('·');
},
animate(title, description) {
this.title = title;
this.description = description;
this.setTabTitle();
this.$nextTick(() => {
this.updateFlag('titleFlag', false);
this.updateFlag('descriptionFlag', false);
});
},
triggerAnimation() {
// always reset to false before checking the change
this.descriptionChange = false;
const { title, description } = this.apiData;
this.descriptionText = this.apiData.description_text;
const noTitleChange = this.title === title;
const noDescriptionChange = this.description === description;
/**
* since opacity is changed, even if there is no diff for Vue to update
* we must check the title/description even on a 304 to ensure no visual change
*/
if (noTitleChange && noDescriptionChange) return;
this.elementsToVisualize(noTitleChange, noDescriptionChange);
this.animate(title, description);
},
updateEditedTimeAgo() {
const toolTipTime = gl.utils.formatDate(this.apiData.updated_at);
this.timeAgoEl.attr('datetime', this.apiData.updated_at);
this.timeAgoEl.attr('title', toolTipTime).tooltip('fixTitle');
},
},
created() {
if (!Visibility.hidden()) {
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
updated() {
// if new html is injected (description changed) - bind TaskList and call renderGFM
if (this.descriptionChange) {
this.updateEditedTimeAgo();
$(this.$refs['issue-content-container-gfm-entry']).renderGFM();
const tl = new gl.TaskList({
dataType: 'issue',
fieldName: 'description',
selector: '.detail-page-description',
});
return tl && null;
}
return null;
},
};
</script>
<template>
<div>
<h2
class="title"
:class="{ 'issue-realtime-pre-pulse': titleFlag.pre, 'issue-realtime-trigger-pulse': titleFlag.pulse }"
ref="issue-title"
v-html="title"
>
</h2>
<div
class="description is-task-list-enabled"
:class="canUpdateTasksClass"
v-if="description"
>
<div
class="wiki"
:class="{ 'issue-realtime-pre-pulse': descriptionFlag.pre, 'issue-realtime-trigger-pulse': descriptionFlag.pulse }"
v-html="description"
ref="issue-content-container-gfm-entry"
>
</div>
<textarea
class="hidden js-task-list-field"
v-if="descriptionText"
>{{descriptionText}}</textarea>
</div>
</div>
</template>
export default {
methods: {
animateChange() {
this.preAnimation = true;
this.pulseAnimation = false;
this.$nextTick(() => {
this.preAnimation = false;
this.pulseAnimation = true;
});
},
},
};
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class Service { export default class Service {
constructor(resource, endpoint) { constructor(endpoint) {
this.resource = resource;
this.endpoint = endpoint; this.endpoint = endpoint;
this.resource = Vue.resource(this.endpoint);
} }
getTitle() { getData() {
return this.resource.get(this.endpoint); return this.resource.get();
} }
} }
export default class Store {
constructor({
titleHtml,
descriptionHtml,
descriptionText,
}) {
this.state = {
titleHtml,
titleText: '',
descriptionHtml,
descriptionText,
taskStatus: '',
updatedAt: '',
};
}
updateState(data) {
this.state.titleHtml = data.title;
this.state.titleText = data.title_text;
this.state.descriptionHtml = data.description;
this.state.descriptionText = data.description_text;
this.state.taskStatus = data.task_status;
this.state.updatedAt = data.updated_at;
}
}
...@@ -4,8 +4,10 @@ import illustrationSvg from '../icons/intro_illustration.svg'; ...@@ -4,8 +4,10 @@ import illustrationSvg from '../icons/intro_illustration.svg';
const cookieKey = 'pipeline_schedules_callout_dismissed'; const cookieKey = 'pipeline_schedules_callout_dismissed';
export default { export default {
name: 'PipelineSchedulesCallout',
data() { data() {
return { return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
illustrationSvg, illustrationSvg,
calloutDismissed: Cookies.get(cookieKey) === 'true', calloutDismissed: Cookies.get(cookieKey) === 'true',
}; };
...@@ -28,13 +30,15 @@ export default { ...@@ -28,13 +30,15 @@ export default {
<div class="svg-container" v-html="illustrationSvg"></div> <div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy"> <div class="user-callout-copy">
<h4>Scheduling Pipelines</h4> <h4>Scheduling Pipelines</h4>
<p> <p>
The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags.
Those scheduled pipelines will inherit limited project access based on their associated user. Those scheduled pipelines will inherit limited project access based on their associated user.
</p> </p>
<p> Learn more in the <p> Learn more in the
<!-- FIXME --> <a
<a href="random.com">pipeline schedules documentation</a>. :href="docsUrl"
target="_blank"
rel="nofollow">pipeline schedules documentation</a>. <!-- oneline to prevent extra space before period -->
</p> </p>
</div> </div>
</div> </div>
......
import Vue from 'vue'; import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout'; import PipelineSchedulesCallout from './components/pipeline_schedules_callout';
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout); document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout',
document.addEventListener('DOMContentLoaded', () => { components: {
new PipelineSchedulesCalloutComponent() 'pipeline-schedules-callout': PipelineSchedulesCallout,
.$mount('#scheduling-pipelines-callout'); },
}); render(createElement) {
return createElement('pipeline-schedules-callout');
},
}));
...@@ -29,7 +29,7 @@ export default { ...@@ -29,7 +29,7 @@ export default {
</a> </a>
<span <span
v-if="!user" v-if="!user"
class="js-pipeline-url-api api monospace"> class="js-pipeline-url-api api">
API API
</span> </span>
<span <span
......
/* global Flash */
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
export default {
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
props: {
stage: {
type: Object,
required: true,
},
},
updated() {
if (this.builds) {
this.stopDropdownClickPropagation();
}
},
methods: {
fetchBuilds(e) {
const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
return flash;
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
e.stopPropagation();
});
},
},
computed: {
buildsOrSpinner() {
return this.builds ? this.builds : this.spinner;
},
dropdownClass() {
if (this.builds) return 'js-builds-dropdown-container';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
return `Build: ${this.stage.status.label}`;
},
tooltip() {
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
},
triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
},
svgHTML() {
return borderlessStatusIconEntityMap[this.stage.status.icon];
},
},
watch: {
'stage.title': function stageTitle() {
$(this.$refs.button).tooltip('destroy').tooltip();
},
},
template: `
<div>
<button
@click="fetchBuilds($event)"
:class="triggerButtonClass"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
type="button"
ref="button"
:aria-label="stage.title">
<span v-html="svgHTML" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up" aria-hidden="true"></div>
<div
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
v-html="buildsOrSpinner">
</div>
</ul>
</div>
`,
};
import canceledSvg from 'icons/_icon_status_canceled.svg';
import createdSvg from 'icons/_icon_status_created.svg';
import failedSvg from 'icons/_icon_status_failed.svg';
import manualSvg from 'icons/_icon_status_manual.svg';
import pendingSvg from 'icons/_icon_status_pending.svg';
import runningSvg from 'icons/_icon_status_running.svg';
import skippedSvg from 'icons/_icon_status_skipped.svg';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
export default {
props: {
pipeline: {
type: Object,
required: true,
},
},
data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return {
svg: svgsDictionary[this.pipeline.details.status.icon],
};
},
computed: {
cssClasses() {
return `ci-status ci-${this.pipeline.details.status.group}`;
},
detailsPath() {
const { status } = this.pipeline.details;
return status.has_details ? status.details_path : false;
},
content() {
return `${this.svg} ${this.pipeline.details.status.text}`;
},
},
template: `
<td class="commit-link">
<a
:class="cssClasses"
:href="detailsPath"
v-html="content">
</a>
</td>
`,
};
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
/* global UsersSelect */
import UsersSelect from './users_select';
class Todos { class Todos {
constructor() { constructor() {
......
This diff is collapsed.
/* global Flash */ /* global Flash */
import '~/lib/utils/datetime_utility'; import '~/lib/utils/datetime_utility';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons'; import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
import MemoryUsage from './mr_widget_memory_usage'; import MemoryUsage from './mr_widget_memory_usage';
import MRWidgetService from '../services/mr_widget_service'; import MRWidgetService from '../services/mr_widget_service';
...@@ -16,7 +16,7 @@ export default { ...@@ -16,7 +16,7 @@ export default {
}, },
computed: { computed: {
svg() { svg() {
return statusClassToSvgMap.icon_status_success; return statusIconEntityMap.icon_status_success;
}, },
}, },
methods: { methods: {
......
...@@ -70,32 +70,34 @@ export default { ...@@ -70,32 +70,34 @@ export default {
</span> </span>
</div> </div>
<div class="normal"> <div class="normal">
<b>Request to merge</b> <strong>
<span Request to merge
class="label-branch" <span
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}" class="label-branch"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''" :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
data-placement="bottom" :title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
v-html="mr.sourceBranchLink"></span> data-placement="bottom"
<button v-html="mr.sourceBranchLink"></span>
class="btn btn-transparent btn-clipboard has-tooltip" <button
data-title="Copy branch name to clipboard" class="btn btn-transparent btn-clipboard has-tooltip"
:data-clipboard-text="branchNameClipboardData"> data-title="Copy branch name to clipboard"
<i :data-clipboard-text="branchNameClipboardData">
aria-hidden="true" <i
class="fa fa-clipboard"></i> aria-hidden="true"
</button> class="fa fa-clipboard"></i>
<b>into</b> </button>
<span into
class="label-branch" <span
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}" class="label-branch"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''" :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
data-placement="bottom"> :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
<a data-placement="bottom">
:href="mr.targetBranchCommitsPath"> <a
{{mr.targetBranch}} :href="mr.targetBranchPath">
</a> {{mr.targetBranch}}
</span> </a>
</span>
</strong>
<span <span
v-if="shouldShowCommitsBehindText" v-if="shouldShowCommitsBehindText"
class="diverged-commits-count"> class="diverged-commits-count">
......
import PipelineStage from '../../pipelines/components/stage'; import PipelineStage from '../../pipelines/components/stage.vue';
import pipelineStatusIcon from '../../vue_shared/components/pipeline_status_icon'; import ciIcon from '../../vue_shared/components/ci_icon.vue';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons'; import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
export default { export default {
name: 'MRWidgetPipeline', name: 'MRWidgetPipeline',
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
}, },
components: { components: {
'pipeline-stage': PipelineStage, 'pipeline-stage': PipelineStage,
'pipeline-status-icon': pipelineStatusIcon, ciIcon,
}, },
computed: { computed: {
hasCIError() { hasCIError() {
...@@ -18,11 +18,14 @@ export default { ...@@ -18,11 +18,14 @@ export default {
return hasCI && !ciStatus; return hasCI && !ciStatus;
}, },
svg() { svg() {
return statusClassToSvgMap.icon_status_failed; return statusIconEntityMap.icon_status_failed;
}, },
stageText() { stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage'; return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
}, },
status() {
return this.mr.pipeline.details.status || {};
},
}, },
template: ` template: `
<div class="mr-widget-heading"> <div class="mr-widget-heading">
...@@ -38,13 +41,22 @@ export default { ...@@ -38,13 +41,22 @@ export default {
<span>Could not connect to the CI server. Please check your settings and try again.</span> <span>Could not connect to the CI server. Please check your settings and try again.</span>
</template> </template>
<template v-else> <template v-else>
<pipeline-status-icon :pipelineStatus="mr.pipelineDetailedStatus" /> <div>
<a
class="icon-link"
:href="this.status.details_path">
<ci-icon :status="status" />
</a>
</div>
<span> <span>
Pipeline Pipeline
<a <a
:href="mr.pipeline.path" :href="mr.pipeline.path"
class="pipeline-id">#{{mr.pipeline.id}}</a> class="pipeline-id">#{{mr.pipeline.id}}</a>
{{mr.pipeline.details.status.label}} {{mr.pipeline.details.status.label}}
</span>
<span
v-if="mr.pipeline.details.stages.length > 0">
with {{stageText}} with {{stageText}}
</span> </span>
<div class="mr-widget-pipeline-graph"> <div class="mr-widget-pipeline-graph">
...@@ -61,7 +73,7 @@ export default { ...@@ -61,7 +73,7 @@ export default {
for for
<a <a
:href="mr.pipeline.commit.commit_path" :href="mr.pipeline.commit.commit_path"
class="monospace js-commit-link"> class="commit-sha js-commit-link">
{{mr.pipeline.commit.short_id}}</a>. {{mr.pipeline.commit.short_id}}</a>.
</span> </span>
<span <span
......
...@@ -20,7 +20,7 @@ export default { ...@@ -20,7 +20,7 @@ export default {
<p> <p>
The changes were not merged into The changes were not merged into
<a <a
:href="mr.targetBranchCommitsPath" :href="mr.targetBranchPath"
class="label-branch"> class="label-branch">
{{mr.targetBranch}}</a>. {{mr.targetBranch}}</a>.
</p> </p>
......
...@@ -16,7 +16,7 @@ export default { ...@@ -16,7 +16,7 @@ export default {
The changes will be merged into The changes will be merged into
<span class="label-branch"> <span class="label-branch">
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a> <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
</span> </span>.
</p> </p>
</section> </section>
</div> </div>
......
...@@ -87,7 +87,7 @@ export default { ...@@ -87,7 +87,7 @@ export default {
:href="mr.targetBranchPath" :href="mr.targetBranchPath"
class="label-branch"> class="label-branch">
{{mr.targetBranch}} {{mr.targetBranch}}
</a> </a>.
</p> </p>
<p v-if="mr.shouldRemoveSourceBranch"> <p v-if="mr.shouldRemoveSourceBranch">
The source branch will be removed. The source branch will be removed.
......
export default {
name: 'MRWidgetSHAMismatch',
template: `
<div class="mr-widget-body">
<button
type="button"
class="btn btn-success btn-small"
disabled="true">
Merge
</button>
<span class="bold">
The source branch HEAD has recently changed. Please reload the page and review the changes before merging.
</span>
</div>
`,
};
...@@ -27,6 +27,7 @@ export { default as NothingToMergeState } from './components/states/mr_widget_no ...@@ -27,6 +27,7 @@ export { default as NothingToMergeState } from './components/states/mr_widget_no
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch'; export { default as MissingBranchState } from './components/states/mr_widget_missing_branch';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed'; export { default as NotAllowedState } from './components/states/mr_widget_not_allowed';
export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge'; export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge';
export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch';
export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions'; export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked'; export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
......
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
MissingBranchState, MissingBranchState,
NotAllowedState, NotAllowedState,
ReadyToMergeState, ReadyToMergeState,
SHAMismatchState,
UnresolvedDiscussionsState, UnresolvedDiscussionsState,
PipelineBlockedState, PipelineBlockedState,
PipelineFailedState, PipelineFailedState,
...@@ -203,6 +204,7 @@ export default { ...@@ -203,6 +204,7 @@ export default {
'mr-widget-not-allowed': NotAllowedState, 'mr-widget-not-allowed': NotAllowedState,
'mr-widget-missing-branch': MissingBranchState, 'mr-widget-missing-branch': MissingBranchState,
'mr-widget-ready-to-merge': ReadyToMergeState, 'mr-widget-ready-to-merge': ReadyToMergeState,
'mr-widget-sha-mismatch': SHAMismatchState,
'mr-widget-squash-before-merge': SquashBeforeMerge, 'mr-widget-squash-before-merge': SquashBeforeMerge,
'mr-widget-checking': CheckingState, 'mr-widget-checking': CheckingState,
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
......
...@@ -21,6 +21,8 @@ export default function deviseState(data) { ...@@ -21,6 +21,8 @@ export default function deviseState(data) {
return 'unresolvedDiscussions'; return 'unresolvedDiscussions';
} else if (this.isPipelineBlocked) { } else if (this.isPipelineBlocked) {
return 'pipelineBlocked'; return 'pipelineBlocked';
} else if (this.hasSHAChanged) {
return 'shaMismatch';
} else if (this.canBeMerged) { } else if (this.canBeMerged) {
return 'readyToMerge'; return 'readyToMerge';
} }
......
...@@ -4,6 +4,7 @@ import { getStateKey } from '../dependencies'; ...@@ -4,6 +4,7 @@ import { getStateKey } from '../dependencies';
export default class MergeRequestStore { export default class MergeRequestStore {
constructor(data) { constructor(data) {
this.startingSha = data.diff_head_sha;
this.setData(data); this.setData(data);
} }
...@@ -67,6 +68,7 @@ export default class MergeRequestStore { ...@@ -67,6 +68,7 @@ export default class MergeRequestStore {
this.canMerge = !!data.merge_path; this.canMerge = !!data.merge_path;
this.canCreateIssue = currentUser.can_create_issue || false; this.canCreateIssue = currentUser.can_create_issue || false;
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path; this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
this.hasSHAChanged = this.sha !== this.startingSha;
this.canBeMerged = data.can_be_merged || false; this.canBeMerged = data.can_be_merged || false;
// Cherry-pick and Revert actions related // Cherry-pick and Revert actions related
......
...@@ -16,6 +16,7 @@ const stateToComponentMap = { ...@@ -16,6 +16,7 @@ const stateToComponentMap = {
mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds', mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds',
failedToMerge: 'mr-widget-failed-to-merge', failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed', autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'mr-widget-sha-mismatch',
}; };
const statesToShowHelpWidget = [ const statesToShowHelpWidget = [
......
...@@ -3,24 +3,19 @@ import retrySVG from 'icons/_icon_action_retry.svg'; ...@@ -3,24 +3,19 @@ import retrySVG from 'icons/_icon_action_retry.svg';
import playSVG from 'icons/_icon_action_play.svg'; import playSVG from 'icons/_icon_action_play.svg';
import stopSVG from 'icons/_icon_action_stop.svg'; import stopSVG from 'icons/_icon_action_stop.svg';
/**
* For the provided action returns the respective SVG
*
* @param {String} action
* @return {SVG|String}
*/
export default function getActionIcon(action) { export default function getActionIcon(action) {
let icon; const icons = {
switch (action) { icon_action_cancel: cancelSVG,
case 'icon_action_cancel': icon_action_play: playSVG,
icon = cancelSVG; icon_action_retry: retrySVG,
break; icon_action_stop: stopSVG,
case 'icon_action_retry': };
icon = retrySVG;
break;
case 'icon_action_play':
icon = playSVG;
break;
case 'icon_action_stop':
icon = stopSVG;
break;
default:
icon = '';
}
return icon; return icons[action] || '';
} }
...@@ -41,15 +41,3 @@ export const statusIconEntityMap = { ...@@ -41,15 +41,3 @@ export const statusIconEntityMap = {
icon_status_success: SUCCESS_SVG, icon_status_success: SUCCESS_SVG,
icon_status_warning: WARNING_SVG, icon_status_warning: WARNING_SVG,
}; };
export const statusCssClasses = {
icon_status_canceled: 'canceled',
icon_status_created: 'created',
icon_status_failed: 'failed',
icon_status_manual: 'manual',
icon_status_pending: 'pending',
icon_status_running: 'running',
icon_status_skipped: 'skipped',
icon_status_success: 'success',
icon_status_warning: 'warning',
};
<script>
import ciIcon from './ci_icon.vue';
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table - first column
* - Jobs table - first column
* - Pipeline show view - header
* - Job show view - header
* - MR widget
*/
export default {
props: {
status: {
type: Object,
required: true,
},
},
components: {
ciIcon,
},
computed: {
cssClass() {
const className = this.status.group;
return className ? `ci-status ci-${this.status.group}` : 'ci-status';
},
},
};
</script>
<template>
<a
:href="status.details_path"
:class="cssClass">
<ci-icon :status="status" />
{{status.text}}
</a>
</template>
<script> <script>
import { statusIconEntityMap, statusCssClasses } from '../../vue_shared/ci_status_icons'; import { statusIconEntityMap } from '../ci_status_icons';
/**
* Renders CI icon based on API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table Badge
* - Pipelines table mini graph
* - Pipeline graph
* - Pipeline show view badge
* - Jobs table
* - Jobs show view header
* - Jobs show view sidebar
*/
export default { export default {
props: { props: {
status: { status: {
...@@ -15,7 +36,7 @@ ...@@ -15,7 +36,7 @@
}, },
cssClass() { cssClass() {
const status = statusCssClasses[this.status.icon]; const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
}, },
}, },
......
...@@ -119,14 +119,14 @@ export default { ...@@ -119,14 +119,14 @@ export default {
</div> </div>
<a v-if="hasCommitRef" <a v-if="hasCommitRef"
class="monospace branch-name" class="ref-name"
:href="commitRef.ref_url"> :href="commitRef.ref_url">
{{commitRef.name}} {{commitRef.name}}
</a> </a>
<div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div> <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
<a class="commit-id monospace" <a class="commit-sha"
:href="commitUrl"> :href="commitUrl">
{{shortSha}} {{shortSha}}
</a> </a>
......
import { statusClassToSvgMap } from '../pipeline_svg_icons';
export default {
name: 'PipelineStatusIcon',
props: {
pipelineStatus: { type: Object, required: true, default: () => ({}) },
},
computed: {
svg() {
return statusClassToSvgMap[this.pipelineStatus.icon];
},
statusClass() {
return `ci-status-icon ci-status-icon-${this.pipelineStatus.group}`;
},
},
template: `
<div :class="statusClass">
<a class="icon-link" :href="pipelineStatus.details_path">
<span v-html="svg" aria-hidden="true"></span>
</a>
</div>
`,
};
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import PipelinesStatusComponent from '../../pipelines/components/status'; import ciBadge from './ci_badge_link.vue';
import PipelinesStageComponent from '../../pipelines/components/stage.vue'; import PipelinesStageComponent from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url'; import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago'; import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
'commit-component': CommitComponent, 'commit-component': CommitComponent,
'dropdown-stage': PipelinesStageComponent, 'dropdown-stage': PipelinesStageComponent,
'pipeline-url': PipelinesUrlComponent, 'pipeline-url': PipelinesUrlComponent,
'status-scope': PipelinesStatusComponent, ciBadge,
'time-ago': PipelinesTimeagoComponent, 'time-ago': PipelinesTimeagoComponent,
}, },
...@@ -196,11 +196,20 @@ export default { ...@@ -196,11 +196,20 @@ export default {
return ''; return '';
}, },
pipelineStatus() {
if (this.pipeline.details && this.pipeline.details.status) {
return this.pipeline.details.status;
}
return {};
},
}, },
template: ` template: `
<tr class="commit"> <tr class="commit">
<status-scope :pipeline="pipeline"/> <td class="commit-link">
<ci-badge :status="pipelineStatus"/>
</td>
<pipeline-url :pipeline="pipeline"></pipeline-url> <pipeline-url :pipeline="pipeline"></pipeline-url>
......
import canceledSvg from 'icons/_icon_status_canceled.svg';
import createdSvg from 'icons/_icon_status_created.svg';
import failedSvg from 'icons/_icon_status_failed.svg';
import manualSvg from 'icons/_icon_status_manual.svg';
import pendingSvg from 'icons/_icon_status_pending.svg';
import runningSvg from 'icons/_icon_status_running.svg';
import skippedSvg from 'icons/_icon_status_skipped.svg';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import canceledBorderlessSvg from 'icons/_icon_status_canceled_borderless.svg';
import createdBorderlessSvg from 'icons/_icon_status_created_borderless.svg';
import failedBorderlessSvg from 'icons/_icon_status_failed_borderless.svg';
import manualBorderlessSvg from 'icons/_icon_status_manual_borderless.svg';
import pendingBorderlessSvg from 'icons/_icon_status_pending_borderless.svg';
import runningBorderlessSvg from 'icons/_icon_status_running_borderless.svg';
import skippedBorderlessSvg from 'icons/_icon_status_skipped_borderless.svg';
import successBorderlessSvg from 'icons/_icon_status_success_borderless.svg';
import warningBorderlessSvg from 'icons/_icon_status_warning_borderless.svg';
export const statusClassToSvgMap = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
export const statusClassToBorderlessSvgMap = {
icon_status_canceled: canceledBorderlessSvg,
icon_status_created: createdBorderlessSvg,
icon_status_failed: failedBorderlessSvg,
icon_status_manual: manualBorderlessSvg,
icon_status_pending: pendingBorderlessSvg,
icon_status_running: runningBorderlessSvg,
icon_status_skipped: skippedBorderlessSvg,
icon_status_success: successBorderlessSvg,
icon_status_warning: warningBorderlessSvg,
};
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
* Styles that apply to all GFM related forms. * Styles that apply to all GFM related forms.
*/ */
.gfm-commit,
.gfm-commit_range { .gfm-commit_range {
font-family: $monospace_font; @extend .commit-sha;
font-size: 90%;
} }
...@@ -112,7 +112,7 @@ ...@@ -112,7 +112,7 @@
} }
} }
.issue_edited_ago, .issue-edited-ago,
.note_edited_ago { .note_edited_ago {
display: none; display: none;
} }
......
...@@ -289,11 +289,6 @@ pre { ...@@ -289,11 +289,6 @@ pre {
} }
} }
.monospace {
font-family: $monospace_font;
font-size: 90%;
}
code { code {
&.key-fingerprint { &.key-fingerprint {
background: $body-bg; background: $body-bg;
...@@ -305,6 +300,24 @@ a > code { ...@@ -305,6 +300,24 @@ a > code {
color: $link-color; color: $link-color;
} }
.monospace {
font-family: $monospace_font;
}
.commit-sha,
.ref-name {
@extend .monospace;
font-size: 95%;
}
.git-revision-dropdown-toggle {
@extend .monospace;
}
.git-revision-dropdown .dropdown-content ul li a {
@extend .ref-name;
}
/** /**
* Apply Markdown typography * Apply Markdown typography
* *
......
...@@ -12,10 +12,14 @@ ...@@ -12,10 +12,14 @@
} }
&.branch-info { &.branch-info {
.monospace, .commit-sha,
.commit-info { .commit-info {
margin-left: 4px; margin-left: 4px;
} }
.ref-name {
font-size: 12px;
}
} }
} }
......
...@@ -206,11 +206,11 @@ ...@@ -206,11 +206,11 @@
margin-left: $gl-padding; margin-left: $gl-padding;
} }
} }
}
.commit-short-id { .commit-sha {
font-family: $monospace_font; font-size: 14px;
font-weight: 600; font-weight: 600;
}
} }
.commit, .commit,
...@@ -271,7 +271,7 @@ ...@@ -271,7 +271,7 @@
} }
} }
.commit-id { .commit-sha {
color: $gl-link-color; color: $gl-link-color;
} }
......
...@@ -387,7 +387,7 @@ ...@@ -387,7 +387,7 @@
padding: 0 3px 0 0; padding: 0 3px 0 0;
} }
.branch-name { .ref-name {
color: $black; color: $black;
display: inline-block; display: inline-block;
max-width: 180px; max-width: 180px;
...@@ -398,7 +398,7 @@ ...@@ -398,7 +398,7 @@
vertical-align: top; vertical-align: top;
} }
.short-sha { .commit-sha {
color: $gl-link-color; color: $gl-link-color;
line-height: 1.3; line-height: 1.3;
vertical-align: top; vertical-align: top;
......
...@@ -2,11 +2,6 @@ ...@@ -2,11 +2,6 @@
.diff-file { .diff-file {
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
.commit-short-id {
font-family: $regular_font;
font-weight: 400;
}
.file-title, .file-title,
.file-title-flex-parent { .file-title-flex-parent {
cursor: pointer; cursor: pointer;
......
...@@ -90,7 +90,7 @@ ...@@ -90,7 +90,7 @@
} }
.build-link, .build-link,
.branch-name { .ref-name {
color: $gl-text-color; color: $gl-text-color;
} }
...@@ -135,7 +135,7 @@ ...@@ -135,7 +135,7 @@
} }
.branch-commit { .branch-commit {
.commit-id { .commit-sha {
margin-right: 0; margin-right: 0;
} }
} }
......
...@@ -90,11 +90,6 @@ ...@@ -90,11 +90,6 @@
align-items: center; align-items: center;
padding: $gl-padding-top $gl-padding 0; padding: $gl-padding-top $gl-padding 0;
i,
svg {
margin-right: 8px;
}
svg { svg {
position: relative; position: relative;
top: 1px; top: 1px;
...@@ -109,9 +104,10 @@ ...@@ -109,9 +104,10 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.ci-status-icon > .icon-link svg { .icon-link > .ci-status-icon > svg {
width: 22px; width: 22px;
height: 22px; height: 22px;
margin-right: 8px;
} }
} }
...@@ -160,6 +156,34 @@ ...@@ -160,6 +156,34 @@
text-transform: capitalize; text-transform: capitalize;
} }
.label-branch {
@extend .ref-name;
color: $gl-text-color;
font-weight: bold;
overflow: hidden;
margin: 0 3px;
word-break: break-all;
&.label-truncated {
position: relative;
display: inline-block;
width: 250px;
margin-bottom: -3px;
white-space: nowrap;
text-overflow: clip;
line-height: 14px;
&::after {
position: absolute;
content: '...';
right: 0;
font-family: $regular_font;
background-color: $gray-light;
}
}
}
.js-deployment-link { .js-deployment-link {
display: inline-block; display: inline-block;
} }
...@@ -359,34 +383,6 @@ ...@@ -359,34 +383,6 @@
} }
} }
.label-branch {
color: $gl-text-color;
font-family: $monospace_font;
font-weight: bold;
overflow: hidden;
font-size: 90%;
margin: 0 3px;
word-break: break-all;
&.label-truncated {
position: relative;
display: inline-block;
width: 250px;
margin-bottom: -3px;
white-space: nowrap;
text-overflow: clip;
line-height: 14px;
&::after {
position: absolute;
content: '...';
right: 0;
font-family: $regular_font;
background-color: $gray-light;
}
}
}
.commits-empty { .commits-empty {
text-align: center; text-align: center;
......
...@@ -238,11 +238,6 @@ ul.notes { ...@@ -238,11 +238,6 @@ ul.notes {
ul { ul {
margin: 3px 0 3px 16px !important; margin: 3px 0 3px 16px !important;
.gfm-commit {
font-family: $monospace_font;
font-size: 12px;
}
} }
p:first-child { p:first-child {
......
...@@ -158,9 +158,13 @@ ...@@ -158,9 +158,13 @@
float: none; float: none;
} }
.api {
@extend .monospace;
}
.branch-commit { .branch-commit {
.branch-name { .ref-name {
font-weight: bold; font-weight: bold;
max-width: 120px; max-width: 120px;
overflow: hidden; overflow: hidden;
...@@ -182,7 +186,7 @@ ...@@ -182,7 +186,7 @@
color: $gl-text-color; color: $gl-text-color;
} }
.commit-id { .commit-sha {
color: $gl-link-color; color: $gl-link-color;
} }
......
...@@ -657,9 +657,8 @@ pre.light-well { ...@@ -657,9 +657,8 @@ pre.light-well {
color: $gl-text-color; color: $gl-text-color;
} }
.commit_short_id { .commit-sha {
margin-right: 5px; margin-right: 5px;
color: $gl-link-color;
font-weight: 600; font-weight: 600;
} }
...@@ -825,7 +824,8 @@ pre.light-well { ...@@ -825,7 +824,8 @@ pre.light-well {
} }
.compare-form-group { .compare-form-group {
.dropdown-menu { .dropdown-menu,
.inline-input-group {
width: 100%; width: 100%;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
...@@ -844,14 +844,6 @@ pre.light-well { ...@@ -844,14 +844,6 @@ pre.light-well {
width: auto; width: auto;
} }
} }
.inline-input-group {
width: 100%;
@media (min-width: $screen-sm-min) {
width: 250px;
}
}
} }
.clearable-input { .clearable-input {
......
...@@ -60,17 +60,24 @@ module IssuableActions ...@@ -60,17 +60,24 @@ module IssuableActions
end end
def bulk_update_params def bulk_update_params
params.require(:update).permit( permitted_keys = [
:issuable_ids, :issuable_ids,
:assignee_id, :assignee_id,
:milestone_id, :milestone_id,
:state_event, :state_event,
:subscription_event, :subscription_event,
assignee_ids: [],
label_ids: [], label_ids: [],
add_label_ids: [], add_label_ids: [],
remove_label_ids: [] remove_label_ids: []
) ]
if resource_name == 'issue'
permitted_keys << { assignee_ids: [] }
else
permitted_keys.unshift(:assignee_id)
end
params.require(:update).permit(permitted_keys)
end end
def resource_name def resource_name
......
...@@ -4,7 +4,7 @@ module RoutableActions ...@@ -4,7 +4,7 @@ module RoutableActions
def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil) def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil)
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?) routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
if routable_authorized?(routable_klass, routable, extra_authorization_proc) if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path) ensure_canonical_path(routable, requested_full_path)
routable routable
else else
...@@ -13,8 +13,8 @@ module RoutableActions ...@@ -13,8 +13,8 @@ module RoutableActions
end end
end end
def routable_authorized?(routable_klass, routable, extra_authorization_proc) def routable_authorized?(routable, extra_authorization_proc)
action = :"read_#{routable_klass.to_s.underscore}" action = :"read_#{routable.class.to_s.underscore}"
return false unless can?(current_user, action, routable) return false unless can?(current_user, action, routable)
if extra_authorization_proc if extra_authorization_proc
...@@ -30,7 +30,7 @@ module RoutableActions ...@@ -30,7 +30,7 @@ module RoutableActions
canonical_path = routable.full_path canonical_path = routable.full_path
if canonical_path != requested_path if canonical_path != requested_path
if canonical_path.casecmp(requested_path) != 0 if canonical_path.casecmp(requested_path) != 0
flash[:notice] = "Project '#{requested_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path." flash[:notice] = "#{routable.class.to_s.titleize} '#{requested_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path."
end end
redirect_to request.original_url.sub(requested_path, canonical_path) redirect_to request.original_url.sub(requested_path, canonical_path)
end end
......
...@@ -208,7 +208,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -208,7 +208,6 @@ class Projects::IssuesController < Projects::ApplicationController
description: view_context.markdown_field(@issue, :description), description: view_context.markdown_field(@issue, :description),
description_text: @issue.description, description_text: @issue.description,
task_status: @issue.task_status, task_status: @issue.task_status,
issue_number: @issue.iid,
updated_at: @issue.updated_at updated_at: @issue.updated_at
} }
end end
......
...@@ -42,7 +42,10 @@ module ButtonHelper ...@@ -42,7 +42,10 @@ module ButtonHelper
class: "btn #{css_class}", class: "btn #{css_class}",
data: data, data: data,
type: :button, type: :button,
title: title title: title,
aria: {
label: title
}
end end
def http_clone_button(project, placement = 'right', append_link: true) def http_clone_button(project, placement = 'right', append_link: true)
......
...@@ -74,12 +74,8 @@ module CommitsHelper ...@@ -74,12 +74,8 @@ module CommitsHelper
# Returns the sorted alphabetically links to branches, separated by a comma # Returns the sorted alphabetically links to branches, separated by a comma
def commit_branches_links(project, branches) def commit_branches_links(project, branches)
branches.sort.map do |branch| branches.sort.map do |branch|
link_to( link_to(project_ref_path(project, branch), class: "label label-gray ref-name") do
namespace_project_tree_path(project.namespace, project, branch) icon('code-fork') + " #{branch}"
) do
content_tag :span, class: 'label label-gray' do
icon('code-fork') + ' ' + branch
end
end end
end.join(" ").html_safe end.join(" ").html_safe
end end
...@@ -88,13 +84,8 @@ module CommitsHelper ...@@ -88,13 +84,8 @@ module CommitsHelper
def commit_tags_links(project, tags) def commit_tags_links(project, tags)
sorted = VersionSorter.rsort(tags) sorted = VersionSorter.rsort(tags)
sorted.map do |tag| sorted.map do |tag|
link_to( link_to(project_ref_path(project, tag), class: "label label-gray ref-name") do
namespace_project_commits_path(project.namespace, project, icon('tag') + " #{tag}"
project.repository.find_tag(tag).name)
) do
content_tag :span, class: 'label label-gray' do
icon('tag') + ' ' + tag
end
end end
end.join(" ").html_safe end.join(" ").html_safe
end end
...@@ -198,8 +189,8 @@ module CommitsHelper ...@@ -198,8 +189,8 @@ module CommitsHelper
tree_join(commit_sha, diff_new_path)), tree_join(commit_sha, diff_new_path)),
class: 'btn view-file js-view-file' class: 'btn view-file js-view-file'
) do ) do
raw('View file @') + content_tag(:span, commit_sha[0..6], raw('View file @ ') + content_tag(:span, Commit.truncate_sha(commit_sha),
class: 'commit-short-id') class: 'commit-sha')
end end
end end
......
...@@ -63,7 +63,7 @@ module DiffHelper ...@@ -63,7 +63,7 @@ module DiffHelper
def parallel_diff_discussions(left, right, diff_file) def parallel_diff_discussions(left, right, diff_file)
return unless @grouped_diff_discussions return unless @grouped_diff_discussions
discussions_left = discussions_right = nil discussions_left = discussions_right = nil
if left && (left.unchanged? || left.removed?) if left && (left.unchanged? || left.removed?)
...@@ -98,7 +98,7 @@ module DiffHelper ...@@ -98,7 +98,7 @@ module DiffHelper
[ [
content_tag(:span, link_to(truncate(blob.name, length: 40), tree)), content_tag(:span, link_to(truncate(blob.name, length: 40), tree)),
'@', '@',
content_tag(:span, commit_id, class: 'monospace') content_tag(:span, commit_id, class: 'commit-sha')
].join(' ').html_safe ].join(' ').html_safe
end end
......
...@@ -164,9 +164,14 @@ module EventsHelper ...@@ -164,9 +164,14 @@ module EventsHelper
def event_note_title_html(event) def event_note_title_html(event)
if event.note_target if event.note_target
link_to(event_note_target_path(event), title: event.target_title, class: 'has-tooltip') do text = raw("#{event.note_target_type} ") +
"#{event.note_target_type} #{event.note_target_reference}" if event.commit_note?
end content_tag(:span, event.note_target_reference, class: 'commit-sha')
else
event.note_target_reference
end
link_to(text, event_note_target_path(event), title: event.target_title, class: 'has-tooltip')
else else
content_tag(:strong, '(deleted)') content_tag(:strong, '(deleted)')
end end
......
...@@ -54,6 +54,10 @@ module GitlabRoutingHelper ...@@ -54,6 +54,10 @@ module GitlabRoutingHelper
namespace_project_builds_path(project.namespace, project, *args) namespace_project_builds_path(project.namespace, project, *args)
end end
def project_ref_path(project, ref_name, *args)
namespace_project_commits_path(project.namespace, project, ref_name, *args)
end
def project_container_registry_path(project, *args) def project_container_registry_path(project, *args)
namespace_project_container_registry_index_path(project.namespace, project, *args) namespace_project_container_registry_index_path(project.namespace, project, *args)
end end
......
...@@ -67,9 +67,10 @@ module IssuablesHelper ...@@ -67,9 +67,10 @@ module IssuablesHelper
end end
def users_dropdown_label(selected_users) def users_dropdown_label(selected_users)
if selected_users.length == 0 case selected_users.length
when 0
"Unassigned" "Unassigned"
elsif selected_users.length == 1 when 1
selected_users[0].name selected_users[0].name
else else
"#{selected_users[0].name} + #{selected_users.length - 1} more" "#{selected_users[0].name} + #{selected_users.length - 1} more"
...@@ -136,11 +137,9 @@ module IssuablesHelper ...@@ -136,11 +137,9 @@ module IssuablesHelper
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
end end
if issuable.tasks? output << "&ensp;".html_safe
output << "&ensp;".html_safe output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm")
output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg")
output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg")
end
output output
end end
......
...@@ -24,10 +24,13 @@ module TodosHelper ...@@ -24,10 +24,13 @@ module TodosHelper
end end
def todo_target_link(todo) def todo_target_link(todo)
target = todo.target_type.titleize.downcase text = raw("#{todo.target_type.titleize.downcase} ") +
link_to "#{target} #{todo.target_reference}", todo_target_path(todo), if todo.for_commit?
class: 'has-tooltip', content_tag(:span, todo.target_reference, class: 'commit-sha')
title: todo.target.title else
todo.target_reference
end
link_to text, todo_target_path(todo), class: 'has-tooltip', title: todo.target.title
end end
def todo_target_path(todo) def todo_target_path(todo)
......
...@@ -326,10 +326,7 @@ class Commit ...@@ -326,10 +326,7 @@ class Commit
end end
def raw_diffs(*args) def raw_diffs(*args)
use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs) if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
if use_gitaly && !deltas_only
Gitlab::GitalyClient::Commit.diff_from_parent(self, *args) Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
else else
raw.diffs(*args) raw.diffs(*args)
......
...@@ -44,14 +44,15 @@ module Mentionable ...@@ -44,14 +44,15 @@ module Mentionable
end end
def all_references(current_user = nil, extractor: nil) def all_references(current_user = nil, extractor: nil)
@extractors ||= {}
# Use custom extractor if it's passed in the function parameters. # Use custom extractor if it's passed in the function parameters.
if extractor if extractor
@extractor = extractor @extractors[current_user] = extractor
else else
@extractor ||= Gitlab::ReferenceExtractor. extractor = @extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user)
new(project, current_user)
@extractor.reset_memoized_values extractor.reset_memoized_values
end end
self.class.mentionable_attrs.each do |attr, options| self.class.mentionable_attrs.each do |attr, options|
...@@ -62,10 +63,10 @@ module Mentionable ...@@ -62,10 +63,10 @@ module Mentionable
skip_project_check: skip_project_check? skip_project_check: skip_project_check?
) )
@extractor.analyze(text, options) extractor.analyze(text, options)
end end
@extractor extractor
end end
def mentioned_users(current_user = nil) def mentioned_users(current_user = nil)
......
...@@ -7,5 +7,27 @@ module ProtectedBranchAccess ...@@ -7,5 +7,27 @@ module ProtectedBranchAccess
belongs_to :protected_branch belongs_to :protected_branch
delegate :project, to: :protected_branch delegate :project, to: :protected_branch
validates :access_level, presence: true, inclusion: {
in: [
Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER,
Gitlab::Access::NO_ACCESS
]
}
def self.human_access_levels
{
Gitlab::Access::MASTER => "Masters",
Gitlab::Access::DEVELOPER => "Developers + Masters",
Gitlab::Access::NO_ACCESS => "No one"
}.with_indifferent_access
end
def check_access(user)
return false if access_level == Gitlab::Access::NO_ACCESS
super
end
end end
end end
...@@ -174,7 +174,7 @@ class Issue < ActiveRecord::Base ...@@ -174,7 +174,7 @@ class Issue < ActiveRecord::Base
# Returns boolean if a related branch exists for the current issue # Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs # ignores merge requests branchs
def has_related_branch? def has_related_branch?
project.repository.branch_names.any? do |branch| project.repository.branch_names.any? do |branch|
/\A#{iid}-(?!\d+-stable)/i =~ branch /\A#{iid}-(?!\d+-stable)/i =~ branch
end end
......
...@@ -3,11 +3,4 @@ class IssueAssignee < ActiveRecord::Base ...@@ -3,11 +3,4 @@ class IssueAssignee < ActiveRecord::Base
belongs_to :issue belongs_to :issue
belongs_to :assignee, class_name: "User", foreign_key: :user_id belongs_to :assignee, class_name: "User", foreign_key: :user_id
after_create :update_assignee_cache_counts
after_destroy :update_assignee_cache_counts
def update_assignee_cache_counts
assignee&.update_cache_counts
end
end end
...@@ -125,7 +125,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -125,7 +125,6 @@ class MergeRequest < ActiveRecord::Base
participant :assignee participant :assignee
after_save :keep_around_commit after_save :keep_around_commit
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
def self.reference_prefix def self.reference_prefix
'!' '!'
...@@ -187,13 +186,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -187,13 +186,6 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}" work_in_progress?(title) ? title : "WIP: #{title}"
end end
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist)
previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
previous_assignee&.update_cache_counts
assignee&.update_cache_counts
end
# Returns a Hash of attributes to be used for Twitter card metadata # Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes def card_attributes
{ {
......
...@@ -35,7 +35,7 @@ module ChatMessage ...@@ -35,7 +35,7 @@ module ChatMessage
def activity def activity
{ {
title: "Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status}", title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: "in #{pretty_duration(duration)}", text: "in #{pretty_duration(duration)}",
image: user_avatar || '' image: user_avatar || ''
...@@ -45,7 +45,7 @@ module ChatMessage ...@@ -45,7 +45,7 @@ module ChatMessage
private private
def message def message
"#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{pretty_duration(duration)}" "#{project_link}: Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status} in #{pretty_duration(duration)}"
end end
def humanized_status def humanized_status
...@@ -70,7 +70,7 @@ module ChatMessage ...@@ -70,7 +70,7 @@ module ChatMessage
end end
def branch_link def branch_link
"[#{ref}](#{branch_url})" "`[#{ref}](#{branch_url})`"
end end
def project_link def project_link
......
...@@ -61,7 +61,7 @@ module ChatMessage ...@@ -61,7 +61,7 @@ module ChatMessage
end end
def removed_branch_message def removed_branch_message
"#{user_name} removed #{ref_type} #{ref} from #{project_link}" "#{user_name} removed #{ref_type} `#{ref}` from #{project_link}"
end end
def push_message def push_message
...@@ -102,7 +102,7 @@ module ChatMessage ...@@ -102,7 +102,7 @@ module ChatMessage
end end
def branch_link def branch_link
"[#{ref}](#{branch_url})" "`[#{ref}](#{branch_url})`"
end end
def project_link def project_link
......
class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess include ProtectedBranchAccess
validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER] }
def self.human_access_levels
{
Gitlab::Access::MASTER => "Masters",
Gitlab::Access::DEVELOPER => "Developers + Masters"
}.with_indifferent_access
end
end end
class ProtectedBranch::PushAccessLevel < ActiveRecord::Base class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess include ProtectedBranchAccess
validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER,
Gitlab::Access::NO_ACCESS] }
def self.human_access_levels
{
Gitlab::Access::MASTER => "Masters",
Gitlab::Access::DEVELOPER => "Developers + Masters",
Gitlab::Access::NO_ACCESS => "No one"
}.with_indifferent_access
end
def check_access(user)
return false if access_level == Gitlab::Access::NO_ACCESS
super
end
end end
...@@ -517,8 +517,8 @@ class Repository ...@@ -517,8 +517,8 @@ class Repository
cache_method :avatar cache_method :avatar
def readme def readme
if head = tree(:head) if readme = tree(:head)&.readme
ReadmeBlob.new(head.readme, self) ReadmeBlob.new(readme, self)
end end
end end
......
...@@ -929,6 +929,11 @@ class User < ActiveRecord::Base ...@@ -929,6 +929,11 @@ class User < ActiveRecord::Base
assigned_open_issues_count(force: true) assigned_open_issues_count(force: true)
end end
def invalidate_cache_counts
Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
end
def todos_done_count(force: false) def todos_done_count(force: false)
Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
TodosFinder.new(self, state: :done).execute.count TodosFinder.new(self, state: :done).execute.count
......
...@@ -38,10 +38,7 @@ class PipelineEntity < Grape::Entity ...@@ -38,10 +38,7 @@ class PipelineEntity < Grape::Entity
expose :path do |pipeline| expose :path do |pipeline|
if pipeline.ref if pipeline.ref
namespace_project_tree_path( project_ref_path(pipeline.project, pipeline.ref)
pipeline.project.namespace,
pipeline.project,
id: pipeline.ref)
end end
end end
......
...@@ -3,6 +3,7 @@ module RequestAwareEntity ...@@ -3,6 +3,7 @@ module RequestAwareEntity
included do included do
include Gitlab::Routing include Gitlab::Routing
include GitlabRoutingHelper
include Gitlab::Allowable include Gitlab::Allowable
end end
......
...@@ -7,7 +7,7 @@ module Issuable ...@@ -7,7 +7,7 @@ module Issuable
ids = params.delete(:issuable_ids).split(",") ids = params.delete(:issuable_ids).split(",")
items = model_class.where(id: ids) items = model_class.where(id: ids)
%i(state_event milestone_id assignee_id assignee_ids add_label_ids remove_label_ids subscription_event).each do |key| permitted_attrs(type).each do |key|
params.delete(key) unless params[key].present? params.delete(key) unless params[key].present?
end end
...@@ -26,5 +26,17 @@ module Issuable ...@@ -26,5 +26,17 @@ module Issuable
success: !items.count.zero? success: !items.count.zero?
} }
end end
private
def permitted_attrs(type)
attrs = %i(state_event milestone_id assignee_id assignee_ids add_label_ids remove_label_ids subscription_event)
if type == 'issue'
attrs.push(:assignee_ids)
else
attrs.push(:assignee_id)
end
end
end end
end end
...@@ -178,6 +178,7 @@ class IssuableBaseService < BaseService ...@@ -178,6 +178,7 @@ class IssuableBaseService < BaseService
after_create(issuable) after_create(issuable)
issuable.create_cross_references!(current_user) issuable.create_cross_references!(current_user)
execute_hooks(issuable) execute_hooks(issuable)
issuable.assignees.each(&:invalidate_cache_counts)
end end
issuable issuable
...@@ -234,6 +235,11 @@ class IssuableBaseService < BaseService ...@@ -234,6 +235,11 @@ class IssuableBaseService < BaseService
old_assignees: old_assignees old_assignees: old_assignees
) )
if old_assignees != issuable.assignees
assignees = old_assignees + issuable.assignees.to_a
assignees.compact.each(&:invalidate_cache_counts)
end
after_update(issuable) after_update(issuable)
issuable.create_new_cross_references!(current_user) issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update') execute_hooks(issuable, 'update')
......
...@@ -43,8 +43,9 @@ module Members ...@@ -43,8 +43,9 @@ module Members
) )
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
member.user.update_cache_counts
end end
member.user.invalidate_cache_counts
end end
end end
end end
...@@ -79,7 +79,7 @@ module SystemNoteService ...@@ -79,7 +79,7 @@ module SystemNoteService
text_parts.join(' and ') text_parts.join(' and ')
elsif old_assignees.any? elsif old_assignees.any?
"removed all assignees" "removed assignee"
elsif issue.assignees.any? elsif issue.assignees.any?
"assigned to #{issue.assignees.map(&:to_reference).to_sentence}" "assigned to #{issue.assignees.map(&:to_reference).to_sentence}"
end end
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
- commit = discussion.noteable - commit = discussion.noteable
- if commit - if commit
commit commit
= link_to commit.short_id, url, class: 'monospace' = link_to commit.short_id, url, class: 'commit-sha'
- else - else
a deleted commit a deleted commit
- elsif discussion.diff_discussion? - elsif discussion.diff_discussion?
......
%li.commit %li.commit
.commit-row-title .commit-row-title
= link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id]) = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit-sha", alt: '', title: truncate_sha(commit[:id])
&middot; &middot;
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author
...@@ -46,6 +46,3 @@ ...@@ -46,6 +46,3 @@
.form-actions .form-actions
= submit_tag 'Continue to the next step', class: 'btn btn-create' = submit_tag 'Continue to the next step', class: 'btn btn-create'
:javascript
new UsersSelect();
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
.location-badge= label .location-badge= label
.search-input-wrap .search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } } .dropdown{ data: { url: search_autocomplete_path } }
= search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url } = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' }
.dropdown-menu.dropdown-select .dropdown-menu.dropdown-select
= dropdown_content do = dropdown_content do
%ul %ul
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
= ci_icon_for_status(status) = ci_icon_for_status(status)
= ci_text_for_status(status) = ci_text_for_status(status)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-sha"
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
&middot; &middot;
#{time_ago_with_tooltip(commit.committed_date)} by #{time_ago_with_tooltip(commit.committed_date)} by
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
.event-last-push .event-last-push
.event-last-push-text .event-last-push-text
%span You pushed to %span You pushed to
= link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name, class: 'commit-sha') do
%strong= event.ref_name %strong= event.ref_name
- if @project && event.project != @project - if @project && event.project != @project
%span at %span at
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
%strong %strong
= link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark" = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark"
.pull-right .pull-right
= link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace" = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "commit-sha"
&nbsp; &nbsp;
.light .light
= commit_author_link(commit, avatar: false) = commit_author_link(commit, avatar: false)
......
...@@ -6,7 +6,8 @@ ...@@ -6,7 +6,8 @@
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
%li{ class: "js-branch-#{branch.name}" } %li{ class: "js-branch-#{branch.name}" }
%div %div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do = link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated ref-name' do
= icon('code-fork')
= branch.name = branch.name
&nbsp; &nbsp;
- if branch.name == @repository.root_ref - if branch.name == @repository.root_ref
......
.branch-commit .branch-commit
.icon-container.commit-icon .icon-container.commit-icon
= custom_icon("icon_commit") = custom_icon("icon_commit")
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-id monospace" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-sha"
&middot; &middot;
%span.str-truncated %span.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
......
...@@ -17,11 +17,13 @@ ...@@ -17,11 +17,13 @@
.help-block.text-danger.js-branch-name-error .help-block.text-danger.js-branch-name-error
.form-group .form-group
= label_tag :ref, 'Create from', class: 'control-label' = label_tag :ref, 'Create from', class: 'control-label'
.col-sm-10.dropdown.create-from .col-sm-10.create-from
= hidden_field_tag :ref, default_ref .dropdown
= button_tag type: 'button', title: default_ref, class: 'dropdown-toggle form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do = hidden_field_tag :ref, default_ref
.text-left.dropdown-toggle-text= default_ref = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
= render 'shared/ref_dropdown', dropdown_class: 'wide' .text-left.dropdown-toggle-text= default_ref
= icon('chevron-down')
= render 'shared/ref_dropdown', dropdown_class: 'wide'
.help-block Existing branch name, tag, or commit SHA .help-block Existing branch name, tag, or commit SHA
.form-actions .form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
......
...@@ -6,18 +6,16 @@ ...@@ -6,18 +6,16 @@
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
%strong %strong
Job Job
= link_to namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id' do = link_to "##{@build.id}", namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id'
\##{@build.id}
in pipeline in pipeline
= link_to pipeline_path(pipeline) do %strong
%strong ##{pipeline.id} = link_to "##{pipeline.id}", pipeline_path(pipeline)
for commit for
= link_to namespace_project_commit_path(@project.namespace, @project, pipeline.sha) do %strong
%strong= pipeline.short_sha = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: 'commit-sha'
from from
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do %strong
%code = link_to @build.ref, project_ref_path(@project, @build.ref), class: 'ref-name'
= @build.ref
= render "projects/builds/user" if @build.user = render "projects/builds/user" if @build.user
......
...@@ -23,14 +23,14 @@ ...@@ -23,14 +23,14 @@
- if job.ref - if job.ref
.icon-container .icon-container
= job.tag? ? icon('tag') : icon('code-fork') = job.tag? ? icon('tag') : icon('code-fork')
= link_to job.ref, namespace_project_commits_path(job.project.namespace, job.project, job.ref), class: "monospace branch-name" = link_to job.ref, project_ref_path(job.project, job.ref), class: "ref-name"
- else - else
.light none .light none
.icon-container.commit-icon .icon-container.commit-icon
= custom_icon("icon_commit") = custom_icon("icon_commit")
- if commit_sha - if commit_sha
= link_to job.short_sha, namespace_project_commit_path(job.project.namespace, job.project, job.sha), class: "commit-id monospace" = link_to job.short_sha, namespace_project_commit_path(job.project.namespace, job.project, job.sha), class: "commit-sha"
- if job.stuck? - if job.stuck?
= icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.') = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.')
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
- if pipeline.user - if pipeline.user
= user_avatar(user: pipeline.user, size: 20) = user_avatar(user: pipeline.user, size: 20)
- else - else
%span.monospace API %span.api API
- if admin - if admin
%td %td
......
.page-content-header .page-content-header
.header-main-content .header-main-content
%strong Commit #{@commit.short_id} %strong
Commit
%span.commit-sha= @commit.short_id
= clipboard_button(text: @commit.id, title: "Copy commit SHA to clipboard") = clipboard_button(text: @commit.id, title: "Copy commit SHA to clipboard")
%span.hidden-xs authored %span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)} #{time_ago_with_tooltip(@commit.authored_date)}
...@@ -57,7 +59,7 @@ ...@@ -57,7 +59,7 @@
= custom_icon("icon_commit") = custom_icon("icon_commit")
%span.cgray= pluralize(@commit.parents.count, "parent") %span.cgray= pluralize(@commit.parents.count, "parent")
- @commit.parents.each do |parent| - @commit.parents.each do |parent|
= link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace" = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "commit-sha"
%span.commit-info.branches %span.commit-info.branches
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
...@@ -68,9 +70,10 @@ ...@@ -68,9 +70,10 @@
= link_to namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) do = link_to namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) do
= ci_icon_for_status(last_pipeline.status) = ci_icon_for_status(last_pipeline.status)
Pipeline Pipeline
= link_to "##{last_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id), class: "monospace" = link_to "##{last_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id)
= ci_label_for_status(last_pipeline.status) = ci_label_for_status(last_pipeline.status)
- if last_pipeline.stages.any? - if last_pipeline.stages.any?
with #{"stage".pluralize(last_pipeline.stages.count)}
.mr-widget-pipeline-graph .mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph' = render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph'
in in
......
.pipeline-graph-container
.row-content-block.build-content.middle-block.pipeline-actions
.pull-right
- if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?)
= link_to "Retry", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'js-retry-button btn btn-grouped btn-primary', method: :post
- if pipeline.builds.running_or_pending.any?
= link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
.oneline.clearfix
- if defined?(pipeline_details) && pipeline_details
Pipeline
= link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
with
= pluralize pipeline.statuses.count(:id), "job"
- if pipeline.ref
for
= link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
- if defined?(link_to_commit) && link_to_commit
for commit
= link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace"
- if pipeline.duration
in
= time_interval_in_words pipeline.duration
.row-content-block.build-content.middle-block.js-pipeline-graph.hidden
= render "projects/pipelines/graph", pipeline: pipeline
- if pipeline.yaml_errors.present?
.bs-callout.bs-callout-danger
%h4 Found errors in your .gitlab-ci.yml:
%ul
- pipeline.yaml_errors.split(",").each do |error|
%li= error
You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
.bs-callout.bs-callout-warning
\.gitlab-ci.yml not found in this commit
.table-holder.pipeline-holder
%table.table.ci-table.pipeline
%thead
%tr
%th Status
%th Job ID
%th Name
%th
%th Coverage
%th
= render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage
- if @branches.any? - if @branches.any? || @tags.any?
%span - branch = commit_default_branch(@project, @branches)
- branch = commit_default_branch(@project, @branches) = link_to(project_ref_path(@project, branch), class: "label label-gray ref-name") do
= link_to(namespace_project_tree_path(@project.namespace, @project, branch)) do = icon('code-fork')
%span.label.label-gray = branch
= branch
- if @branches.any? || @tags.any? -# `commit_default_branch` deletes the default branch from `@branches`,
= link_to("#", class: "js-details-expand") do -# so only render this if we have more branches left
%span.label.label-gray - if @branches.any? || @tags.any?
\... %span
= link_to "…", "#", class: "js-details-expand label label-gray"
%span.js-details-content.hide %span.js-details-content.hide
- if @branches.any? = commit_branches_links(@project, @branches) if @branches.any?
= commit_branches_links(@project, @branches) = commit_tags_links(@project, @tags) if @tags.any?
- if @tags.any?
= commit_tags_links(@project, @tags)
...@@ -37,6 +37,6 @@ ...@@ -37,6 +37,6 @@
.commit-actions.flex-row.hidden-xs .commit-actions.flex-row.hidden-xs
- if commit.status(ref) - if commit.status(ref)
= render_commit_status(commit, ref: ref) = render_commit_status(commit, ref: ref)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-sha btn btn-transparent"
= clipboard_button(text: commit.id, title: "Copy commit SHA to clipboard") = clipboard_button(text: commit.id, title: "Copy commit SHA to clipboard")
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
= link_to_browse_code(project, commit) = link_to_browse_code(project, commit)
%li.commit.inline-commit %li.commit.inline-commit
.commit-row-title .commit-row-title
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-sha"
&nbsp; &nbsp;
%span.str-truncated %span.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment