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:
- webpack-report/
rake karma:
cache:
paths:
- vendor/ruby
stage: test
<<: *use-pg
<<: *dedicated-runner
......
......@@ -33,7 +33,7 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
<span>
{{ __('FirstPushedBy|First') }}
<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') }}
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
......
......@@ -26,9 +26,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<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>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
......
......@@ -29,9 +29,9 @@ global.cycleAnalytics.StageTestComponent = Vue.extend({
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<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>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
......
......@@ -14,7 +14,6 @@
/* global NotificationsForm */
/* global TreeView */
/* global NotificationsDropdown */
/* global UsersSelect */
/* global GroupAvatar */
/* global LineHighlighter */
/* global ProjectFork */
......@@ -52,6 +51,7 @@ import ShortcutsWiki from './shortcuts_wiki';
import Pipelines from './pipelines';
import BlobViewer from './blob/viewer/index';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import UsersSelect from './users_select';
const ShortcutsBlob = require('./shortcuts_blob');
......@@ -113,6 +113,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:boards:show':
case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;
case 'projects:builds:show':
new Build();
......@@ -127,6 +128,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
});
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;
case 'projects:issues:show':
new Issue();
......@@ -139,6 +141,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
new Milestone();
new Sidebar();
break;
case 'groups:issues':
case 'groups:merge_requests':
new UsersSelect();
break;
case 'dashboard:todos:index':
new gl.Todos();
break;
......@@ -223,6 +229,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'dashboard:activity':
new gl.Activities();
break;
case 'dashboard:issues':
case 'dashboard:merge_requests':
new UsersSelect();
break;
case 'projects:commit:show':
new Commit();
new gl.Diff();
......@@ -377,6 +387,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
new LineHighlighter();
new BlobViewer();
break;
case 'import:fogbugz:new_user_map':
new UsersSelect();
break;
}
switch (path.first()) {
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 */
/* global UsersSelect */
/* global bp */
import Cookies from 'js-cookie';
import UsersSelect from './users_select';
(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 */
/* global GitLab */
/* global UsersSelect */
/* global ZenMode */
/* global Autosave */
/* global dateFormat */
/* global Pikaday */
import UsersSelect from './users_select';
(function() {
this.IssuableForm = (function() {
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 IssueTitle from './issue_title_description.vue';
import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor';
(() => {
const issueTitleData = document.querySelector('.issue-title-data').dataset;
const { canUpdateTasksClass, endpoint } = issueTitleData;
document.addEventListener('DOMContentLoaded', () => new Vue({
el: document.getElementById('js-issuable-app'),
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({
el: '.issue-title-entrypoint',
render: createElement => createElement(IssueTitle, {
return {
canUpdate: gl.utils.convertPermissionToBoolean(canUpdate),
endpoint,
issuableRef,
initialTitle: issuableTitleElement.innerHTML,
initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
};
},
render(createElement) {
return createElement('issuable-app', {
props: {
canUpdateTasksClass,
endpoint,
canUpdate: this.canUpdate,
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 {
constructor(resource, endpoint) {
this.resource = resource;
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(this.endpoint);
}
getTitle() {
return this.resource.get(this.endpoint);
getData() {
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';
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
illustrationSvg,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
......@@ -28,13 +30,15 @@ export default {
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>Scheduling Pipelines</h4>
<p>
The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags.
<p>
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.
</p>
<p> Learn more in the
<!-- FIXME -->
<a href="random.com">pipeline schedules documentation</a>.
<a
:href="docsUrl"
target="_blank"
rel="nofollow">pipeline schedules documentation</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
......
import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout';
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
document.addEventListener('DOMContentLoaded', () => {
new PipelineSchedulesCalloutComponent()
.$mount('#scheduling-pipelines-callout');
});
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout',
components: {
'pipeline-schedules-callout': PipelineSchedulesCallout,
},
render(createElement) {
return createElement('pipeline-schedules-callout');
},
}));
......@@ -29,7 +29,7 @@ export default {
</a>
<span
v-if="!user"
class="js-pipeline-url-api api monospace">
class="js-pipeline-url-api api">
API
</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 */
/* global UsersSelect */
import UsersSelect from './users_select';
class Todos {
constructor() {
......
This diff is collapsed.
/* global Flash */
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 MRWidgetService from '../services/mr_widget_service';
......@@ -16,7 +16,7 @@ export default {
},
computed: {
svg() {
return statusClassToSvgMap.icon_status_success;
return statusIconEntityMap.icon_status_success;
},
},
methods: {
......
......@@ -70,32 +70,34 @@ export default {
</span>
</div>
<div class="normal">
<b>Request to merge</b>
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
v-html="mr.sourceBranchLink"></span>
<button
class="btn btn-transparent btn-clipboard has-tooltip"
data-title="Copy branch name to clipboard"
:data-clipboard-text="branchNameClipboardData">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
<b>into</b>
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a
:href="mr.targetBranchCommitsPath">
{{mr.targetBranch}}
</a>
</span>
<strong>
Request to merge
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
v-html="mr.sourceBranchLink"></span>
<button
class="btn btn-transparent btn-clipboard has-tooltip"
data-title="Copy branch name to clipboard"
:data-clipboard-text="branchNameClipboardData">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
into
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a
:href="mr.targetBranchPath">
{{mr.targetBranch}}
</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
......
import PipelineStage from '../../pipelines/components/stage';
import pipelineStatusIcon from '../../vue_shared/components/pipeline_status_icon';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
import PipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
export default {
name: 'MRWidgetPipeline',
......@@ -9,7 +9,7 @@ export default {
},
components: {
'pipeline-stage': PipelineStage,
'pipeline-status-icon': pipelineStatusIcon,
ciIcon,
},
computed: {
hasCIError() {
......@@ -18,11 +18,14 @@ export default {
return hasCI && !ciStatus;
},
svg() {
return statusClassToSvgMap.icon_status_failed;
return statusIconEntityMap.icon_status_failed;
},
stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
},
status() {
return this.mr.pipeline.details.status || {};
},
},
template: `
<div class="mr-widget-heading">
......@@ -38,13 +41,22 @@ export default {
<span>Could not connect to the CI server. Please check your settings and try again.</span>
</template>
<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>
Pipeline
<a
:href="mr.pipeline.path"
class="pipeline-id">#{{mr.pipeline.id}}</a>
{{mr.pipeline.details.status.label}}
</span>
<span
v-if="mr.pipeline.details.stages.length > 0">
with {{stageText}}
</span>
<div class="mr-widget-pipeline-graph">
......@@ -61,7 +73,7 @@ export default {
for
<a
:href="mr.pipeline.commit.commit_path"
class="monospace js-commit-link">
class="commit-sha js-commit-link">
{{mr.pipeline.commit.short_id}}</a>.
</span>
<span
......
......@@ -20,7 +20,7 @@ export default {
<p>
The changes were not merged into
<a
:href="mr.targetBranchCommitsPath"
:href="mr.targetBranchPath"
class="label-branch">
{{mr.targetBranch}}</a>.
</p>
......
......@@ -16,7 +16,7 @@ export default {
The changes will be merged into
<span class="label-branch">
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
</span>
</span>.
</p>
</section>
</div>
......
......@@ -87,7 +87,7 @@ export default {
:href="mr.targetBranchPath"
class="label-branch">
{{mr.targetBranch}}
</a>
</a>.
</p>
<p v-if="mr.shouldRemoveSourceBranch">
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
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 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 PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
......
......@@ -16,6 +16,7 @@ import {
MissingBranchState,
NotAllowedState,
ReadyToMergeState,
SHAMismatchState,
UnresolvedDiscussionsState,
PipelineBlockedState,
PipelineFailedState,
......@@ -203,6 +204,7 @@ export default {
'mr-widget-not-allowed': NotAllowedState,
'mr-widget-missing-branch': MissingBranchState,
'mr-widget-ready-to-merge': ReadyToMergeState,
'mr-widget-sha-mismatch': SHAMismatchState,
'mr-widget-squash-before-merge': SquashBeforeMerge,
'mr-widget-checking': CheckingState,
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
......
......@@ -21,6 +21,8 @@ export default function deviseState(data) {
return 'unresolvedDiscussions';
} else if (this.isPipelineBlocked) {
return 'pipelineBlocked';
} else if (this.hasSHAChanged) {
return 'shaMismatch';
} else if (this.canBeMerged) {
return 'readyToMerge';
}
......
......@@ -4,6 +4,7 @@ import { getStateKey } from '../dependencies';
export default class MergeRequestStore {
constructor(data) {
this.startingSha = data.diff_head_sha;
this.setData(data);
}
......@@ -67,6 +68,7 @@ export default class MergeRequestStore {
this.canMerge = !!data.merge_path;
this.canCreateIssue = currentUser.can_create_issue || false;
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
this.hasSHAChanged = this.sha !== this.startingSha;
this.canBeMerged = data.can_be_merged || false;
// Cherry-pick and Revert actions related
......
......@@ -16,6 +16,7 @@ const stateToComponentMap = {
mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds',
failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'mr-widget-sha-mismatch',
};
const statesToShowHelpWidget = [
......
......@@ -3,24 +3,19 @@ import retrySVG from 'icons/_icon_action_retry.svg';
import playSVG from 'icons/_icon_action_play.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) {
let icon;
switch (action) {
case 'icon_action_cancel':
icon = cancelSVG;
break;
case 'icon_action_retry':
icon = retrySVG;
break;
case 'icon_action_play':
icon = playSVG;
break;
case 'icon_action_stop':
icon = stopSVG;
break;
default:
icon = '';
}
const icons = {
icon_action_cancel: cancelSVG,
icon_action_play: playSVG,
icon_action_retry: retrySVG,
icon_action_stop: stopSVG,
};
return icon;
return icons[action] || '';
}
......@@ -41,15 +41,3 @@ export const statusIconEntityMap = {
icon_status_success: SUCCESS_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>
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 {
props: {
status: {
......@@ -15,7 +36,7 @@
},
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}`;
},
},
......
......@@ -119,14 +119,14 @@ export default {
</div>
<a v-if="hasCommitRef"
class="monospace branch-name"
class="ref-name"
:href="commitRef.ref_url">
{{commitRef.name}}
</a>
<div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
<a class="commit-id monospace"
<a class="commit-sha"
:href="commitUrl">
{{shortSha}}
</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 @@
import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
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 PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
......@@ -39,7 +39,7 @@ export default {
'commit-component': CommitComponent,
'dropdown-stage': PipelinesStageComponent,
'pipeline-url': PipelinesUrlComponent,
'status-scope': PipelinesStatusComponent,
ciBadge,
'time-ago': PipelinesTimeagoComponent,
},
......@@ -196,11 +196,20 @@ export default {
return '';
},
pipelineStatus() {
if (this.pipeline.details && this.pipeline.details.status) {
return this.pipeline.details.status;
}
return {};
},
},
template: `
<tr class="commit">
<status-scope :pipeline="pipeline"/>
<td class="commit-link">
<ci-badge :status="pipelineStatus"/>
</td>
<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 @@
* Styles that apply to all GFM related forms.
*/
.gfm-commit,
.gfm-commit_range {
font-family: $monospace_font;
font-size: 90%;
@extend .commit-sha;
}
......@@ -112,7 +112,7 @@
}
}
.issue_edited_ago,
.issue-edited-ago,
.note_edited_ago {
display: none;
}
......
......@@ -289,11 +289,6 @@ pre {
}
}
.monospace {
font-family: $monospace_font;
font-size: 90%;
}
code {
&.key-fingerprint {
background: $body-bg;
......@@ -305,6 +300,24 @@ a > code {
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
*
......
......@@ -12,10 +12,14 @@
}
&.branch-info {
.monospace,
.commit-sha,
.commit-info {
margin-left: 4px;
}
.ref-name {
font-size: 12px;
}
}
}
......
......@@ -206,11 +206,11 @@
margin-left: $gl-padding;
}
}
}
.commit-short-id {
font-family: $monospace_font;
font-weight: 600;
.commit-sha {
font-size: 14px;
font-weight: 600;
}
}
.commit,
......@@ -271,7 +271,7 @@
}
}
.commit-id {
.commit-sha {
color: $gl-link-color;
}
......
......@@ -387,7 +387,7 @@
padding: 0 3px 0 0;
}
.branch-name {
.ref-name {
color: $black;
display: inline-block;
max-width: 180px;
......@@ -398,7 +398,7 @@
vertical-align: top;
}
.short-sha {
.commit-sha {
color: $gl-link-color;
line-height: 1.3;
vertical-align: top;
......
......@@ -2,11 +2,6 @@
.diff-file {
margin-bottom: $gl-padding;
.commit-short-id {
font-family: $regular_font;
font-weight: 400;
}
.file-title,
.file-title-flex-parent {
cursor: pointer;
......
......@@ -90,7 +90,7 @@
}
.build-link,
.branch-name {
.ref-name {
color: $gl-text-color;
}
......@@ -135,7 +135,7 @@
}
.branch-commit {
.commit-id {
.commit-sha {
margin-right: 0;
}
}
......
......@@ -90,11 +90,6 @@
align-items: center;
padding: $gl-padding-top $gl-padding 0;
i,
svg {
margin-right: 8px;
}
svg {
position: relative;
top: 1px;
......@@ -109,9 +104,10 @@
flex-wrap: wrap;
}
.ci-status-icon > .icon-link svg {
.icon-link > .ci-status-icon > svg {
width: 22px;
height: 22px;
margin-right: 8px;
}
}
......@@ -160,6 +156,34 @@
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 {
display: inline-block;
}
......@@ -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 {
text-align: center;
......
......@@ -238,11 +238,6 @@ ul.notes {
ul {
margin: 3px 0 3px 16px !important;
.gfm-commit {
font-family: $monospace_font;
font-size: 12px;
}
}
p:first-child {
......
......@@ -158,9 +158,13 @@
float: none;
}
.api {
@extend .monospace;
}
.branch-commit {
.branch-name {
.ref-name {
font-weight: bold;
max-width: 120px;
overflow: hidden;
......@@ -182,7 +186,7 @@
color: $gl-text-color;
}
.commit-id {
.commit-sha {
color: $gl-link-color;
}
......
......@@ -657,9 +657,8 @@ pre.light-well {
color: $gl-text-color;
}
.commit_short_id {
.commit-sha {
margin-right: 5px;
color: $gl-link-color;
font-weight: 600;
}
......@@ -825,7 +824,8 @@ pre.light-well {
}
.compare-form-group {
.dropdown-menu {
.dropdown-menu,
.inline-input-group {
width: 100%;
@media (min-width: $screen-sm-min) {
......@@ -844,14 +844,6 @@ pre.light-well {
width: auto;
}
}
.inline-input-group {
width: 100%;
@media (min-width: $screen-sm-min) {
width: 250px;
}
}
}
.clearable-input {
......
......@@ -60,17 +60,24 @@ module IssuableActions
end
def bulk_update_params
params.require(:update).permit(
permitted_keys = [
:issuable_ids,
:assignee_id,
:milestone_id,
:state_event,
:subscription_event,
assignee_ids: [],
label_ids: [],
add_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
def resource_name
......
......@@ -4,7 +4,7 @@ module RoutableActions
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?)
if routable_authorized?(routable_klass, routable, extra_authorization_proc)
if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path)
routable
else
......@@ -13,8 +13,8 @@ module RoutableActions
end
end
def routable_authorized?(routable_klass, routable, extra_authorization_proc)
action = :"read_#{routable_klass.to_s.underscore}"
def routable_authorized?(routable, extra_authorization_proc)
action = :"read_#{routable.class.to_s.underscore}"
return false unless can?(current_user, action, routable)
if extra_authorization_proc
......@@ -30,7 +30,7 @@ module RoutableActions
canonical_path = routable.full_path
if canonical_path != requested_path
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
redirect_to request.original_url.sub(requested_path, canonical_path)
end
......
......@@ -208,7 +208,6 @@ class Projects::IssuesController < Projects::ApplicationController
description: view_context.markdown_field(@issue, :description),
description_text: @issue.description,
task_status: @issue.task_status,
issue_number: @issue.iid,
updated_at: @issue.updated_at
}
end
......
......@@ -42,7 +42,10 @@ module ButtonHelper
class: "btn #{css_class}",
data: data,
type: :button,
title: title
title: title,
aria: {
label: title
}
end
def http_clone_button(project, placement = 'right', append_link: true)
......
......@@ -74,12 +74,8 @@ module CommitsHelper
# Returns the sorted alphabetically links to branches, separated by a comma
def commit_branches_links(project, branches)
branches.sort.map do |branch|
link_to(
namespace_project_tree_path(project.namespace, project, branch)
) do
content_tag :span, class: 'label label-gray' do
icon('code-fork') + ' ' + branch
end
link_to(project_ref_path(project, branch), class: "label label-gray ref-name") do
icon('code-fork') + " #{branch}"
end
end.join(" ").html_safe
end
......@@ -88,13 +84,8 @@ module CommitsHelper
def commit_tags_links(project, tags)
sorted = VersionSorter.rsort(tags)
sorted.map do |tag|
link_to(
namespace_project_commits_path(project.namespace, project,
project.repository.find_tag(tag).name)
) do
content_tag :span, class: 'label label-gray' do
icon('tag') + ' ' + tag
end
link_to(project_ref_path(project, tag), class: "label label-gray ref-name") do
icon('tag') + " #{tag}"
end
end.join(" ").html_safe
end
......@@ -198,8 +189,8 @@ module CommitsHelper
tree_join(commit_sha, diff_new_path)),
class: 'btn view-file js-view-file'
) do
raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id')
raw('View file @ ') + content_tag(:span, Commit.truncate_sha(commit_sha),
class: 'commit-sha')
end
end
......
......@@ -63,7 +63,7 @@ module DiffHelper
def parallel_diff_discussions(left, right, diff_file)
return unless @grouped_diff_discussions
discussions_left = discussions_right = nil
if left && (left.unchanged? || left.removed?)
......@@ -98,7 +98,7 @@ module DiffHelper
[
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
end
......
......@@ -164,9 +164,14 @@ module EventsHelper
def event_note_title_html(event)
if event.note_target
link_to(event_note_target_path(event), title: event.target_title, class: 'has-tooltip') do
"#{event.note_target_type} #{event.note_target_reference}"
end
text = raw("#{event.note_target_type} ") +
if event.commit_note?
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
content_tag(:strong, '(deleted)')
end
......
......@@ -54,6 +54,10 @@ module GitlabRoutingHelper
namespace_project_builds_path(project.namespace, project, *args)
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)
namespace_project_container_registry_index_path(project.namespace, project, *args)
end
......
......@@ -67,9 +67,10 @@ module IssuablesHelper
end
def users_dropdown_label(selected_users)
if selected_users.length == 0
case selected_users.length
when 0
"Unassigned"
elsif selected_users.length == 1
when 1
selected_users[0].name
else
"#{selected_users[0].name} + #{selected_users.length - 1} more"
......@@ -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")
end
if issuable.tasks?
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_short, id: "task_status_short", class: "hidden-md hidden-lg")
end
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_short, id: "task_status_short", class: "hidden-md hidden-lg")
output
end
......
......@@ -24,10 +24,13 @@ module TodosHelper
end
def todo_target_link(todo)
target = todo.target_type.titleize.downcase
link_to "#{target} #{todo.target_reference}", todo_target_path(todo),
class: 'has-tooltip',
title: todo.target.title
text = raw("#{todo.target_type.titleize.downcase} ") +
if todo.for_commit?
content_tag(:span, todo.target_reference, class: 'commit-sha')
else
todo.target_reference
end
link_to text, todo_target_path(todo), class: 'has-tooltip', title: todo.target.title
end
def todo_target_path(todo)
......
......@@ -326,10 +326,7 @@ class Commit
end
def raw_diffs(*args)
use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
if use_gitaly && !deltas_only
if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
else
raw.diffs(*args)
......
......@@ -44,14 +44,15 @@ module Mentionable
end
def all_references(current_user = nil, extractor: nil)
@extractors ||= {}
# Use custom extractor if it's passed in the function parameters.
if extractor
@extractor = extractor
@extractors[current_user] = extractor
else
@extractor ||= Gitlab::ReferenceExtractor.
new(project, current_user)
extractor = @extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user)
@extractor.reset_memoized_values
extractor.reset_memoized_values
end
self.class.mentionable_attrs.each do |attr, options|
......@@ -62,10 +63,10 @@ module Mentionable
skip_project_check: skip_project_check?
)
@extractor.analyze(text, options)
extractor.analyze(text, options)
end
@extractor
extractor
end
def mentioned_users(current_user = nil)
......
......@@ -7,5 +7,27 @@ module ProtectedBranchAccess
belongs_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
......@@ -174,7 +174,7 @@ class Issue < ActiveRecord::Base
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
def has_related_branch?
def has_related_branch?
project.repository.branch_names.any? do |branch|
/\A#{iid}-(?!\d+-stable)/i =~ branch
end
......
......@@ -3,11 +3,4 @@ class IssueAssignee < ActiveRecord::Base
belongs_to :issue
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
......@@ -125,7 +125,6 @@ class MergeRequest < ActiveRecord::Base
participant :assignee
after_save :keep_around_commit
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
def self.reference_prefix
'!'
......@@ -187,13 +186,6 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
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
def card_attributes
{
......
......@@ -35,7 +35,7 @@ module ChatMessage
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}",
text: "in #{pretty_duration(duration)}",
image: user_avatar || ''
......@@ -45,7 +45,7 @@ module ChatMessage
private
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
def humanized_status
......@@ -70,7 +70,7 @@ module ChatMessage
end
def branch_link
"[#{ref}](#{branch_url})"
"`[#{ref}](#{branch_url})`"
end
def project_link
......
......@@ -61,7 +61,7 @@ module ChatMessage
end
def removed_branch_message
"#{user_name} removed #{ref_type} #{ref} from #{project_link}"
"#{user_name} removed #{ref_type} `#{ref}` from #{project_link}"
end
def push_message
......@@ -102,7 +102,7 @@ module ChatMessage
end
def branch_link
"[#{ref}](#{branch_url})"
"`[#{ref}](#{branch_url})`"
end
def project_link
......
class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
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
class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
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
......@@ -517,8 +517,8 @@ class Repository
cache_method :avatar
def readme
if head = tree(:head)
ReadmeBlob.new(head.readme, self)
if readme = tree(:head)&.readme
ReadmeBlob.new(readme, self)
end
end
......
......@@ -929,6 +929,11 @@ class User < ActiveRecord::Base
assigned_open_issues_count(force: true)
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)
Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
TodosFinder.new(self, state: :done).execute.count
......
......@@ -38,10 +38,7 @@ class PipelineEntity < Grape::Entity
expose :path do |pipeline|
if pipeline.ref
namespace_project_tree_path(
pipeline.project.namespace,
pipeline.project,
id: pipeline.ref)
project_ref_path(pipeline.project, pipeline.ref)
end
end
......
......@@ -3,6 +3,7 @@ module RequestAwareEntity
included do
include Gitlab::Routing
include GitlabRoutingHelper
include Gitlab::Allowable
end
......
......@@ -7,7 +7,7 @@ module Issuable
ids = params.delete(:issuable_ids).split(",")
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?
end
......@@ -26,5 +26,17 @@ module Issuable
success: !items.count.zero?
}
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
......@@ -178,6 +178,7 @@ class IssuableBaseService < BaseService
after_create(issuable)
issuable.create_cross_references!(current_user)
execute_hooks(issuable)
issuable.assignees.each(&:invalidate_cache_counts)
end
issuable
......@@ -234,6 +235,11 @@ class IssuableBaseService < BaseService
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)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
......
......@@ -43,8 +43,9 @@ module Members
)
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
member.user.update_cache_counts
end
member.user.invalidate_cache_counts
end
end
end
......@@ -79,7 +79,7 @@ module SystemNoteService
text_parts.join(' and ')
elsif old_assignees.any?
"removed all assignees"
"removed assignee"
elsif issue.assignees.any?
"assigned to #{issue.assignees.map(&:to_reference).to_sentence}"
end
......
......@@ -26,7 +26,7 @@
- commit = discussion.noteable
- if commit
commit
= link_to commit.short_id, url, class: 'monospace'
= link_to commit.short_id, url, class: 'commit-sha'
- else
a deleted commit
- elsif discussion.diff_discussion?
......
%li.commit
.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;
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author
......@@ -46,6 +46,3 @@
.form-actions
= submit_tag 'Continue to the next step', class: 'btn btn-create'
:javascript
new UsersSelect();
......@@ -13,7 +13,7 @@
.location-badge= label
.search-input-wrap
.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_content do
%ul
......
......@@ -5,7 +5,7 @@
= ci_icon_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"
&middot;
#{time_ago_with_tooltip(commit.committed_date)} by
......
......@@ -5,7 +5,7 @@
.event-last-push
.event-last-push-text
%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
- if @project && event.project != @project
%span at
......
......@@ -22,7 +22,7 @@
%strong
= link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark"
.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;
.light
= commit_author_link(commit, avatar: false)
......
......@@ -6,7 +6,8 @@
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
%li{ class: "js-branch-#{branch.name}" }
%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
&nbsp;
- if branch.name == @repository.root_ref
......
.branch-commit
.icon-container.commit-icon
= 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;
%span.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
......
......@@ -17,11 +17,13 @@
.help-block.text-danger.js-branch-name-error
.form-group
= label_tag :ref, 'Create from', class: 'control-label'
.col-sm-10.dropdown.create-from
= hidden_field_tag :ref, default_ref
= 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
.text-left.dropdown-toggle-text= default_ref
= render 'shared/ref_dropdown', dropdown_class: 'wide'
.col-sm-10.create-from
.dropdown
= hidden_field_tag :ref, 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
.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
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
......
......@@ -6,18 +6,16 @@
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
%strong
Job
= link_to namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id' do
\##{@build.id}
= link_to "##{@build.id}", namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id'
in pipeline
= link_to pipeline_path(pipeline) do
%strong ##{pipeline.id}
for commit
= link_to namespace_project_commit_path(@project.namespace, @project, pipeline.sha) do
%strong= pipeline.short_sha
%strong
= link_to "##{pipeline.id}", pipeline_path(pipeline)
for
%strong
= link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: 'commit-sha'
from
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
%code
= @build.ref
%strong
= link_to @build.ref, project_ref_path(@project, @build.ref), class: 'ref-name'
= render "projects/builds/user" if @build.user
......
......@@ -23,14 +23,14 @@
- if job.ref
.icon-container
= 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
.light none
.icon-container.commit-icon
= custom_icon("icon_commit")
- 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?
= icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.')
......@@ -58,7 +58,7 @@
- if pipeline.user
= user_avatar(user: pipeline.user, size: 20)
- else
%span.monospace API
%span.api API
- if admin
%td
......
.page-content-header
.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")
%span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)}
......@@ -57,7 +59,7 @@
= custom_icon("icon_commit")
%span.cgray= pluralize(@commit.parents.count, "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
%i.fa.fa-spinner.fa-spin
......@@ -68,9 +70,10 @@
= link_to namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) do
= ci_icon_for_status(last_pipeline.status)
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)
- if last_pipeline.stages.any?
with #{"stage".pluralize(last_pipeline.stages.count)}
.mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph'
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?
%span
- branch = commit_default_branch(@project, @branches)
= link_to(namespace_project_tree_path(@project.namespace, @project, branch)) do
%span.label.label-gray
= branch
- if @branches.any? || @tags.any?
= link_to("#", class: "js-details-expand") do
%span.label.label-gray
\...
- if @branches.any? || @tags.any?
- branch = commit_default_branch(@project, @branches)
= link_to(project_ref_path(@project, branch), class: "label label-gray ref-name") do
= icon('code-fork')
= branch
-# `commit_default_branch` deletes the default branch from `@branches`,
-# so only render this if we have more branches left
- if @branches.any? || @tags.any?
%span
= link_to "…", "#", class: "js-details-expand label label-gray"
%span.js-details-content.hide
- if @branches.any?
= commit_branches_links(@project, @branches)
- if @tags.any?
= commit_tags_links(@project, @tags)
= commit_branches_links(@project, @branches) if @branches.any?
= commit_tags_links(@project, @tags) if @tags.any?
......@@ -37,6 +37,6 @@
.commit-actions.flex-row.hidden-xs
- if commit.status(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")
= 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)
%li.commit.inline-commit
.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;
%span.str-truncated
= 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