提交 7942d863 编写于 作者: D 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
......@@ -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() {
......
/* 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"
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册