Commit c07b2c04 authored by Alejandro Rodríguez's avatar Alejandro Rodríguez

Merge branch 'ce-upstream' into 'master'

CE upstream

See merge request !902
parents 516c323d 64aa4138
...@@ -24,6 +24,8 @@ ...@@ -24,6 +24,8 @@
"spyOnEvent": false, "spyOnEvent": false,
"Turbolinks": false, "Turbolinks": false,
"window": false, "window": false,
"Vue": false "Vue": false,
"Flash": false,
"Cookies": false
} }
} }
...@@ -300,12 +300,11 @@ migration paths: ...@@ -300,12 +300,11 @@ migration paths:
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- master@gitlab/gitlab-ee - master@gitlab/gitlab-ee
script: script:
- git checkout HEAD . - git fetch origin v8.5.9
- git fetch --tags - git checkout -f FETCH_HEAD
- git checkout v8.5.9
- cp config/resque.yml.example config/resque.yml - cp config/resque.yml.example config/resque.yml
- sed -i 's/localhost/redis/g' config/resque.yml - sed -i 's/localhost/redis/g' config/resque.yml
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3 - bundle install --without postgres production --jobs $(nproc) ${FLAGS[@]} --retry=3
- rake db:drop db:create db:schema:load db:seed_fu - rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_BUILD_REF - git checkout $CI_BUILD_REF
- source scripts/prepare_build.sh - source scripts/prepare_build.sh
......
...@@ -32,6 +32,7 @@ entry. ...@@ -32,6 +32,7 @@ entry.
- Fix sidekiq stats in admin area (blackst0ne) - Fix sidekiq stats in admin area (blackst0ne)
- Added label description as tooltip to issue board list title - Added label description as tooltip to issue board list title
- Created cycle analytics bundle JavaScript file - Created cycle analytics bundle JavaScript file
- Make the milestone page more responsive (yury-n)
- Hides container registry when repository is disabled - Hides container registry when repository is disabled
- API: Fix booleans not recognized as such when using the `to_boolean` helper - API: Fix booleans not recognized as such when using the `to_boolean` helper
- Removed delete branch tooltip !6954 - Removed delete branch tooltip !6954
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, quotes, no-var, padded-blocks, max-len */
(function() {
this.Activities = (function() {
function Activities() {
Pager.init(20, true, false, this.updateTooltips);
$(".event-filter-link").on("click", (function(_this) {
return function(event) {
event.preventDefault();
_this.toggleFilter($(event.currentTarget));
return _this.reloadActivities();
};
})(this));
}
Activities.prototype.updateTooltips = function() {
gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
};
Activities.prototype.reloadActivities = function() {
$(".content_list").html('');
Pager.init(20, true, false, this.updateTooltips);
};
Activities.prototype.toggleFilter = function(sender) {
var filter = sender.attr("id").split("_")[0];
$('.event-filter .active').removeClass("active");
Cookies.set("event_filter", filter);
sender.closest('li').toggleClass("active");
};
return Activities;
})();
}).call(this);
/* eslint-disable no-param-reassign, class-methods-use-this */
/* global Pager, Cookies */
((global) => {
class Activities {
constructor() {
Pager.init(20, true, false, this.updateTooltips);
$('.event-filter-link').on('click', (e) => {
e.preventDefault();
this.toggleFilter(e.currentTarget);
this.reloadActivities();
});
}
updateTooltips() {
gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
}
reloadActivities() {
$('.content_list').html('');
Pager.init(20, true, false, this.updateTooltips);
}
toggleFilter(sender) {
const $sender = $(sender);
const filter = $sender.attr('id').split('_')[0];
$('.event-filter .active').removeClass('active');
Cookies.set('event_filter', filter);
$sender.closest('li').toggleClass('active');
}
}
global.Activities = Activities;
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageCodeComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl">
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
Opened
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
by
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageIssueComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl">
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
Opened
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
by
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StagePlanComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
<div class="item-details item-conmmit-component">
<img class="avatar" :src="commit.author.avatarUrl">
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
First
<span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
pushed by
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="commit.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageProductionComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl">
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
Opened
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
by
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageReviewComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl">
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
Opened
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
by
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<i class="fa fa-ban"></i>
{{ mergeRequest.state.toUpperCase() }}
</span>
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<i class= "fa fa-code-fork"></i>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageStagingComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<img class="avatar" :src="build.author.avatarUrl">
<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>
<span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
by
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageTestComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span>
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&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>
<span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
{{ build.date }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.TotalTimeComponent = Vue.extend({
props: {
time: Object,
},
template: `
<span class="total-time">
<template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
</span>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable */
//= require vue //= require vue
//= require_tree ./svg
//= require_tree .
$(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath,
});
((global) => { gl.cycleAnalyticsApp = new Vue({
const COOKIE_NAME = 'cycle_analytics_help_dismissed';
const store = gl.cycleAnalyticsStore = {
isLoading: true,
hasError: false,
isHelpDismissed: Cookies.get(COOKIE_NAME),
analytics: {}
};
gl.CycleAnalytics = class CycleAnalytics {
constructor() {
const that = this;
this.vue = new Vue({
el: '#cycle-analytics', el: '#cycle-analytics',
name: 'CycleAnalytics', name: 'CycleAnalytics',
created: this.fetchData(),
data: store,
methods: {
dismissLanding() {
that.dismissLanding();
}
}
});
}
fetchData(options) {
store.isLoading = true;
options = options || { startDate: 30 };
$.ajax({
url: $('#cycle-analytics').data('request-path'),
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: { data: {
cycle_analytics: { state: cycleAnalyticsStore.state,
start_date: options.startDate isLoading: false,
} isLoadingStage: false,
} isEmptyStage: false,
}).done((data) => { hasError: false,
this.decorateData(data); startDate: 30,
this.initDropdown(); isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
}) },
.error((data) => { computed: {
this.handleError(data); currentStage() {
}) return cycleAnalyticsStore.currentActiveStage();
.always(() => { },
store.isLoading = false; },
}) components: {
} 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent,
'stage-plan-component': gl.cycleAnalytics.StagePlanComponent,
decorateData(data) { 'stage-code-component': gl.cycleAnalytics.StageCodeComponent,
data.summary = data.summary || []; 'stage-test-component': gl.cycleAnalytics.StageTestComponent,
data.stats = data.stats || []; 'stage-review-component': gl.cycleAnalytics.StageReviewComponent,
'stage-staging-component': gl.cycleAnalytics.StageStagingComponent,
data.summary.forEach((item) => { 'stage-production-component': gl.cycleAnalytics.StageProductionComponent,
item.value = item.value || '-'; },
}); created() {
this.fetchCycleAnalyticsData();
data.stats.forEach((item) => { },
item.value = item.value || '- - -'; methods: {
}); handleError() {
cycleAnalyticsStore.setErrorState(true);
store.analytics = data; return new Flash('There was an error while fetching cycle analytics data.');
} },
handleError(data) {
store.hasError = true;
new Flash('There was an error while fetching cycle analytics data.', 'alert');
}
dismissLanding() {
store.isHelpDismissed = true;
Cookies.set(COOKIE_NAME, true);
}
initDropdown() { initDropdown() {
const $dropdown = $('.js-ca-dropdown'); const $dropdown = $('.js-ca-dropdown');
const $label = $dropdown.find('.dropdown-label'); const $label = $dropdown.find('.dropdown-label');
...@@ -86,13 +51,71 @@ ...@@ -86,13 +51,71 @@
$dropdown.find('li a').off('click').on('click', (e) => { $dropdown.find('li a').off('click').on('click', (e) => {
e.preventDefault(); e.preventDefault();
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
const value = $target.data('value'); this.startDate = $target.data('value');
$label.text($target.text().trim()); $label.text($target.text().trim());
this.fetchData({ startDate: value }); this.fetchCycleAnalyticsData({ startDate: this.startDate });
});
},
fetchCycleAnalyticsData(options) {
const fetchOptions = options || { startDate: this.startDate };
this.isLoading = true;
cycleAnalyticsService
.fetchCycleAnalyticsData(fetchOptions)
.done((response) => {
cycleAnalyticsStore.setCycleAnalyticsData(response);
this.selectDefaultStage();
this.initDropdown();
})
.error(() => {
this.handleError();
}) })
.always(() => {
this.isLoading = false;
});
},
selectDefaultStage() {
const stage = this.state.stages.first();
this.selectStage(stage);
},
selectStage(stage) {
if (this.isLoadingStage) return;
if (this.currentStage === stage) return;
if (!stage.isUserAllowed) {
cycleAnalyticsStore.setActiveStage(stage);
return;
} }
} this.isLoadingStage = true;
cycleAnalyticsStore.setStageEvents([]);
cycleAnalyticsStore.setActiveStage(stage);
cycleAnalyticsService
.fetchStageData({
stage,
startDate: this.startDate,
})
.done((response) => {
this.isEmptyStage = !response.events.length;
cycleAnalyticsStore.setStageEvents(response.events);
})
.error(() => {
this.isEmptyStage = true;
})
.always(() => {
this.isLoadingStage = false;
});
},
dismissOverviewDialog() {
this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1');
},
},
});
})(window.gl || (window.gl = {})); // Register global components
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
});
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
class CycleAnalyticsService {
constructor(options) {
this.requestPath = options.requestPath;
}
fetchCycleAnalyticsData(options) {
options = options || { startDate: 30 };
return $.ajax({
url: this.requestPath,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: {
cycle_analytics: {
start_date: options.startDate,
},
},
});
}
fetchStageData(options) {
const {
stage,
startDate,
} = options;
return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
cycle_analytics: {
start_date: startDate,
},
});
}
}
global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
const EMPTY_STAGE_TEXTS = {
issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
};
global.cycleAnalytics.CycleAnalyticsStore = {
state: {
summary: '',
stats: '',
analytics: '',
events: [],
stages: [],
},
setCycleAnalyticsData(data) {
this.state = Object.assign(this.state, this.decorateData(data));
},
decorateData(data) {
const newData = {};
newData.stages = data.stats || [];
newData.summary = data.summary || [];
newData.summary.forEach((item) => {
item.value = item.value || '-';
});
newData.stages.forEach((item) => {
const stageName = item.title.toLowerCase();
item.active = false;
item.isUserAllowed = data.permissions[stageName];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageName];
item.component = `stage-${stageName}-component`;
});
newData.analytics = data;
return newData;
},
setLoadingState(state) {
this.state.isLoading = state;
},
setErrorState(state) {
this.state.hasError = state;
},
deactivateAllStages() {
this.state.stages.forEach((stage) => {
stage.active = false;
});
},
setActiveStage(stage) {
this.deactivateAllStages();
stage.active = true;
},
setStageEvents(events) {
this.state.events = this.decorateEvents(events);
},
decorateEvents(events) {
const newEvents = events;
newEvents.forEach((item) => {
item.totalTime = item.total_time;
item.author.webUrl = item.author.web_url;
item.author.avatarUrl = item.author.avatar_url;
if (item.created_at) item.createdAt = item.created_at;
if (item.short_sha) item.shortSha = item.short_sha;
if (item.commit_url) item.commitUrl = item.commit_url;
delete item.author.web_url;
delete item.author.avatar_url;
delete item.total_time;
delete item.created_at;
delete item.short_sha;
delete item.commit_url;
});
return newEvents;
},
currentActiveStage() {
return this.state.stages.find(stage => stage.active);
},
};
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>';
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>';
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>';
})(window.gl || (window.gl = {}));
...@@ -111,10 +111,10 @@ ...@@ -111,10 +111,10 @@
Issuable.init(); Issuable.init();
break; break;
case 'dashboard:activity': case 'dashboard:activity':
new Activities(); new gl.Activities();
break; break;
case 'dashboard:projects:starred': case 'dashboard:projects:starred':
new Activities(); new gl.Activities();
break; break;
case 'projects:commit:show': case 'projects:commit:show':
new Commit(); new Commit();
...@@ -140,7 +140,7 @@ ...@@ -140,7 +140,7 @@
new gl.Pipelines(); new gl.Pipelines();
break; break;
case 'groups:activity': case 'groups:activity':
new Activities(); new gl.Activities();
break; break;
case 'groups:show': case 'groups:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
...@@ -216,9 +216,6 @@ ...@@ -216,9 +216,6 @@
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
break; break;
case 'projects:cycle_analytics:show':
new gl.CycleAnalytics();
break;
} }
switch (path.first()) { switch (path.first()) {
case 'admin': case 'admin':
......
...@@ -157,17 +157,17 @@ ...@@ -157,17 +157,17 @@
<li v-bind:class="{ 'active': scope === undefined }"> <li v-bind:class="{ 'active': scope === undefined }">
<a :href="projectEnvironmentsPath"> <a :href="projectEnvironmentsPath">
Available Available
<span <span class="badge js-available-environments-count">
class="badge js-available-environments-count" {{state.availableCounter}}
v-html="state.availableCounter"></span> </span>
</a> </a>
</li> </li>
<li v-bind:class="{ 'active' : scope === 'stopped' }"> <li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath"> <a :href="projectStoppedEnvironmentsPath">
Stopped Stopped
<span <span class="badge js-stopped-environments-count">
class="badge js-stopped-environments-count" {{state.stoppedCounter}}
v-html="state.stoppedCounter"></span> </span>
</a> </a>
</li> </li>
</ul> </ul>
...@@ -183,8 +183,7 @@ ...@@ -183,8 +183,7 @@
<i class="fa fa-spinner spin"></i> <i class="fa fa-spinner spin"></i>
</div> </div>
<div <div class="blank-state blank-state-no-icon"
class="blank-state blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0"> v-if="!isLoading && state.environments.length === 0">
<h2 class="blank-state-title"> <h2 class="blank-state-title">
You don't have any environments right now. You don't have any environments right now.
...@@ -205,8 +204,7 @@ ...@@ -205,8 +204,7 @@
</a> </a>
</div> </div>
<div <div class="table-holder"
class="table-holder"
v-if="!isLoading && state.environments.length > 0"> v-if="!isLoading && state.environments.length > 0">
<table class="table ci-table environments"> <table class="table ci-table environments">
<thead> <thead>
...@@ -234,7 +232,9 @@ ...@@ -234,7 +232,9 @@
is="environment-item" is="environment-item"
v-for="children in model.children" v-for="children in model.children"
:model="children" :model="children"
:toggleRow="toggleRow.bind(children)"> :toggleRow="toggleRow.bind(children)"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed">
</tr> </tr>
</template> </template>
......
...@@ -43,8 +43,7 @@ ...@@ -43,8 +43,7 @@
<div class="inline"> <div class="inline">
<div class="dropdown"> <div class="dropdown">
<a class="dropdown-new btn btn-default" data-toggle="dropdown"> <a class="dropdown-new btn btn-default" data-toggle="dropdown">
<span class="dropdown-play-icon-container"> <span class="dropdown-play-icon-container"></span>
</span>
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down"></i>
</a> </a>
...@@ -54,9 +53,10 @@ ...@@ -54,9 +53,10 @@
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
class="js-manual-action-link"> class="js-manual-action-link">
<span class="action-play-icon-container"> <span class="action-play-icon-container"></span>
<span>
{{action.name}}
</span> </span>
<span v-html="action.name"></span>
</a> </a>
</li> </li>
</ul> </ul>
......
...@@ -389,11 +389,10 @@ ...@@ -389,11 +389,10 @@
template: ` template: `
<tr> <tr>
<td v-bind:class="{ 'children-row': isChildren}"> <td v-bind:class="{ 'children-row': isChildren}">
<a <a v-if="!isFolder"
v-if="!isFolder"
class="environment-name" class="environment-name"
:href="model.environment_path" :href="model.environment_path">
v-html="model.name"> {{model.name}}
</a> </a>
<span v-else v-on:click="toggleRow(model)" class="folder-name"> <span v-else v-on:click="toggleRow(model)" class="folder-name">
<span class="folder-icon"> <span class="folder-icon">
...@@ -401,16 +400,19 @@ ...@@ -401,16 +400,19 @@
<i v-show="!model.isOpen" class="fa fa-caret-right"></i> <i v-show="!model.isOpen" class="fa fa-caret-right"></i>
</span> </span>
<span v-html="model.name"></span> <span>
{{model.name}}
</span>
<span class="badge" v-html="childrenCounter"></span> <span class="badge">
{{childrenCounter}}
</span>
</span> </span>
</td> </td>
<td class="deployment-column"> <td class="deployment-column">
<span <span v-if="shouldRenderDeploymentID">
v-if="shouldRenderDeploymentID" {{deploymentInternalId}}
v-html="deploymentInternalId">
</span> </span>
<span v-if="!isFolder && deploymentHasUser"> <span v-if="!isFolder && deploymentHasUser">
...@@ -427,8 +429,8 @@ ...@@ -427,8 +429,8 @@
<td> <td>
<a v-if="shouldRenderBuildName" <a v-if="shouldRenderBuildName"
class="build-link" class="build-link"
:href="model.last_deployment.deployable.build_path" :href="model.last_deployment.deployable.build_path">
v-html="buildName"> {{buildName}}
</a> </a>
</td> </td>
...@@ -451,8 +453,8 @@ ...@@ -451,8 +453,8 @@
<td> <td>
<span <span
v-if="!isFolder && model.last_deployment" v-if="!isFolder && model.last_deployment"
class="environment-created-date-timeago" class="environment-created-date-timeago">
v-html="createdDate"> {{createdDate}}
</span> </span>
</td> </td>
......
...@@ -14,8 +14,7 @@ ...@@ -14,8 +14,7 @@
}, },
template: ` template: `
<a <a class="btn stop-env-link"
class="btn stop-env-link"
:href="stop_url" :href="stop_url"
data-confirm="Are you sure you want to stop this environment?" data-confirm="Are you sure you want to stop this environment?"
data-method="post" data-method="post"
......
...@@ -235,7 +235,7 @@ ...@@ -235,7 +235,7 @@
} }
if (environment.deployed_at && environment.deployed_at_formatted) { if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = gl.utils.getTimeago(environment.deployed_at) + '.'; environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
} else { } else {
$('.js-environment-timeago', $template).remove(); $('.js-environment-timeago', $template).remove();
environment.name += '.'; environment.name += '.';
......
/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-undef, prefer-template, wrap-iife, comma-dangle, no-return-assign, no-else-return, consistent-return, no-unused-vars, padded-blocks, max-len */
(function() {
this.Pager = {
init: function(limit, preload, disable, callback) {
this.limit = limit != null ? limit : 0;
this.disable = disable != null ? disable : false;
this.callback = callback != null ? callback : $.noop;
this.loading = $('.loading').first();
if (preload) {
this.offset = 0;
this.getOld();
} else {
this.offset = this.limit;
}
return this.initLoadMore();
},
getOld: function() {
this.loading.show();
return $.ajax({
type: "GET",
url: $(".content_list").data('href') || location.href,
data: "limit=" + this.limit + "&offset=" + this.offset,
complete: (function(_this) {
return function() {
return _this.loading.hide();
};
})(this),
success: function(data) {
Pager.append(data.count, data.html);
return Pager.callback();
},
dataType: "json"
});
},
append: function(count, html) {
$(".content_list").append(html);
if (count > 0) {
return this.offset += count;
} else {
return this.disable = true;
}
},
initLoadMore: function() {
$(document).unbind('scroll');
return $(document).endlessScroll({
bottomPixels: 400,
fireDelay: 1000,
fireOnce: true,
ceaseFire: function() {
return Pager.disable;
},
callback: (function(_this) {
return function(i) {
if (!_this.loading.is(':visible')) {
_this.loading.show();
return Pager.getOld();
}
};
})(this)
});
}
};
}).call(this);
(() => {
const ENDLESS_SCROLL_BOTTOM_PX = 400;
const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
const Pager = {
init(limit = 0, preload = false, disable = false, callback = $.noop) {
this.limit = limit;
this.offset = this.limit;
this.disable = disable;
this.callback = callback;
this.loading = $('.loading').first();
if (preload) {
this.offset = 0;
this.getOld();
}
this.initLoadMore();
},
getOld() {
this.loading.show();
$.ajax({
type: 'GET',
url: $('.content_list').data('href') || window.location.href,
data: `limit=${this.limit}&offset=${this.offset}`,
dataType: 'json',
error: () => this.loading.hide(),
success: (data) => {
this.append(data.count, data.html);
this.callback();
// keep loading until we've filled the viewport height
if (!this.disable && !this.isScrollable()) {
this.getOld();
} else {
this.loading.hide();
}
},
});
},
append(count, html) {
$('.content_list').append(html);
if (count > 0) {
this.offset += count;
} else {
this.disable = true;
}
},
isScrollable() {
const $w = $(window);
return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
},
initLoadMore() {
$(document).unbind('scroll');
$(document).endlessScroll({
bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
fireOnce: true,
ceaseFire: () => this.disable === true,
callback: () => {
if (!this.loading.is(':visible')) {
this.loading.show();
this.getOld();
}
},
});
},
};
window.Pager = Pager;
})();
...@@ -134,7 +134,7 @@ content on the Users#show page. ...@@ -134,7 +134,7 @@ content on the Users#show page.
} }
const $calendarWrap = this.$parentEl.find('.user-calendar'); const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href')); $calendarWrap.load($calendarWrap.data('href'));
new Activities(); new gl.Activities();
return this.loaded['activity'] = true; return this.loaded['activity'] = true;
} }
......
...@@ -138,16 +138,15 @@ ...@@ -138,16 +138,15 @@
<a v-if="hasRef" <a v-if="hasRef"
class="monospace branch-name" class="monospace branch-name"
:href="ref.ref_url" :href="ref.ref_url">
v-html="ref.name"> {{ref.name}}
</a> </a>
<div class="icon-container commit-icon commit-icon-container"> <div class="icon-container commit-icon commit-icon-container"></div>
</div>
<a class="commit-id monospace" <a class="commit-id monospace"
:href="commit_url" :href="commit_url">
v-html="short_sha"> {{short_sha}}
</a> </a>
<p class="commit-title"> <p class="commit-title">
...@@ -163,7 +162,8 @@ ...@@ -163,7 +162,8 @@
</a> </a>
<a class="commit-row-message" <a class="commit-row-message"
:href="commit_url" v-html="title"> :href="commit_url">
{{title}}
</a> </a>
</span> </span>
<span v-else> <span v-else>
......
...@@ -254,3 +254,32 @@ ...@@ -254,3 +254,32 @@
.content-block-small { .content-block-small {
padding: 10px 0; padding: 10px 0;
} }
.empty-state {
margin: 100px 0 0;
.text-content {
max-width: 460px;
margin: 0 auto;
padding: $gl-padding;
}
.svg-content {
text-align: center;
svg {
max-width: 425px;
width: 100%;
padding: $gl-padding;
}
}
@media(max-width: $screen-xs-max) {
margin-top: 50px;
text-align: center;
.btn {
width: 100%;
}
}
}
...@@ -161,6 +161,7 @@ $settings-icon-size: 18px; ...@@ -161,6 +161,7 @@ $settings-icon-size: 18px;
$provider-btn-group-border: #e5e5e5; $provider-btn-group-border: #e5e5e5;
$provider-btn-not-active-color: #4688f1; $provider-btn-not-active-color: #4688f1;
$link-underline-blue: #4a8bee; $link-underline-blue: #4a8bee;
$active-item-blue: #4a8bee;
$layout-link-gray: #7e7c7c; $layout-link-gray: #7e7c7c;
$todo-alert-blue: #428bca; $todo-alert-blue: #428bca;
$btn-side-margin: 10px; $btn-side-margin: 10px;
...@@ -284,6 +285,9 @@ $calendar-unselectable-bg: $gray-light; ...@@ -284,6 +285,9 @@ $calendar-unselectable-bg: $gray-light;
*/ */
$cycle-analytics-box-padding: 30px; $cycle-analytics-box-padding: 30px;
$cycle-analytics-box-text-color: #8c8c8c; $cycle-analytics-box-text-color: #8c8c8c;
$cycle-analytics-big-font: 19px;
$cycle-analytics-dark-text: $gl-title-color;
$cycle-analytics-light-gray: #bfbfbf;
/* /*
* Personal Access Tokens * Personal Access Tokens
......
#cycle-analytics { #cycle-analytics {
max-width: 1000px;
margin: 24px auto 0; margin: 24px auto 0;
max-width: 800px;
position: relative; position: relative;
.panel { .col-headers {
ul {
margin: 0;
padding: 0;
@include clearfix;
}
li {
display: inline-block;
float: left;
line-height: 50px;
width: 20%;
}
.fa {
color: $cycle-analytics-light-gray;
}
.stage-header {
width: 28%;
padding-left: $gl-padding;
}
.median-header {
width: 12%;
}
.event-header {
width: 45%;
padding-left: $gl-padding;
}
.total-time-header {
width: 15%;
text-align: right;
padding-right: $gl-padding;
}
.stage-name {
font-weight: 600;
}
}
.panel {
.content-block { .content-block {
padding: 24px 0; padding: 24px 0;
border-bottom: none; border-bottom: none;
...@@ -35,23 +78,20 @@ ...@@ -35,23 +78,20 @@
} }
&:last-child { &:last-child {
text-align: right;
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-min) {
text-align: center; text-align: center;
} }
} }
} }
.dropdown {
top: 13px;
} }
.js-ca-dropdown {
top: $gl-padding-top;
} }
.bordered-box { .bordered-box {
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
} }
.content-list { .content-list {
...@@ -141,4 +181,302 @@ ...@@ -141,4 +181,302 @@
margin-top: 36px; margin-top: 36px;
} }
.stage-panel-body {
display: flex;
flex-wrap: wrap;
}
.stage-nav,
.stage-entries {
display: flex;
vertical-align: top;
font-size: $gl-font-size;
}
.stage-nav {
width: 40%;
margin-bottom: 0;
ul {
padding: 0;
margin: 0;
width: 100%;
}
li {
list-style-type: none;
@include clearfix;
}
.stage-nav-item {
display: block;
line-height: 65px;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
border-right: 1px solid $border-color;
background-color: $gray-light;
cursor: default;
&.active {
background-color: transparent;
border-right-color: transparent;
border-top-color: $border-color;
border-bottom-color: $border-color;
box-shadow: inset 2px 0 0 0 $active-item-blue;
.stage-name {
font-weight: 600;
}
}
&:hover:not(.active) {
background-color: $gray-lightest;
box-shadow: inset 2px 0 0 0 $border-color;
}
&:first-child {
border-top: none;
}
&:last-child {
border-bottom: none;
}
.stage-nav-item-cell {
float: left;
&.stage-name {
width: 70%;
}
&.stage-median {
width: 30%;
}
}
.stage-name {
padding-left: 16px;
}
.stage-empty,
.not-available {
color: $gl-text-color-light;
}
}
}
.stage-panel-container {
width: 100%;
overflow: auto;
}
.stage-panel {
min-width: 968px;
.panel-heading {
padding: 0;
background-color: transparent;
}
.events-description {
line-height: 65px;
padding-left: $gl-padding;
}
}
.stage-events {
width: 60%;
overflow: scroll;
height: 467px;
}
.stage-event-list {
margin: 0;
padding: 0;
}
.stage-event-item {
list-style-type: none;
padding: 0 0 $gl-padding;
margin: 0 $gl-padding $gl-padding;
border-bottom: 1px solid $gray-darker;
@include clearfix;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
.item-details,
.item-time {
float: left;
}
.item-details {
width: 75%;
}
.item-title {
margin: 0 0 2px;
&.issue-title,
&.commit-title,
&.merge-merquest-title {
max-width: 100%;
display: block;
@include text-overflow();
a {
color: $gl-dark-link-color;
}
}
}
.item-time {
width: 25%;
text-align: right;
}
.total-time {
font-size: $cycle-analytics-big-font;
color: $cycle-analytics-dark-text;
span {
color: $gl-text-color;
font-size: $gl-font-size;
}
}
.issue-date,
.build-date {
color: $gl-text-color;
}
.issue-link,
.commit-author-link,
.issue-author-link {
color: $gl-dark-link-color;
}
// Custom CSS for components
.item-conmmit-component {
.commit-icon {
position: relative;
top: 3px;
left: 1px;
display: inline-block;
svg {
float: left;
}
}
}
.merge-request-branch {
a {
max-width: 180px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
vertical-align: bottom;
}
}
}
// Custom Styles for stage items
.item-build-component {
.item-title {
.icon-build-status {
float: left;
margin-right: 5px;
position: relative;
top: 2px;
}
.item-build-name {
color: $gl-title-color;
}
.pipeline-id {
color: $gl-title-color;
padding: 0 3px 0 0;
}
.branch-name {
color: $black;
display: inline-block;
max-width: 180px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
line-height: 1.3;
vertical-align: top;
}
.short-sha {
color: $gl-link-color;
line-height: 1.3;
vertical-align: top;
font-weight: normal;
}
.fa {
color: $gl-text-color-light;
font-size: $code_font_size;
}
}
}
.empty-stage,
.no-access-stage {
text-align: center;
width: 75%;
margin: 0 auto;
padding-top: 130px;
color: $gl-text-color-light;
h4 {
color: $gl-text-color;
}
}
.empty-stage {
.icon-no-data {
height: 36px;
width: 78px;
display: inline-block;
margin-bottom: 20px;
}
}
.no-access-stage {
.icon-lock {
height: 36px;
width: 78px;
display: inline-block;
margin-bottom: 20px;
}
}
}
.cycle-analytics-overview {
padding-top: 100px;
.overview-details {
display: flex;
align-items: center;
}
.overview-image {
text-align: right;
}
.overview-icon {
svg {
width: 365px;
height: 227px;
}
}
} }
...@@ -255,26 +255,3 @@ ...@@ -255,26 +255,3 @@
} }
} }
// For sign in pane only, to improve tab order, the following removes the submit button from
// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928
.login-box {
.new_user {
position: relative;
padding-bottom: 35px;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
.forgot-password {
float: none !important;
margin-top: 5px;
}
}
}
.move-submit-down {
position: absolute;
width: 100%;
bottom: 0;
}
}
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
} }
.progress { .progress {
width: 100%;
height: 6px; height: 6px;
} }
} }
...@@ -30,7 +31,6 @@ ...@@ -30,7 +31,6 @@
margin-right: 7px; margin-right: 7px;
} }
// Issue title
span a { span a {
color: $gl-text-color; color: $gl-text-color;
word-wrap: break-word; word-wrap: break-word;
...@@ -39,15 +39,66 @@ ...@@ -39,15 +39,66 @@
} }
.milestone-summary { .milestone-summary {
margin-bottom: 25px;
.milestone-stat { .milestone-stat {
white-space: nowrap;
margin-right: 10px; margin-right: 10px;
&.with-drilldown {
margin-right: 2px;
}
} }
.remaining-days { .remaining-days {
color: $orange-light; color: $orange-light;
} }
.milestone-stats-and-buttons {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
@media (min-width: $screen-xs-min) {
justify-content: space-between;
flex-wrap: nowrap;
}
}
.milestone-progress-buttons {
order: 1;
margin-top: 10px;
@media (min-width: $screen-xs-min) {
order: 2;
margin-top: 0;
flex-shrink: 0;
}
.btn {
float: left;
margin-right: $btn-side-margin;
&:last-child {
margin-right: 0;
}
}
}
.milestone-stats {
order: 2;
width: 100%;
padding: 7px 0;
flex-shrink: 1;
@media (min-width: $screen-xs-min) {
// when displayed on one line stats go first, buttons second
order: 1;
}
}
.progress {
width: 100%;
margin: 15px 0;
}
} }
.issues-sortable-list, .issues-sortable-list,
...@@ -82,3 +133,50 @@ ...@@ -82,3 +133,50 @@
} }
} }
} }
.milestone-page-header {
display: flex;
flex-flow: row;
align-items: center;
flex-wrap: wrap;
.status-box {
margin-top: 0;
}
.milestone-buttons {
margin-left: auto;
}
.status-box {
order: 1;
}
.milestone-buttons {
order: 2;
}
.header-text-content {
order: 3;
width: 100%;
}
.milestone-buttons .verbose {
display: none;
}
@media (min-width: $screen-xs-min) {
.milestone-buttons .verbose {
display: inline;
}
.header-text-content {
order: 2;
width: auto;
}
.milestone-buttons {
order: 3;
}
}
}
...@@ -8,6 +8,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -8,6 +8,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
def show def show
@cycle_analytics = ::CycleAnalytics.new(@project, from: start_date(cycle_analytics_params)) @cycle_analytics = ::CycleAnalytics.new(@project, from: start_date(cycle_analytics_params))
stats_values, cycle_analytics_json = generate_cycle_analytics_data
@cycle_analytics_no_data = stats_values.blank?
respond_to do |format| respond_to do |format|
format.html format.html
format.json { render json: cycle_analytics_json } format.json { render json: cycle_analytics_json }
...@@ -22,23 +26,29 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -22,23 +26,29 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
{ start_date: params[:cycle_analytics][:start_date] } { start_date: params[:cycle_analytics][:start_date] }
end end
def cycle_analytics_json def generate_cycle_analytics_data
cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"], stats_values = []
[:plan, "Plan", "Time before an issue starts implementation"],
[:code, "Code", "Time until first merge request"], cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"],
[:test, "Test", "Total test time for all commits/merges"], [:plan, "Plan", "Related Commits", "Time before an issue starts implementation"],
[:review, "Review", "Time between merge request creation and merge/close"], [:code, "Code", "Related Merge Requests", "Time spent coding"],
[:staging, "Staging", "From merge request merge until deploy to production"], [:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"],
[:production, "Production", "From issue creation until deploy to production"]] [:review, "Review", "Relative Merged Requests", "The time taken to review the code"],
[:staging, "Staging", "Relative Deployed Builds", "The time taken in staging"],
[:production, "Production", "Related Issues", "The total time taken from idea to production"]]
stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)| stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_legend, stage_description)|
value = @cycle_analytics.send(stage_method).presence value = @cycle_analytics.send(stage_method).presence
stats_values << value.abs if value
stats << { stats << {
title: stage_text, title: stage_text,
description: stage_description, description: stage_description,
legend: stage_legend,
value: value && !value.zero? ? distance_of_time_in_words(value) : nil value: value && !value.zero? ? distance_of_time_in_words(value) : nil
} }
stats stats
end end
...@@ -52,9 +62,11 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -52,9 +62,11 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
{ title: "Deploy".pluralize(deploys), value: deploys } { title: "Deploy".pluralize(deploys), value: deploys }
] ]
{ cycle_analytics_hash = { summary: summary,
summary: summary, stats: stats,
stats: stats permissions: @cycle_analytics.permissions(user: current_user)
} }
[stats_values, cycle_analytics_hash]
end end
end end
...@@ -84,12 +84,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -84,12 +84,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request_diff = @merge_request_diff =
if params[:diff_id] if params[:diff_id]
@merge_request.merge_request_diffs.find(params[:diff_id]) @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else else
@merge_request.merge_request_diff @merge_request.merge_request_diff
end end
@merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present? if params[:start_sha].present?
...@@ -442,7 +442,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -442,7 +442,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
response = { response = {
title: merge_request.title, title: merge_request.title,
sha: merge_request.diff_head_commit.short_id, sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status, status: status,
coverage: coverage coverage: coverage
} }
...@@ -601,7 +601,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -601,7 +601,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_pipelines_vars def define_pipelines_vars
@pipelines = @merge_request.all_pipelines @pipelines = @merge_request.all_pipelines
if @pipelines.present? if @pipelines.present? && @merge_request.commits.present?
@pipeline = @pipelines.first @pipeline = @pipelines.first
@statuses = @pipeline.statuses.relevant @statuses = @pipeline.statuses.relevant
end end
......
...@@ -198,6 +198,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -198,6 +198,7 @@ class Projects::NotesController < Projects::ApplicationController
end end
attrs[:commands_changes] = note.commands_changes unless attrs[:award] attrs[:commands_changes] = note.commands_changes unless attrs[:award]
attrs attrs
end end
......
...@@ -50,14 +50,14 @@ module ApplicationSettingsHelper ...@@ -50,14 +50,14 @@ module ApplicationSettingsHelper
def restricted_level_checkboxes(help_block_id) def restricted_level_checkboxes(help_block_id)
Gitlab::VisibilityLevel.options.map do |name, level| Gitlab::VisibilityLevel.options.map do |name, level|
checked = restricted_visibility_levels(true).include?(level) checked = restricted_visibility_levels(true).include?(level)
css_class = 'btn' css_class = checked ? 'active' : ''
css_class += ' active' if checked checkbox_name = "application_setting[restricted_visibility_levels][]"
checkbox_name = 'application_setting[restricted_visibility_levels][]'
label_tag(checkbox_name, class: css_class) do label_tag(name, class: css_class) do
check_box_tag(checkbox_name, level, checked, check_box_tag(checkbox_name, level, checked,
autocomplete: 'off', autocomplete: 'off',
'aria-describedby' => help_block_id) + name 'aria-describedby' => help_block_id,
id: name) + visibility_level_icon(level) + name
end end
end end
end end
......
...@@ -54,4 +54,8 @@ module GroupsHelper ...@@ -54,4 +54,8 @@ module GroupsHelper
"#{status.humanize} #{projects_lfs_status(group)}" "#{status.humanize} #{projects_lfs_status(group)}"
end end
end end
def group_issues(group)
IssuesFinder.new(current_user, group_id: group.id).execute
end
end end
...@@ -486,4 +486,8 @@ module ProjectsHelper ...@@ -486,4 +486,8 @@ module ProjectsHelper
def project_child_container_class(view_path) def project_child_container_class(view_path)
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}" view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
end end
def project_issues(project)
IssuesFinder.new(current_user, project_id: project.id).execute
end
end end
...@@ -17,6 +17,8 @@ module ServicesHelper ...@@ -17,6 +17,8 @@ module ServicesHelper
"Event will be triggered when a build status changes" "Event will be triggered when a build status changes"
when "wiki_page" when "wiki_page"
"Event will be triggered when a wiki page is created/updated" "Event will be triggered when a wiki page is created/updated"
when "commit"
"Event will be triggered when a commit is created/updated"
end end
end end
......
...@@ -70,7 +70,11 @@ module Ci ...@@ -70,7 +70,11 @@ module Ci
environment: build.environment, environment: build.environment,
status_event: 'enqueue' status_event: 'enqueue'
) )
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
MergeRequests::AddTodoWhenBuildFailsService
.new(build.project, nil)
.close(new_build)
build.pipeline.mark_as_processable_after_stage(build.stage_idx) build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build new_build
end end
...@@ -484,6 +488,10 @@ module Ci ...@@ -484,6 +488,10 @@ module Ci
] ]
end end
def credentials
Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end
private private
def update_artifacts_size def update_artifacts_size
......
# == Mentionable concern # == Mentionable concern
# #
# Contains functionality related to objects that can mention Users, Issues, MergeRequests, or Commits by # Contains functionality related to objects that can mention Users, Issues, MergeRequests, Commits or Snippets by
# GFM references. # GFM references.
# #
# Used by Issue, Note, MergeRequest, and Commit. # Used by Issue, Note, MergeRequest, and Commit.
......
class CycleAnalytics class CycleAnalytics
STAGES = %i[issue plan code test review staging production].freeze
def initialize(project, from:) def initialize(project, from:)
@project = project @project = project
@from = from @from = from
...@@ -9,6 +11,10 @@ class CycleAnalytics ...@@ -9,6 +11,10 @@ class CycleAnalytics
@summary ||= Summary.new(@project, from: @from) @summary ||= Summary.new(@project, from: @from)
end end
def permissions(user:)
Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
end
def issue def issue
@fetcher.calculate_metric(:issue, @fetcher.calculate_metric(:issue,
Issue.arel_table[:created_at], Issue.arel_table[:created_at],
......
...@@ -19,7 +19,7 @@ class Environment < ActiveRecord::Base ...@@ -19,7 +19,7 @@ class Environment < ActiveRecord::Base
allow_nil: true, allow_nil: true,
addressable_url: true addressable_url: true
delegate :stop_action, to: :last_deployment, allow_nil: true delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true
scope :available, -> { with_state(:available) } scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) } scope :stopped, -> { with_state(:stopped) }
...@@ -99,4 +99,12 @@ class Environment < ActiveRecord::Base ...@@ -99,4 +99,12 @@ class Environment < ActiveRecord::Base
stop stop
stop_action.play(current_user) stop_action.play(current_user)
end end
def actions_for(environment)
return [] unless manual_actions
manual_actions.select do |action|
action.expanded_environment_name == environment
end
end
end end
...@@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base
belongs_to :merge_request belongs_to :merge_request
serialize :st_commits
serialize :st_diffs
state_machine :state, initial: :empty do state_machine :state, initial: :empty do
state :collected state :collected
state :overflow state :overflow
...@@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base
state :overflow_diff_lines_limit state :overflow_diff_lines_limit
end end
serialize :st_commits scope :viewable, -> { without_state(:empty) }
serialize :st_diffs
# All diff information is collected from repository after object is created. # All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff. # It allows you to override variables like head_commit_sha before getting diff.
......
...@@ -1196,7 +1196,7 @@ class Project < ActiveRecord::Base ...@@ -1196,7 +1196,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}", "refs/heads/#{branch}",
force: true) force: true)
repository.copy_gitattributes(branch) repository.copy_gitattributes(branch)
repository.expire_avatar_cache(branch) repository.expire_avatar_cache
reload_default_branch reload_default_branch
end end
......
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
# template :boolean default(FALSE)
# push_events :boolean default(TRUE)
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
# build_events :boolean default(FALSE), not null
#
class JiraService < IssueTrackerService class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
...@@ -30,6 +9,10 @@ class JiraService < IssueTrackerService ...@@ -30,6 +9,10 @@ class JiraService < IssueTrackerService
before_update :reset_password before_update :reset_password
def supported_events
%w(commit merge_request)
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def reference_pattern def reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
...@@ -137,19 +120,17 @@ class JiraService < IssueTrackerService ...@@ -137,19 +120,17 @@ class JiraService < IssueTrackerService
end end
def create_cross_reference_note(mentioned, noteable, author) def create_cross_reference_note(mentioned, noteable, author)
unless can_cross_reference?(noteable)
return "Events for #{noteable.model_name.plural.humanize(capitalize: false)} are disabled."
end
jira_issue = jira_request { client.Issue.find(mentioned.id) } jira_issue = jira_request { client.Issue.find(mentioned.id) }
return false unless jira_issue.present? return unless jira_issue.present?
project = self.project noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
noteable_name = noteable.class.name.underscore.downcase noteable_type = noteable_name(noteable)
noteable_id = if noteable.is_a?(Commit) entity_url = build_entity_url(noteable_type, noteable_id)
noteable.id
else
noteable.iid
end
entity_url = build_entity_url(noteable_name.to_sym, noteable_id)
data = { data = {
user: { user: {
...@@ -157,11 +138,11 @@ class JiraService < IssueTrackerService ...@@ -157,11 +138,11 @@ class JiraService < IssueTrackerService
url: resource_url(user_path(author)), url: resource_url(user_path(author)),
}, },
project: { project: {
name: project.path_with_namespace, name: self.project.path_with_namespace,
url: resource_url(namespace_project_path(project.namespace, project)) url: resource_url(namespace_project_path(project.namespace, self.project))
}, },
entity: { entity: {
name: noteable_name.humanize.downcase, name: noteable_type.humanize.downcase,
url: entity_url, url: entity_url,
title: noteable.title title: noteable.title
} }
...@@ -193,8 +174,16 @@ class JiraService < IssueTrackerService ...@@ -193,8 +174,16 @@ class JiraService < IssueTrackerService
private private
def can_cross_reference?(noteable)
case noteable
when Commit then commit_events
when MergeRequest then merge_requests_events
else true
end
end
def close_issue(entity, issue) def close_issue(entity, issue)
return if issue.nil? || issue.resolution.present? return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present?
commit_id = if entity.is_a?(Commit) commit_id = if entity.is_a?(Commit)
entity.id entity.id
...@@ -290,18 +279,26 @@ class JiraService < IssueTrackerService ...@@ -290,18 +279,26 @@ class JiraService < IssueTrackerService
"#{Settings.gitlab.base_url.chomp("/")}#{resource}" "#{Settings.gitlab.base_url.chomp("/")}#{resource}"
end end
def build_entity_url(entity_name, entity_id) def build_entity_url(noteable_type, entity_id)
polymorphic_url( polymorphic_url(
[ [
self.project.namespace.becomes(Namespace), self.project.namespace.becomes(Namespace),
self.project, self.project,
entity_name noteable_type.to_sym
], ],
id: entity_id, id: entity_id,
host: Settings.gitlab.base_url host: Settings.gitlab.base_url
) )
end end
def noteable_name(noteable)
name = noteable.model_name.singular
# ProjectSnippet inherits from Snippet class so it causes
# routing error building the URL.
name == "project_snippet" ? "snippet" : name
end
# Handle errors when doing JIRA API calls # Handle errors when doing JIRA API calls
def jira_request def jira_request
yield yield
......
This diff is collapsed.
...@@ -8,6 +8,7 @@ class Service < ActiveRecord::Base ...@@ -8,6 +8,7 @@ class Service < ActiveRecord::Base
default_value_for :push_events, true default_value_for :push_events, true
default_value_for :issues_events, true default_value_for :issues_events, true
default_value_for :confidential_issues_events, true default_value_for :confidential_issues_events, true
default_value_for :commit_events, true
default_value_for :merge_requests_events, true default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true default_value_for :tag_push_events, true
default_value_for :note_events, true default_value_for :note_events, true
......
...@@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base ...@@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base
include Sortable include Sortable
include Elastic::SnippetsSearch include Elastic::SnippetsSearch
include Awardable include Awardable
include Mentionable
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content cache_markdown_field :content
......
...@@ -18,7 +18,9 @@ class Tree ...@@ -18,7 +18,9 @@ class Tree
def readme def readme
return @readme if defined?(@readme) return @readme if defined?(@readme)
available_readmes = blobs.select(&:readme?) available_readmes = blobs.select do |blob|
Gitlab::FileDetector.type_of(blob.name) == :readme
end
previewable_readmes = available_readmes.select do |blob| previewable_readmes = available_readmes.select do |blob|
previewable?(blob.name) previewable?(blob.name)
......
...@@ -247,19 +247,19 @@ class User < ActiveRecord::Base ...@@ -247,19 +247,19 @@ class User < ActiveRecord::Base
def filter(filter_name) def filter(filter_name)
case filter_name case filter_name
when 'admins' when 'admins'
self.admins admins
when 'blocked' when 'blocked'
self.blocked blocked
when 'two_factor_disabled' when 'two_factor_disabled'
self.without_two_factor without_two_factor
when 'two_factor_enabled' when 'two_factor_enabled'
self.with_two_factor with_two_factor
when 'wop' when 'wop'
self.without_projects without_projects
when 'external' when 'external'
self.external external
else else
self.active active
end end
end end
...@@ -378,7 +378,7 @@ class User < ActiveRecord::Base ...@@ -378,7 +378,7 @@ class User < ActiveRecord::Base
end end
def generate_password def generate_password
if self.force_random_password if force_random_password
self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min) self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
end end
end end
...@@ -419,56 +419,55 @@ class User < ActiveRecord::Base ...@@ -419,56 +419,55 @@ class User < ActiveRecord::Base
end end
def two_factor_otp_enabled? def two_factor_otp_enabled?
self.otp_required_for_login? otp_required_for_login?
end end
def two_factor_u2f_enabled? def two_factor_u2f_enabled?
self.u2f_registrations.exists? u2f_registrations.exists?
end end
def namespace_uniq def namespace_uniq
# Return early if username already failed the first uniqueness validation # Return early if username already failed the first uniqueness validation
return if self.errors.key?(:username) && return if errors.key?(:username) &&
self.errors[:username].include?('has already been taken') errors[:username].include?('has already been taken')
namespace_name = self.username existing_namespace = Namespace.by_path(username)
existing_namespace = Namespace.by_path(namespace_name) if existing_namespace && existing_namespace != namespace
if existing_namespace && existing_namespace != self.namespace errors.add(:username, 'has already been taken')
self.errors.add(:username, 'has already been taken')
end end
end end
def avatar_type def avatar_type
unless self.avatar.image? unless avatar.image?
self.errors.add :avatar, "only images allowed" errors.add :avatar, "only images allowed"
end end
end end
def unique_email def unique_email
if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email) if !emails.exists?(email: email) && Email.exists?(email: email)
self.errors.add(:email, 'has already been taken') errors.add(:email, 'has already been taken')
end end
end end
def owns_notification_email def owns_notification_email
return if self.temp_oauth_email? return if temp_oauth_email?
self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email) errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
end end
def owns_public_email def owns_public_email
return if self.public_email.blank? return if public_email.blank?
self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email) errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end end
def update_emails_with_primary_email def update_emails_with_primary_email
primary_email_record = self.emails.find_by(email: self.email) primary_email_record = emails.find_by(email: email)
if primary_email_record if primary_email_record
primary_email_record.destroy primary_email_record.destroy
self.emails.create(email: self.email_was) emails.create(email: email_was)
self.update_secondary_emails! update_secondary_emails!
end end
end end
...@@ -656,7 +655,7 @@ class User < ActiveRecord::Base ...@@ -656,7 +655,7 @@ class User < ActiveRecord::Base
end end
def project_deploy_keys def project_deploy_keys
DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id) DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
end end
def accessible_deploy_keys def accessible_deploy_keys
...@@ -672,38 +671,38 @@ class User < ActiveRecord::Base ...@@ -672,38 +671,38 @@ class User < ActiveRecord::Base
end end
def sanitize_attrs def sanitize_attrs
%w(name username skype linkedin twitter).each do |attr| %w[name username skype linkedin twitter].each do |attr|
value = self.send(attr) value = public_send(attr)
self.send("#{attr}=", Sanitize.clean(value)) if value.present? public_send("#{attr}=", Sanitize.clean(value)) if value.present?
end end
end end
def set_notification_email def set_notification_email
if self.notification_email.blank? || !self.all_emails.include?(self.notification_email) if notification_email.blank? || !all_emails.include?(notification_email)
self.notification_email = self.email self.notification_email = email
end end
end end
def set_public_email def set_public_email
if self.public_email.blank? || !self.all_emails.include?(self.public_email) if public_email.blank? || !all_emails.include?(public_email)
self.public_email = '' self.public_email = ''
end end
end end
def update_secondary_emails! def update_secondary_emails!
self.set_notification_email set_notification_email
self.set_public_email set_public_email
self.save if self.notification_email_changed? || self.public_email_changed? save if notification_email_changed? || public_email_changed?
end end
def set_projects_limit def set_projects_limit
# `User.select(:id)` raises # `User.select(:id)` raises
# `ActiveModel::MissingAttributeError: missing attribute: projects_limit` # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
# without this safeguard! # without this safeguard!
return unless self.has_attribute?(:projects_limit) return unless has_attribute?(:projects_limit)
connection_default_value_defined = new_record? && !projects_limit_changed? connection_default_value_defined = new_record? && !projects_limit_changed?
return unless self.projects_limit.nil? || connection_default_value_defined return unless projects_limit.nil? || connection_default_value_defined
self.projects_limit = current_application_settings.default_projects_limit self.projects_limit = current_application_settings.default_projects_limit
end end
...@@ -733,7 +732,7 @@ class User < ActiveRecord::Base ...@@ -733,7 +732,7 @@ class User < ActiveRecord::Base
def with_defaults def with_defaults
User.defaults.each do |k, v| User.defaults.each do |k, v|
self.send("#{k}=", v) public_send("#{k}=", v)
end end
self self
...@@ -753,7 +752,7 @@ class User < ActiveRecord::Base ...@@ -753,7 +752,7 @@ class User < ActiveRecord::Base
# Thus it will automatically generate a new fragment # Thus it will automatically generate a new fragment
# when the event is updated because the key changes. # when the event is updated because the key changes.
def reset_events_cache def reset_events_cache
Event.where(author_id: self.id). Event.where(author_id: id).
order('id DESC').limit(1000). order('id DESC').limit(1000).
update_all(updated_at: Time.now) update_all(updated_at: Time.now)
end end
...@@ -786,8 +785,8 @@ class User < ActiveRecord::Base ...@@ -786,8 +785,8 @@ class User < ActiveRecord::Base
def all_emails def all_emails
all_emails = [] all_emails = []
all_emails << self.email unless self.temp_oauth_email? all_emails << email unless temp_oauth_email?
all_emails.concat(self.emails.map(&:email)) all_emails.concat(emails.map(&:email))
all_emails all_emails
end end
...@@ -801,21 +800,21 @@ class User < ActiveRecord::Base ...@@ -801,21 +800,21 @@ class User < ActiveRecord::Base
def ensure_namespace_correct def ensure_namespace_correct
# Ensure user has namespace # Ensure user has namespace
self.create_namespace!(path: self.username, name: self.username) unless self.namespace create_namespace!(path: username, name: username) unless namespace
if self.username_changed? if username_changed?
self.namespace.update_attributes(path: self.username, name: self.username) namespace.update_attributes(path: username, name: username)
end end
end end
def post_create_hook def post_create_hook
log_info("User \"#{self.name}\" (#{self.email}) was created") log_info("User \"#{name}\" (#{email}) was created")
notification_service.new_user(self, @reset_token) if self.created_by_id notification_service.new_user(self, @reset_token) if created_by_id
system_hook_service.execute_hooks_for(self, :create) system_hook_service.execute_hooks_for(self, :create)
end end
def post_destroy_hook def post_destroy_hook
log_info("User \"#{self.name}\" (#{self.email}) was removed") log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy) system_hook_service.execute_hooks_for(self, :destroy)
end end
...@@ -863,7 +862,7 @@ class User < ActiveRecord::Base ...@@ -863,7 +862,7 @@ class User < ActiveRecord::Base
end end
def oauth_authorized_tokens def oauth_authorized_tokens
Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
end end
# Returns the projects a user contributed to in the last year. # Returns the projects a user contributed to in the last year.
...@@ -994,7 +993,7 @@ class User < ActiveRecord::Base ...@@ -994,7 +993,7 @@ class User < ActiveRecord::Base
end end
def ensure_external_user_rights def ensure_external_user_rights
return unless self.external? return unless external?
self.can_create_group = false self.can_create_group = false
self.projects_limit = 0 self.projects_limit = 0
...@@ -1006,7 +1005,7 @@ class User < ActiveRecord::Base ...@@ -1006,7 +1005,7 @@ class User < ActiveRecord::Base
if current_application_settings.domain_blacklist_enabled? if current_application_settings.domain_blacklist_enabled?
blocked_domains = current_application_settings.domain_blacklist blocked_domains = current_application_settings.domain_blacklist
if domain_matches?(blocked_domains, self.email) if domain_matches?(blocked_domains, email)
error = 'is not from an allowed domain.' error = 'is not from an allowed domain.'
valid = false valid = false
end end
...@@ -1014,7 +1013,7 @@ class User < ActiveRecord::Base ...@@ -1014,7 +1013,7 @@ class User < ActiveRecord::Base
allowed_domains = current_application_settings.domain_whitelist allowed_domains = current_application_settings.domain_whitelist
unless allowed_domains.blank? unless allowed_domains.blank?
if domain_matches?(allowed_domains, self.email) if domain_matches?(allowed_domains, email)
valid = true valid = true
else else
error = "domain is not authorized for sign-up" error = "domain is not authorized for sign-up"
...@@ -1022,7 +1021,7 @@ class User < ActiveRecord::Base ...@@ -1022,7 +1021,7 @@ class User < ActiveRecord::Base
end end
end end
self.errors.add(:email, error) unless valid errors.add(:email, error) unless valid
valid valid
end end
......
...@@ -18,7 +18,7 @@ class GitPushService < BaseService ...@@ -18,7 +18,7 @@ class GitPushService < BaseService
# #
def execute def execute
@project.repository.after_create if @project.empty_repo? @project.repository.after_create if @project.empty_repo?
@project.repository.after_push_commit(branch_name, params[:newrev]) @project.repository.after_push_commit(branch_name)
if push_remove_branch? if push_remove_branch?
@project.repository.after_remove_branch @project.repository.after_remove_branch
...@@ -55,12 +55,32 @@ class GitPushService < BaseService ...@@ -55,12 +55,32 @@ class GitPushService < BaseService
execute_related_hooks execute_related_hooks
perform_housekeeping perform_housekeeping
update_caches
end end
def update_gitattributes def update_gitattributes
@project.repository.copy_gitattributes(params[:ref]) @project.repository.copy_gitattributes(params[:ref])
end end
def update_caches
if is_default_branch?
paths = Set.new
@push_commits.each do |commit|
commit.raw_diffs(deltas_only: true).each do |diff|
paths << diff.new_path
end
end
types = Gitlab::FileDetector.types_in_paths(paths.to_a)
else
types = []
end
ProjectCacheWorker.perform_async(@project.id, types)
end
protected protected
def execute_related_hooks def execute_related_hooks
...@@ -75,7 +95,6 @@ class GitPushService < BaseService ...@@ -75,7 +95,6 @@ class GitPushService < BaseService
@project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks)
Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute(mirror_update: mirror_update) Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute(mirror_update: mirror_update)
ProjectCacheWorker.perform_async(@project.id)
if push_remove_branch? if push_remove_branch?
AfterBranchDeleteService AfterBranchDeleteService
......
module MergeRequests module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails # Adds a todo to the parent merge_request when a CI build fails
#
def execute(commit_status) def execute(commit_status)
return if commit_status.allow_failure?
commit_status_merge_requests(commit_status) do |merge_request| commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request) todo_service.merge_request_build_failed(merge_request)
end end
end end
# Closes any pending build failed todos for the parent MRs when a build is retried # Closes any pending build failed todos for the parent MRs when a
# build is retried
#
def close(commit_status) def close(commit_status)
commit_status_merge_requests(commit_status) do |merge_request| commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request) todo_service.merge_request_build_retried(merge_request)
......
...@@ -48,11 +48,11 @@ module MergeRequests ...@@ -48,11 +48,11 @@ module MergeRequests
end end
# See if source and target branches exist # See if source and target branches exist
unless merge_request.source_project.commit(merge_request.source_branch) if merge_request.source_branch.present? && !merge_request.source_project.commit(merge_request.source_branch)
messages << "Source branch \"#{merge_request.source_branch}\" does not exist" messages << "Source branch \"#{merge_request.source_branch}\" does not exist"
end end
unless merge_request.target_project.commit(merge_request.target_branch) if merge_request.target_branch.present? && !merge_request.target_project.commit(merge_request.target_branch)
messages << "Target branch \"#{merge_request.target_branch}\" does not exist" messages << "Target branch \"#{merge_request.target_branch}\" does not exist"
end end
......
...@@ -61,7 +61,15 @@ module MergeRequests ...@@ -61,7 +61,15 @@ module MergeRequests
merge_requests = filter_merge_requests(merge_requests) merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request| merge_requests.each do |merge_request|
reload_diff(merge_request) unless branch_removed? if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
mr_commit_ids = merge_request.commits.map(&:id)
push_commit_ids = @commits.map(&:id)
matches = mr_commit_ids & push_commit_ids
merge_request.reload_diff if matches.any?
end
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
end end
end end
...@@ -180,16 +188,5 @@ module MergeRequests ...@@ -180,16 +188,5 @@ module MergeRequests
def branch_removed? def branch_removed?
Gitlab::Git.blank_ref?(@newrev) Gitlab::Git.blank_ref?(@newrev)
end end
def reload_diff(merge_request)
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
mr_commit_ids = merge_request.commits.map(&:id)
push_commit_ids = @commits.map(&:id)
matches = mr_commit_ids & push_commit_ids
merge_request.reload_diff if matches.any?
end
end
end end
end end
...@@ -22,9 +22,8 @@ ...@@ -22,9 +22,8 @@
.form-group .form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2' = f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
- data_attrs = { toggle: 'buttons' }
.btn-group{ data: data_attrs }
- restricted_level_checkboxes('restricted-visibility-help').each do |level| - restricted_level_checkboxes('restricted-visibility-help').each do |level|
.checkbox
= level = level
%span.help-block#restricted-visibility-help %span.help-block#restricted-visibility-help
Selected levels cannot be used by non-admin users for projects or snippets. Selected levels cannot be used by non-admin users for projects or snippets.
......
...@@ -5,8 +5,6 @@ ...@@ -5,8 +5,6 @@
%div.form-group %div.form-group
= f.label :password = f.label :password
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
%div.submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save"
- if devise_mapping.rememberable? - if devise_mapping.rememberable?
.remember-me.checkbox .remember-me.checkbox
%label{for: "user_remember_me"} %label{for: "user_remember_me"}
...@@ -14,3 +12,5 @@ ...@@ -14,3 +12,5 @@
%span Remember me %span Remember me
.pull-right.forgot-password .pull-right.forgot-password
= link_to "Forgot your password?", new_password_path(resource_name) = link_to "Forgot your password?", new_password_path(resource_name)
%div.submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save"
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
- if current_user - if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues") = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues")
.top-area - if group_issues(@group).exists?
.top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls .nav-controls
- if current_user - if current_user
...@@ -13,14 +14,16 @@ ...@@ -13,14 +14,16 @@
Subscribe Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues = render 'shared/issuable/filter', type: :issues
.row-content-block.second-block .row-content-block.second-block
Only issues from Only issues from the
%strong #{@group.name} %strong #{@group.name}
group are listed here. group are listed here.
- if current_user - if current_user
To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
.prepend-top-default .prepend-top-default
= render 'shared/issues' = render 'shared/issues'
- else
= render 'shared/empty_states/issues', project_select_button: true
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
= chat_name.chat_name = chat_name.chat_name
%td %td
- if chat_name.last_used_at - if chat_name.last_used_at
time_ago_with_tooltip(chat_name.last_used_at) = time_ago_with_tooltip(chat_name.last_used_at)
- else - else
Never Never
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= spinner = spinner
:javascript :javascript
var activity = new Activities(); var activity = new gl.Activities();
$(document).on('page:restore', function (event) { $(document).on('page:restore', function (event) {
activity.reloadActivities() activity.reloadActivities()
}) })
.empty-stage-container
.empty-stage
.icon-no-data
= custom_icon ('icon_no_data')
%h4 We don’t have enough data to show this stage.
%p
{{currentStage.emptyStageText}}
.no-access-stage-container
.no-access-stage
.icon-lock
= custom_icon ('icon_lock')
%h4 You need permission.
%p
Want to see the data? Please ask administrator for access.
.cycle-analytics-overview
.container
.row
.col-md-10.col-md-offset-1
.row.overview-details
.col-md-6.overview-text
%h4 Introducing Cycle Analytics
%p
Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
To set up CA, you must first define a production environment by setting up your CI and then deploy to production.
%p
%a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more
.col-md-6.overview-image
%span.overview-icon
= custom_icon ('icon_cycle_analytics_overview')
- @no_container = true - @no_container = true
- page_title "Cycle Analytics" - page_title "Cycle Analytics"
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('cycle_analytics/cycle_analytics_bundle.js') = page_specific_javascript_tag("cycle_analytics/cycle_analytics_bundle.js")
= render "projects/pipelines/head" = render "projects/pipelines/head"
#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }} #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
- if @cycle_analytics_no_data
.bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"} .bordered-box.landing.content-block{"v-if" => "!isOverviewDialogDismissed"}
= icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()") = icon("times", class: "dismiss-icon", "@click" => "dismissOverviewDialog()")
.row .row
.col-sm-3.col-xs-12.svg-container .col-sm-3.col-xs-12.svg-container
= custom_icon('icon_cycle_analytics_splash') = custom_icon('icon_cycle_analytics_splash')
...@@ -20,21 +19,17 @@ ...@@ -20,21 +19,17 @@
Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
= link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
= icon("spinner spin", "v-show" => "isLoading") = icon("spinner spin", "v-show" => "isLoading")
.wrapper{"v-show" => "!isLoading && !hasError"} .wrapper{"v-show" => "!isLoading && !hasError"}
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Pipeline Health Pipeline Health
.content-block .content-block
.container-fluid .container-fluid
.row .row
.col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"} .col-sm-3.col-xs-12.column{"v-for" => "item in state.summary"}
%h3.header {{item.value}} %h3.header {{item.value}}
%p.text {{item.title}} %p.text {{item.title}}
.col-sm-3.col-xs-12.column .col-sm-3.col-xs-12.column
.dropdown.inline.js-ca-dropdown .dropdown.inline.js-ca-dropdown
%button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"} %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
...@@ -42,22 +37,54 @@ ...@@ -42,22 +37,54 @@
%i.fa.fa-chevron-down %i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li %li
%a{'href' => "#", 'data-value' => '30'} %a{ "href" => "#", "data-value" => "30" }
Last 30 days Last 30 days
%li %li
%a{'href' => "#", 'data-value' => '90'} %a{ "href" => "#", "data-value" => "90" }
Last 90 days Last 90 days
.stage-panel-container
.bordered-box .panel.panel-default.stage-panel
%ul.content-list .panel-heading
%li{"v-for" => "item in analytics.stats"} %nav.col-headers
.container-fluid %ul
.row %li.stage-header
.col-xs-8.title-col %span.stage-name
%p.title Stage
{{item.title}} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The phase of the development lifecycle.", "aria-hidden" => "true" }
%p.text %li.median-header
{{item.description}} %span.stage-name
.col-xs-4.value-col Median
%span %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.", "aria-hidden" => "true" }
{{item.value}} %li.event-header
%span.stage-name
{{ currentStage ? currentStage.legend : 'Related Issues' }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The collection of events added to the data gathered for that stage.", "aria-hidden" => "true" }
%li.total-time-header
%span.stage-name
Total Time
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The time taken by each data entry gathered by that stage.", "aria-hidden" => "true" }
.stage-panel-body
%nav.stage-nav
%ul
%li.stage-nav-item{ ':class' => '{ active: stage.active }', '@click' => 'selectStage(stage)', "v-for" => "stage in state.stages" }
.stage-nav-item-cell.stage-name
{{ stage.title }}
.stage-nav-item-cell.stage-median
%template{ "v-if" => "stage.isUserAllowed" }
%span{ "v-if" => "stage.value" }
{{ stage.value }}
%span.stage-empty{ "v-else" => true }
Not enough data
%template{ "v-else" => true }
%span.not-available
Not available
.section.stage-events
%template{ "v-if" => "isLoadingStage" }
= icon("spinner spin")
%template{ "v-if" => "currentStage && !currentStage.isUserAllowed" }
= render partial: "no_access"
%template{ "v-else" => true }
%template{ "v-if" => "isEmptyStage && !isLoadingStage" }
= render partial: "empty_stage"
%template{ "v-if" => "state.events.length && !isLoadingStage && !isEmptyStage" }
%component{ ":is" => "currentStage.component", ":stage" => "currentStage", ":items" => "state.events" }
%ul.content-list.issues-list.issuable-list %ul.content-list.issues-list.issuable-list
= render partial: "projects/issues/issue", collection: @issues = render partial: "projects/issues/issue", collection: @issues
- if @issues.blank? - if @issues.blank?
%li = render 'shared/empty_states/issues'
.nothing-here-block No issues to show
- if @issues.present? - if @issues.present?
= paginate @issues, theme: "gitlab" = paginate @issues, theme: "gitlab"
...@@ -10,8 +10,8 @@ ...@@ -10,8 +10,8 @@
- if current_user - if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues") = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues")
%div{ class: (container_class) } - if project_issues(@project).exists?
- if @project.issues.any? %div{ class: (container_class) }
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls .nav-controls
...@@ -36,21 +36,5 @@ ...@@ -36,21 +36,5 @@
= render 'issues' = render 'issues'
- if new_issue_email - if new_issue_email
= render 'issue_by_email', email: new_issue_email = render 'issue_by_email', email: new_issue_email
- else - else
.blank-state.blank-state-welcome = render 'shared/empty_states/issues', button_path: new_namespace_project_issue_path(@project.namespace, @project)
%h2.blank-state-title.blank-state-welcome-title
Welcome to GitLab Issues
%p.blank-state-text
Code, test, and deploy together
.blank-state
.blank-state-icon
= custom_icon("issues", size: 50)
%h3.blank-state-title
You don't have any issues right now.
%p.blank-state-text
Issues are the best way to track your project progress
- if can? current_user, :create_issue, @project
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
New Issue
- if new_issue_email
= render 'issue_by_email', email: new_issue_email
...@@ -13,10 +13,10 @@ ...@@ -13,10 +13,10 @@
= render 'projects/merge_requests/widget/open/archived' = render 'projects/merge_requests/widget/open/archived'
- elsif @project.above_size_limit? - elsif @project.above_size_limit?
= render 'projects/merge_requests/widget/open/size_limit_reached' = render 'projects/merge_requests/widget/open/size_limit_reached'
- elsif @merge_request.commits.blank?
= render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.branch_missing? - elsif @merge_request.branch_missing?
= render 'projects/merge_requests/widget/open/missing_branch' = render 'projects/merge_requests/widget/open/missing_branch'
- elsif @merge_request.commits.blank?
= render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.unchecked? - elsif @merge_request.unchecked?
= render 'projects/merge_requests/widget/open/check' = render 'projects/merge_requests/widget/open/check'
- elsif @merge_request.cannot_be_merged? && !resolved_conflicts - elsif @merge_request.cannot_be_merged? && !resolved_conflicts
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
= render "projects/issues/head" = render "projects/issues/head"
%div{ class: container_class } %div{ class: container_class }
.detail-page-header .detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) } .status-box{ class: status_box_class(@milestone) }
- if @milestone.closed? - if @milestone.closed?
Closed Closed
...@@ -12,13 +12,14 @@ ...@@ -12,13 +12,14 @@
Past due Past due
- else - else
Open Open
.header-text-content
%span.identifier %span.identifier
Milestone ##{@milestone.iid} Milestone ##{@milestone.iid}
- if @milestone.expires_at - if @milestone.expires_at
%span.creator %span.creator
&middot; &middot;
= @milestone.expires_at = @milestone.expires_at
.pull-right .milestone-buttons
- if can?(current_user, :admin_milestone, @project) - if can?(current_user, :admin_milestone, @project)
- if @milestone.active? - if @milestone.active?
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
......
...@@ -13,4 +13,4 @@ ...@@ -13,4 +13,4 @@
= render 'projects/issues/issue', issue: issue = render 'projects/issues/issue', issue: issue
= paginate @issues, theme: "gitlab" = paginate @issues, theme: "gitlab"
- else - else
.nothing-here-block No issues to show = render 'shared/empty_states/issues'
- button_path = local_assigns.fetch(:button_path, false)
- project_select_button = local_assigns.fetch(:project_select_button, false)
- has_button = button_path || project_select_button
.row.empty-state
.pull-right.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
.svg-content
= render 'shared/empty_states/icons/issues.svg'
.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
.text-content
- if has_button
%h4
The Issue Tracker is a good place to add things that need to be improved or solved in a project!
%p
An issue can be a bug, a todo or a feature request that needs to be discussed in a project.
Besides, issues are searchable and filterable.
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
- else
= link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
- else
%h4.text-center There are no issues to show.
This diff is collapsed.
<svg width="14px" height="10px" viewBox="322 21 14 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M330.078605,22.8166945 L335.259532,29.6235062 C335.615145,30.0907182 335.412062,30.4694683 334.822641,30.4694683 L331.657805,30.4694683 L324.04678,30.4694683 C323.449879,30.4694683 323.260751,30.0822112 323.609889,29.6235062 L328.790816,22.8166945 C329.146429,22.3494825 329.729467,22.3579895 330.078605,22.8166945 Z" id="delta" stroke="#5C5C5C" stroke-width="1" fill="none"></path>
</svg>
This diff is collapsed.
<svg width="46px" height="54px" viewBox="227 0 46 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
<desc>Created with Sketch.</desc>
<defs>
<rect id="path-1" x="0" y="20" width="46" height="34" rx="8"></rect>
<mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="46" height="34" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<path d="M29,16 C29,8.2680135 22.7319865,2 15,2 C7.2680135,2 1,8.2680135 1,16" id="path-3"></path>
<mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="28" height="14" fill="white">
<use xlink:href="#path-3"></use>
</mask>
</defs>
<g id="locker" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(227.000000, 0.000000)">
<g id="Group-8">
<use id="Rectangle-14" stroke="#B5A7DD" mask="url(#mask-2)" stroke-width="6" xlink:href="#path-1"></use>
<g id="Group-7" transform="translate(8.000000, 0.000000)">
<use id="Oval-3" stroke="#B5A7DD" mask="url(#mask-4)" stroke-width="6" xlink:href="#path-3"></use>
<rect id="Rectangle-13" fill="#B5A7DD" x="1" y="16" width="3" height="6"></rect>
<rect id="Rectangle-13-Copy" fill="#B5A7DD" x="26" y="16" width="3" height="6"></rect>
</g>
<path d="M25,37.4648712 C26.1956027,36.7732524 27,35.4805647 27,34 C27,31.790861 25.209139,30 23,30 C20.790861,30 19,31.790861 19,34 C19,35.4805647 19.8043973,36.7732524 21,37.4648712 L21,41.0026083 C21,42.1041422 21.8954305,43 23,43 C24.1122704,43 25,42.1057373 25,41.0026083 L25,37.4648712 Z" id="Combined-Shape" fill="#6B4FBB"></path>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="211 0 78 36" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<circle id="a" cx="5" cy="31" r="5"/>
<mask id="e" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#a"/>
</mask>
<circle id="b" cx="29" cy="14" r="5"/>
<mask id="f" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#b"/>
</mask>
<circle id="c" cx="53" cy="24" r="5"/>
<mask id="g" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#c"/>
</mask>
<circle id="d" cx="73" cy="5" r="5"/>
<mask id="h" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#d"/>
</mask>
</defs>
<g fill="none" fill-rule="evenodd" transform="translate(211)">
<path stroke="#B5A7DD" stroke-width="2" d="M5 31l24-17 26 10L73 5" stroke-linecap="round" stroke-dasharray="3 6"/>
<use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#e)" xlink:href="#a"/>
<use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#f)" xlink:href="#b"/>
<use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#g)" xlink:href="#c"/>
<use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#h)" xlink:href="#d"/>
</g>
</svg>
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
.context.prepend-top-default .context.prepend-top-default
.milestone-summary .milestone-summary
%h4 Progress %h4 Progress
.milestone-stats-and-buttons
.milestone-stats
%span.milestone-stat.with-drilldown
%strong= milestone.issues_visible_to_user(current_user).size %strong= milestone.issues_visible_to_user(current_user).size
issues: issues:
%span.milestone-stat %span.milestone-stat
...@@ -10,6 +14,7 @@ ...@@ -10,6 +14,7 @@
open and open and
%strong= milestone.issues_visible_to_user(current_user).closed.size %strong= milestone.issues_visible_to_user(current_user).closed.size
closed closed
%span.milestone-stat.with-drilldown
%strong= milestone.merge_requests.size %strong= milestone.merge_requests.size
merge requests: merge requests:
%span.milestone-stat %span.milestone-stat
...@@ -20,22 +25,16 @@ ...@@ -20,22 +25,16 @@
%span.milestone-stat %span.milestone-stat
%strong== #{milestone.percent_complete(current_user)}% %strong== #{milestone.percent_complete(current_user)}%
complete complete
- remaining_days = milestone_remaining_days(milestone)
- if remaining_days
%span.milestone-stat %span.milestone-stat
%span.remaining-days= remaining_days %span.remaining-days= milestone_remaining_days(milestone)
- total_weight = milestone.issues_visible_to_user(current_user).sum(:weight)
- unless total_weight.zero? .milestone-progress-buttons
%span.milestone-stat %span.tab-issues-buttons
Total weight:
%strong= total_weight
%span.pull-right.tab-issues-buttons
- if project && can?(current_user, :create_issue, project) - if project && can?(current_user, :create_issue, project)
= link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do
New Issue New Issue
= link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped" = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
%span.pull-right.tab-merge-requests-buttons.hidden %span.tab-merge-requests-buttons.hidden
= link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped" = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn"
= milestone_progress_bar(milestone) = milestone_progress_bar(milestone)
...@@ -10,7 +10,7 @@ class ElasticCommitIndexerWorker ...@@ -10,7 +10,7 @@ class ElasticCommitIndexerWorker
project = Project.find(project_id) project = Project.find(project_id)
repository = project.repository repository = project.repository
return true unless repository.head_exists? return true unless repository.exists?
indexer = Gitlab::Elastic::Indexer.new indexer = Gitlab::Elastic::Indexer.new
indexer.run( indexer.run(
......
...@@ -24,12 +24,12 @@ class GeoRepositoryUpdateWorker ...@@ -24,12 +24,12 @@ class GeoRepositoryUpdateWorker
def process_hooks def process_hooks
if @push_data['type'] == 'push' if @push_data['type'] == 'push'
branch = Gitlab::Git.ref_name(@push_data['ref']) branch = Gitlab::Git.ref_name(@push_data['ref'])
process_push(branch, @push_data['after']) process_push(branch)
end end
end end
def process_push(branch, revision) def process_push(branch)
@project.repository.after_push_commit(branch, revision) @project.repository.after_push_commit(branch)
if push_remove_branch? if push_remove_branch?
@project.repository.after_remove_branch @project.repository.after_remove_branch
......
# Worker for updating any project specific caches. # Worker for updating any project specific caches.
#
# This worker runs at most once every 15 minutes per project. This is to ensure
# that multiple instances of jobs for this worker don't hammer the underlying
# storage engine as much.
class ProjectCacheWorker class ProjectCacheWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
...@@ -10,46 +6,34 @@ class ProjectCacheWorker ...@@ -10,46 +6,34 @@ class ProjectCacheWorker
LEASE_TIMEOUT = 15.minutes.to_i LEASE_TIMEOUT = 15.minutes.to_i
def self.lease_for(project_id) # project_id - The ID of the project for which to flush the cache.
Gitlab::ExclusiveLease. # refresh - An Array containing extra types of data to refresh such as
new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT) # `:readme` to flush the README and `:changelog` to flush the
end # CHANGELOG.
def perform(project_id, refresh = [])
# Overwrite Sidekiq's implementation so we only schedule when actually needed. project = Project.find_by(id: project_id)
def self.perform_async(project_id)
# If a lease for this project is still being held there's no point in
# scheduling a new job.
super unless lease_for(project_id).exists?
end
def perform(project_id) return unless project && project.repository.exists?
if try_obtain_lease_for(project_id)
Rails.logger.
info("Obtained ProjectCacheWorker lease for project #{project_id}")
else
Rails.logger.
info("Could not obtain ProjectCacheWorker lease for project #{project_id}")
return update_repository_size(project)
end project.update_commit_count
update_caches(project_id) project.repository.refresh_method_caches(refresh.map(&:to_sym))
end end
def update_caches(project_id) def update_repository_size(project)
project = Project.find(project_id) return unless try_obtain_lease_for(project.id, :update_repository_size)
return unless project.repository.exists? Rails.logger.info("Updating repository size for project #{project.id}")
project.update_repository_size project.update_repository_size
project.update_commit_count
if project.repository.root_ref
project.repository.build_cache
end
end end
def try_obtain_lease_for(project_id) private
self.class.lease_for(project_id).try_obtain
def try_obtain_lease_for(project_id, section)
Gitlab::ExclusiveLease.
new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT).
try_obtain
end end
end end
---
title: Changed restricted visibility admin buttons to checkboxes
merge_request: 7463
author:
---
title: Show events per stage on Cycle Analytics page
merge_request: 23449
author:
---
title: Fix activity page endless scroll on large viewports
merge_request: 7608
author:
---
title: Fix regression causing bad error message to appear on Merge Request form
merge_request: 7599
author: Alex Sanford
---
title: Add deployment command to ChatOps
merge_request: 7619
author:
---
title: Add api endpoint for creating a pipeline
merge_request: 7209
author: Ido Leibovich
---
title: Fix 500 error when group name ends with git
merge_request: 7630
author:
---
title: Fix 404 on some group pages when name contains dot
merge_request: 7614
author:
---
title: Send credentials (currently for registry only) with build data to GitLab Runner
merge_request: 7474
author:
---
title: Added permissions per stage to cycle analytics endpoint
merge_request:
author:
---
title: Do not create a new TODO when failed build is allowed to fail
merge_request: 7618
author:
---
title: Do not create a MergeRequestDiff record when source branch is deleted
merge_request: 7481
author:
---
title: Fix errors happening when source branch of merge request is removed and then restored
merge_request: 7568
author:
---
title: Fix JIRA references for project snippets
merge_request:
author:
---
title: Allow enabling and disabling commit and MR events for JIRA
merge_request:
author:
---
title: Remove unnecessary self from user model
merge_request: 7551
author: Semyon Pupkov
---
title: Rework cache invalidation so only changed data is refreshed
merge_request: 7360
author:
...@@ -14,7 +14,9 @@ end ...@@ -14,7 +14,9 @@ end
resources :groups, only: [:index, :new, :create] resources :groups, only: [:index, :new, :create]
scope(path: 'groups/:id', controller: :groups) do scope(path: 'groups/:id',
controller: :groups,
constraints: { id: Gitlab::Regex.namespace_route_regex }) do
get :edit, as: :edit_group get :edit, as: :edit_group
get :issues, as: :issues_group get :issues, as: :issues_group
get :merge_requests, as: :merge_requests_group get :merge_requests, as: :merge_requests_group
...@@ -22,7 +24,10 @@ scope(path: 'groups/:id', controller: :groups) do ...@@ -22,7 +24,10 @@ scope(path: 'groups/:id', controller: :groups) do
get :activity, as: :activity_group get :activity, as: :activity_group
end end
scope(path: 'groups/:group_id', module: :groups, as: :group) do scope(path: 'groups/:group_id',
module: :groups,
as: :group,
constraints: { group_id: Gitlab::Regex.namespace_route_regex }) do
## EE-specific ## EE-specific
resource :analytics, only: [:show] resource :analytics, only: [:show]
resource :ldap, only: [] do resource :ldap, only: [] do
...@@ -61,4 +66,4 @@ scope(path: 'groups/:group_id', module: :groups, as: :group) do ...@@ -61,4 +66,4 @@ scope(path: 'groups/:group_id', module: :groups, as: :group) do
end end
# Must be last route in this file # Must be last route in this file
get 'groups/:id' => 'groups#show', as: :group_canonical get 'groups/:id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex }
class AddCommitEventsToServices < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:services, :commit_events, :boolean, default: true, allow_null: false)
end
def down
remove_column(:services, :commit_events)
end
end
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