Commit 17388eb0 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into fix-cancelling-pipelines

* upstream/master: (133 commits)
  Restructure steps for MM slash commands service
  Add Changelog entry for CI linter validation fix
  Fix entry lookup in CI config inheritance rules
  Extend specs for global ci configuration entry
  Remove unnecessary require_relative calls from service classes
  Use single quote for strings
  Ue svg from SVGs object
  Dont trigger CI builds [ci skip]
  Revert "Test only migrations"
  Add custom copy for each empty stage
  Refactor Mattermost slash commands docs
  Fetch only one revision
  Highlight nav item on hover
  Test only migrations
  Fix migration paths tests
  Scroll CA stage panel on mobile
  Fix CSS declaration
  administer to administrator
  Move SVGs to JS objects for easy reuse
  Improve deploy command message
  ...
parents c7c4850d acaa6d73
...@@ -23,7 +23,9 @@ ...@@ -23,7 +23,9 @@
"spyOn": false, "spyOn": false,
"spyOnEvent": false, "spyOnEvent": false,
"Turbolinks": false, "Turbolinks": false,
"window": false "window": false,
"Vue": false,
"Flash": false,
"Cookies": false
} }
} }
...@@ -271,12 +271,17 @@ rake db:seed_fu: ...@@ -271,12 +271,17 @@ rake db:seed_fu:
- log/development.log - log/development.log
teaspoon: teaspoon:
cache:
paths:
- vendor/ruby
- node_modules/
stage: test stage: test
<<: *use-db <<: *use-db
script: script:
- curl --silent --location https://deb.nodesource.com/setup_6.x | bash - - curl --silent --location https://deb.nodesource.com/setup_6.x | bash -
- apt-get install --assume-yes nodejs - apt-get install --assume-yes nodejs
- npm install --global istanbul - npm install
- npm link istanbul
- rake teaspoon - rake teaspoon
artifacts: artifacts:
name: coverage-javascript name: coverage-javascript
...@@ -319,12 +324,11 @@ migration paths: ...@@ -319,12 +324,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
...@@ -346,8 +350,11 @@ coverage: ...@@ -346,8 +350,11 @@ coverage:
- coverage/assets/ - coverage/assets/
lint-javascript: lint-javascript:
cache:
paths:
- node_modules/
stage: test stage: test
image: "node:latest" image: "node:7.1"
before_script: before_script:
- npm install - npm install
script: script:
......
...@@ -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
((global) => { //= require_tree .
const COOKIE_NAME = 'cycle_analytics_help_dismissed'; $(() => {
const store = gl.cycleAnalyticsStore = { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
isLoading: true, const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
hasError: false, const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
isHelpDismissed: Cookies.get(COOKIE_NAME), const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
analytics: {} requestPath: cycleAnalyticsEl.dataset.requestPath,
}; });
gl.CycleAnalytics = class CycleAnalytics { gl.cycleAnalyticsApp = new Vue({
constructor() { el: '#cycle-analytics',
const that = this; name: 'CycleAnalytics',
data: {
this.vue = new Vue({ state: cycleAnalyticsStore.state,
el: '#cycle-analytics', isLoading: false,
name: 'CycleAnalytics', isLoadingStage: false,
created: this.fetchData(), isEmptyStage: false,
data: store, hasError: false,
methods: { startDate: 30,
dismissLanding() { isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
that.dismissLanding(); },
} computed: {
currentStage() {
return cycleAnalyticsStore.currentActiveStage();
},
},
components: {
'stage-issue-component': gl.cycleAnalytics.StageIssueComponent,
'stage-plan-component': gl.cycleAnalytics.StagePlanComponent,
'stage-code-component': gl.cycleAnalytics.StageCodeComponent,
'stage-test-component': gl.cycleAnalytics.StageTestComponent,
'stage-review-component': gl.cycleAnalytics.StageReviewComponent,
'stage-staging-component': gl.cycleAnalytics.StageStagingComponent,
'stage-production-component': gl.cycleAnalytics.StageProductionComponent,
},
created() {
this.fetchCycleAnalyticsData();
},
methods: {
handleError() {
cycleAnalyticsStore.setErrorState(true);
return new Flash('There was an error while fetching cycle analytics data.');
},
initDropdown() {
const $dropdown = $('.js-ca-dropdown');
const $label = $dropdown.find('.dropdown-label');
$dropdown.find('li a').off('click').on('click', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
this.startDate = $target.data('value');
$label.text($target.text().trim());
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;
} }
});
}
fetchData(options) {
store.isLoading = true;
options = options || { startDate: 30 };
$.ajax({
url: $('#cycle-analytics').data('request-path'),
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: {
cycle_analytics: {
start_date: options.startDate
}
}
}).done((data) => {
this.decorateData(data);
this.initDropdown();
})
.error((data) => {
this.handleError(data);
})
.always(() => {
store.isLoading = false;
})
}
decorateData(data) {
data.summary = data.summary || [];
data.stats = data.stats || [];
data.summary.forEach((item) => {
item.value = item.value || '-';
});
data.stats.forEach((item) => {
item.value = item.value || '- - -';
});
store.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() {
const $dropdown = $('.js-ca-dropdown');
const $label = $dropdown.find('.dropdown-label');
$dropdown.find('li a').off('click').on('click', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
const value = $target.data('value');
$label.text($target.text().trim());
this.fetchData({ startDate: value });
})
}
}
})(window.gl || (window.gl = {})); 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');
},
},
});
// 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 = {}));
...@@ -110,10 +110,10 @@ ...@@ -110,10 +110,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();
...@@ -139,7 +139,7 @@ ...@@ -139,7 +139,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();
...@@ -208,9 +208,6 @@ ...@@ -208,9 +208,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"
......
(() => {
/*
* TODO: Make these methods more configurable (e.g. parseSeconds timePeriodContstraints,
* stringifyTime condensed or non-condensed, abbreviateTimelengths)
* */
class PrettyTime {
/*
* Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
* Seconds can be negative or positive, zero or non-zero.
*/
static parseSeconds(seconds) {
const DAYS_PER_WEEK = 5;
const HOURS_PER_DAY = 8;
const MINUTES_PER_HOUR = 60;
const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
const timePeriodConstraints = {
weeks: MINUTES_PER_WEEK,
days: MINUTES_PER_DAY,
hours: MINUTES_PER_HOUR,
minutes: 1,
};
let unorderedMinutes = PrettyTime.secondsToMinutes(seconds);
return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
unorderedMinutes -= (periodCount * minutesPerPeriod);
return periodCount;
});
}
/*
* Accepts a timeObject and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
*/
static stringifyTime(timeObject) {
const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
}, '').trim();
return reducedTime.length ? reducedTime : '0m';
}
/*
* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
* the first non-zero unit/value pair.
*/
static abbreviateTime(timeStr) {
return timeStr.split(' ')
.filter(unitStr => unitStr.charAt(0) !== '0')[0];
}
static secondsToMinutes(seconds) {
return Math.abs(seconds / 60);
}
}
gl.PrettyTime = PrettyTime;
})(window.gl || (window.gl = {}));
...@@ -218,7 +218,7 @@ ...@@ -218,7 +218,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;
})();
/*
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API.
*
* */
(() => {
class SmartInterval {
/**
* @param { function } callback Function to be called on each iteration (required)
* @param { milliseconds } startingInterval `currentInterval` is set to this initially
* @param { milliseconds } maxInterval `currentInterval` will be incremented to this
* @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor
* @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily
*/
constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) {
this.cfg = {
callback,
startingInterval,
maxInterval,
incrementByFactorOf,
lazyStart,
};
this.state = {
intervalId: null,
currentInterval: startingInterval,
pageVisibility: 'visible',
};
this.initInterval();
}
/* public */
start() {
const cfg = this.cfg;
const state = this.state;
state.intervalId = window.setInterval(() => {
cfg.callback();
if (this.getCurrentInterval() === cfg.maxInterval) {
return;
}
this.incrementInterval();
this.resume();
}, this.getCurrentInterval());
}
// cancel the existing timer, setting the currentInterval back to startingInterval
cancel() {
this.setCurrentInterval(this.cfg.startingInterval);
this.stopTimer();
}
// start a timer, using the existing interval
resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
this.start();
}
destroy() {
this.cancel();
$(document).off('visibilitychange').off('page:before-unload');
}
/* private */
initInterval() {
const cfg = this.cfg;
if (!cfg.lazyStart) {
this.start();
}
this.initVisibilityChangeHandling();
this.initPageUnloadHandling();
}
initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling)
$(document)
.off('visibilitychange').on('visibilitychange', (e) => {
this.state.pageVisibility = e.target.visibilityState;
this.handleVisibilityChange();
});
}
initPageUnloadHandling() {
// prevent interval continuing after page change, when kept in cache by Turbolinks
$(document).on('page:before-unload', () => this.cancel());
}
handleVisibilityChange() {
const state = this.state;
const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume;
intervalAction.apply(this);
}
getCurrentInterval() {
return this.state.currentInterval;
}
setCurrentInterval(newInterval) {
this.state.currentInterval = newInterval;
}
incrementInterval() {
const cfg = this.cfg;
const currentInterval = this.getCurrentInterval();
let nextInterval = currentInterval * cfg.incrementByFactorOf;
if (nextInterval > cfg.maxInterval) {
nextInterval = cfg.maxInterval;
}
this.setCurrentInterval(nextInterval);
}
stopTimer() {
const state = this.state;
state.intervalId = window.clearInterval(state.intervalId);
}
}
gl.SmartInterval = SmartInterval;
})(window.gl || (window.gl = {}));
//= require vue
//= require vue-resource
(() => {
/*
* SubbableResource can be extended to provide a pubsub-style service for one-off REST
* calls. Subscribe by passing a callback or render method you will use to handle responses.
*
* */
class SubbableResource {
constructor(resourcePath) {
this.endpoint = resourcePath;
// TODO: Switch to axios.create
this.resource = $.ajax;
this.subscribers = [];
}
subscribe(callback) {
this.subscribers.push(callback);
}
publish(newResponse) {
const responseCopy = _.extend({}, newResponse);
this.subscribers.forEach((fn) => {
fn(responseCopy);
});
return newResponse;
}
get(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
post(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
put(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
delete(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
}
gl.SubbableResource = SubbableResource;
})(window.gl || (window.gl = {}));
...@@ -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">
...@@ -156,14 +155,15 @@ ...@@ -156,14 +155,15 @@
class="avatar-image-container" class="avatar-image-container"
:href="author.web_url"> :href="author.web_url">
<img <img
class="avatar has-tooltip s20" class="avatar has-tooltip s20"
:src="author.avatar_url" :src="author.avatar_url"
:alt="userImageAltDescription" :alt="userImageAltDescription"
:title="author.username" /> :title="author.username" />
</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%;
}
}
}
...@@ -160,6 +160,7 @@ $settings-icon-size: 18px; ...@@ -160,6 +160,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;
...@@ -283,6 +284,9 @@ $calendar-unselectable-bg: $gray-light; ...@@ -283,6 +284,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 { .js-ca-dropdown {
top: 13px; 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;
}
}
} }
...@@ -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
...@@ -82,12 +82,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -82,12 +82,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?
...@@ -417,7 +417,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -417,7 +417,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
} }
...@@ -564,7 +564,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -564,7 +564,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
......
...@@ -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
......
...@@ -48,4 +48,8 @@ module GroupsHelper ...@@ -48,4 +48,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
...@@ -455,4 +455,8 @@ module ProjectsHelper ...@@ -455,4 +455,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
...@@ -487,6 +487,10 @@ module Ci ...@@ -487,6 +487,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.
......
...@@ -1086,7 +1086,7 @@ class Project < ActiveRecord::Base ...@@ -1086,7 +1086,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
......
...@@ -128,15 +128,9 @@ class JiraService < IssueTrackerService ...@@ -128,15 +128,9 @@ class JiraService < IssueTrackerService
return 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.model_name.singular 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: {
...@@ -144,11 +138,11 @@ class JiraService < IssueTrackerService ...@@ -144,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
} }
...@@ -285,18 +279,26 @@ class JiraService < IssueTrackerService ...@@ -285,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
......
require 'securerandom' require 'securerandom'
class Repository class Repository
include Gitlab::ShellAdapter
attr_accessor :path_with_namespace, :project
class CommitError < StandardError; end class CommitError < StandardError; end
# Files to use as a project avatar in case no avatar was uploaded via the web # Methods that cache data from the Git repository.
# UI. #
AVATAR_FILES = %w{logo.png logo.jpg logo.gif} # Each entry in this Array should have a corresponding method with the exact
# same name. The cache key used by those methods must also match method's
# name.
#
# For example, for entry `:readme` there's a method called `readme` which
# stores its data in the `readme` cache key.
CACHED_METHODS = %i(size commit_count readme version contribution_guide
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref)
# Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = {
readme: :readme,
changelog: :changelog,
license: %i(license_blob license_key),
contributing: :contribution_guide,
version: :version,
gitignore: :gitignore,
koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml,
avatar: :avatar
}
# Wraps around the given method and caches its output in Redis and an instance
# variable.
#
# This only works for methods that do not take any arguments.
def self.cache_method(name, fallback: nil)
original = :"_uncached_#{name}"
include Gitlab::ShellAdapter alias_method(original, name)
attr_accessor :path_with_namespace, :project define_method(name) do
cache_method_output(name, fallback: fallback) { __send__(original) }
end
end
def self.storages def self.storages
Gitlab.config.repositories.storages Gitlab.config.repositories.storages
...@@ -37,24 +75,6 @@ class Repository ...@@ -37,24 +75,6 @@ class Repository
) )
end end
def exists?
return @exists unless @exists.nil?
@exists = cache.fetch(:exists?) do
begin
raw_repository && raw_repository.rugged ? true : false
rescue Gitlab::Git::Repository::NoRepository
false
end
end
end
def empty?
return @empty unless @empty.nil?
@empty = cache.fetch(:empty?) { raw_repository.empty? }
end
# #
# Git repository can contains some hidden refs like: # Git repository can contains some hidden refs like:
# /refs/notes/* # /refs/notes/*
...@@ -221,10 +241,6 @@ class Repository ...@@ -221,10 +241,6 @@ class Repository
branch_names + tag_names branch_names + tag_names
end end
def branch_names
@branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) }
end
def branch_exists?(branch_name) def branch_exists?(branch_name)
branch_names.include?(branch_name) branch_names.include?(branch_name)
end end
...@@ -274,34 +290,6 @@ class Repository ...@@ -274,34 +290,6 @@ class Repository
ref_exists?(keep_around_ref_name(sha)) ref_exists?(keep_around_ref_name(sha))
end end
def tag_names
cache.fetch(:tag_names) { raw_repository.tag_names }
end
def commit_count
cache.fetch(:commit_count) do
begin
raw_repository.commit_count(self.root_ref)
rescue
0
end
end
end
def branch_count
@branch_count ||= cache.fetch(:branch_count) { branches.size }
end
def tag_count
@tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
end
# Return repo size in megabytes
# Cached in redis
def size
cache.fetch(:size) { raw_repository.size }
end
def diverging_commit_counts(branch) def diverging_commit_counts(branch)
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
cache.fetch(:"diverging_commit_counts_#{branch.name}") do cache.fetch(:"diverging_commit_counts_#{branch.name}") do
...@@ -317,48 +305,55 @@ class Repository ...@@ -317,48 +305,55 @@ class Repository
end end
end end
# Keys for data that can be affected for any commit push. def expire_tags_cache
def cache_keys expire_method_caches(%i(tag_names tag_count))
%i(size commit_count @tags = nil
readme version contribution_guide changelog
license_blob license_key gitignore koding_yml)
end end
# Keys for data on branch/tag operations. def expire_branches_cache
def cache_keys_for_branches_and_tags expire_method_caches(%i(branch_names branch_count))
%i(branch_names tag_names branch_count tag_count) @local_branches = nil
end end
def build_cache def expire_statistics_caches
(cache_keys + cache_keys_for_branches_and_tags).each do |key| expire_method_caches(%i(size commit_count))
unless cache.exist?(key)
send(key)
end
end
end end
def expire_tags_cache def expire_all_method_caches
cache.expire(:tag_names) expire_method_caches(CACHED_METHODS)
@tags = nil
end end
def expire_branches_cache # Expires the caches of a specific set of methods
cache.expire(:branch_names) def expire_method_caches(methods)
@branch_names = nil methods.each do |key|
@local_branches = nil cache.expire(key)
ivar = cache_instance_variable_name(key)
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
end
end end
def expire_cache(branch_name = nil, revision = nil) def expire_avatar_cache
cache_keys.each do |key| expire_method_caches(%i(avatar))
cache.expire(key) end
# Refreshes the method caches of this repository.
#
# types - An Array of file types (e.g. `:readme`) used to refresh extra
# caches.
def refresh_method_caches(types)
to_refresh = []
types.each do |type|
methods = METHOD_CACHES_FOR_FILE_TYPES[type.to_sym]
to_refresh.concat(Array(methods)) if methods
end end
expire_branch_cache(branch_name) expire_method_caches(to_refresh)
expire_avatar_cache(branch_name, revision)
# This ensures this particular cache is flushed after the first commit to a to_refresh.each { |method| send(method) }
# new repository.
expire_emptiness_caches if empty?
end end
def expire_branch_cache(branch_name = nil) def expire_branch_cache(branch_name = nil)
...@@ -377,15 +372,14 @@ class Repository ...@@ -377,15 +372,14 @@ class Repository
end end
def expire_root_ref_cache def expire_root_ref_cache
cache.expire(:root_ref) expire_method_caches(%i(root_ref))
@root_ref = nil
end end
# Expires the cache(s) used to determine if a repository is empty or not. # Expires the cache(s) used to determine if a repository is empty or not.
def expire_emptiness_caches def expire_emptiness_caches
cache.expire(:empty?) return unless empty?
@empty = nil
expire_method_caches(%i(empty?))
expire_has_visible_content_cache expire_has_visible_content_cache
end end
...@@ -394,51 +388,22 @@ class Repository ...@@ -394,51 +388,22 @@ class Repository
@has_visible_content = nil @has_visible_content = nil
end end
def expire_branch_count_cache
cache.expire(:branch_count)
@branch_count = nil
end
def expire_tag_count_cache
cache.expire(:tag_count)
@tag_count = nil
end
def lookup_cache def lookup_cache
@lookup_cache ||= {} @lookup_cache ||= {}
end end
def expire_avatar_cache(branch_name = nil, revision = nil)
# Avatars are pulled from the default branch, thus if somebody pushes to a
# different branch there's no need to expire anything.
return if branch_name && branch_name != root_ref
# We don't want to flush the cache if the commit didn't actually make any
# changes to any of the possible avatar files.
if revision && commit = self.commit(revision)
return unless commit.raw_diffs(deltas_only: true).
any? { |diff| AVATAR_FILES.include?(diff.new_path) }
end
cache.expire(:avatar)
@avatar = nil
end
def expire_exists_cache def expire_exists_cache
cache.expire(:exists?) expire_method_caches(%i(exists?))
@exists = nil
end end
# expire cache that doesn't depend on repository data (when expiring) # expire cache that doesn't depend on repository data (when expiring)
def expire_content_cache def expire_content_cache
expire_tags_cache expire_tags_cache
expire_tag_count_cache
expire_branches_cache expire_branches_cache
expire_branch_count_cache
expire_root_ref_cache expire_root_ref_cache
expire_emptiness_caches expire_emptiness_caches
expire_exists_cache expire_exists_cache
expire_statistics_caches
end end
# Runs code after a repository has been created. # Runs code after a repository has been created.
...@@ -453,9 +418,8 @@ class Repository ...@@ -453,9 +418,8 @@ class Repository
# Runs code just before a repository is deleted. # Runs code just before a repository is deleted.
def before_delete def before_delete
expire_exists_cache expire_exists_cache
expire_all_method_caches
expire_cache if exists? expire_branch_cache if exists?
expire_content_cache expire_content_cache
repository_event(:remove_repository) repository_event(:remove_repository)
...@@ -472,9 +436,9 @@ class Repository ...@@ -472,9 +436,9 @@ class Repository
# Runs code before pushing (= creating or removing) a tag. # Runs code before pushing (= creating or removing) a tag.
def before_push_tag def before_push_tag
expire_cache expire_statistics_caches
expire_emptiness_caches
expire_tags_cache expire_tags_cache
expire_tag_count_cache
repository_event(:push_tag) repository_event(:push_tag)
end end
...@@ -482,7 +446,7 @@ class Repository ...@@ -482,7 +446,7 @@ class Repository
# Runs code before removing a tag. # Runs code before removing a tag.
def before_remove_tag def before_remove_tag
expire_tags_cache expire_tags_cache
expire_tag_count_cache expire_statistics_caches
repository_event(:remove_tag) repository_event(:remove_tag)
end end
...@@ -494,12 +458,14 @@ class Repository ...@@ -494,12 +458,14 @@ class Repository
# Runs code after a repository has been forked/imported. # Runs code after a repository has been forked/imported.
def after_import def after_import
expire_content_cache expire_content_cache
build_cache expire_tags_cache
expire_branches_cache
end end
# Runs code after a new commit has been pushed. # Runs code after a new commit has been pushed.
def after_push_commit(branch_name, revision) def after_push_commit(branch_name)
expire_cache(branch_name, revision) expire_statistics_caches
expire_branch_cache(branch_name)
repository_event(:push_commit, branch: branch_name) repository_event(:push_commit, branch: branch_name)
end end
...@@ -508,7 +474,6 @@ class Repository ...@@ -508,7 +474,6 @@ class Repository
def after_create_branch def after_create_branch
expire_branches_cache expire_branches_cache
expire_has_visible_content_cache expire_has_visible_content_cache
expire_branch_count_cache
repository_event(:push_branch) repository_event(:push_branch)
end end
...@@ -523,7 +488,6 @@ class Repository ...@@ -523,7 +488,6 @@ class Repository
# Runs code after an existing branch has been removed. # Runs code after an existing branch has been removed.
def after_remove_branch def after_remove_branch
expire_has_visible_content_cache expire_has_visible_content_cache
expire_branch_count_cache
expire_branches_cache expire_branches_cache
end end
...@@ -550,86 +514,127 @@ class Repository ...@@ -550,86 +514,127 @@ class Repository
Gitlab::Git::Blob.raw(self, oid) Gitlab::Git::Blob.raw(self, oid)
end end
def root_ref
if raw_repository
raw_repository.root_ref
else
# When the repo does not exist we raise this error so no data is cached.
raise Rugged::ReferenceError
end
end
cache_method :root_ref
def exists?
refs_directory_exists?
end
cache_method :exists?
def empty?
raw_repository.empty?
end
cache_method :empty?
# The size of this repository in megabytes.
def size
exists? ? raw_repository.size : 0.0
end
cache_method :size, fallback: 0.0
def commit_count
root_ref ? raw_repository.commit_count(root_ref) : 0
end
cache_method :commit_count, fallback: 0
def branch_names
branches.map(&:name)
end
cache_method :branch_names, fallback: []
def tag_names
raw_repository.tag_names
end
cache_method :tag_names, fallback: []
def branch_count
branches.size
end
cache_method :branch_count, fallback: 0
def tag_count
raw_repository.rugged.tags.count
end
cache_method :tag_count, fallback: 0
def avatar
if tree = file_on_head(:avatar)
tree.path
end
end
cache_method :avatar
def readme def readme
cache.fetch(:readme) { tree(:head).readme } if head = tree(:head)
head.readme
end
end end
cache_method :readme
def version def version
cache.fetch(:version) do file_on_head(:version)
tree(:head).blobs.find do |file|
file.name.casecmp('version').zero?
end
end
end end
cache_method :version
def contribution_guide def contribution_guide
cache.fetch(:contribution_guide) do file_on_head(:contributing)
tree(:head).blobs.find do |file|
file.contributing?
end
end
end end
cache_method :contribution_guide
def changelog def changelog
cache.fetch(:changelog) do file_on_head(:changelog)
file_on_head(/\A(changelog|history|changes|news)/i)
end
end end
cache_method :changelog
def license_blob def license_blob
return nil unless head_exists? file_on_head(:license)
cache.fetch(:license_blob) do
file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i)
end
end end
cache_method :license_blob
def license_key def license_key
return nil unless head_exists? return unless exists?
cache.fetch(:license_key) do Licensee.license(path).try(:key)
Licensee.license(path).try(:key)
end
end end
cache_method :license_key
def gitignore def gitignore
return nil if !exists? || empty? file_on_head(:gitignore)
cache.fetch(:gitignore) do
file_on_head(/\A\.gitignore\z/)
end
end end
cache_method :gitignore
def koding_yml def koding_yml
return nil unless head_exists? file_on_head(:koding)
cache.fetch(:koding_yml) do
file_on_head(/\A\.koding\.yml\z/)
end
end end
cache_method :koding_yml
def gitlab_ci_yml def gitlab_ci_yml
return nil unless head_exists? file_on_head(:gitlab_ci)
@gitlab_ci_yml ||= tree(:head).blobs.find do |file|
file.name == '.gitlab-ci.yml'
end
rescue Rugged::ReferenceError
# For unknow reason spinach scenario "Scenario: I change project path"
# lead to "Reference 'HEAD' not found" exception from Repository#empty?
nil
end end
cache_method :gitlab_ci_yml
def head_commit def head_commit
@head_commit ||= commit(self.root_ref) @head_commit ||= commit(self.root_ref)
end end
def head_tree def head_tree
@head_tree ||= Tree.new(self, head_commit.sha, nil) if head_commit
@head_tree ||= Tree.new(self, head_commit.sha, nil)
end
end end
def tree(sha = :head, path = nil, recursive: false) def tree(sha = :head, path = nil, recursive: false)
if sha == :head if sha == :head
return unless head_commit
if path.nil? if path.nil?
return head_tree return head_tree
else else
...@@ -779,10 +784,6 @@ class Repository ...@@ -779,10 +784,6 @@ class Repository
@tags ||= raw_repository.tags @tags ||= raw_repository.tags
end end
def root_ref
@root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
end
def commit_dir(user, path, message, branch, author_email: nil, author_name: nil) def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref| update_branch_with_hooks(user, branch) do |ref|
options = { options = {
...@@ -1140,28 +1141,55 @@ class Repository ...@@ -1140,28 +1141,55 @@ class Repository
end end
end end
def avatar # Caches the supplied block both in a cache and in an instance variable.
return nil unless exists? #
# The cache key and instance variable are named the same way as the value of
# the `key` argument.
#
# This method will return `nil` if the corresponding instance variable is also
# set to `nil`. This ensures we don't keep yielding the block when it returns
# `nil`.
#
# key - The name of the key to cache the data in.
# fallback - A value to fall back to in the event of a Git error.
def cache_method_output(key, fallback: nil, &block)
ivar = cache_instance_variable_name(key)
@avatar ||= cache.fetch(:avatar) do if instance_variable_defined?(ivar)
AVATAR_FILES.find do |file| instance_variable_get(ivar)
blob_at_branch(root_ref, file) else
begin
instance_variable_set(ivar, cache.fetch(key, &block))
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
# if e.g. HEAD or the entire repository doesn't exist we want to
# gracefully handle this and not cache anything.
fallback
end end
end end
end end
private def cache_instance_variable_name(key)
:"@#{key.to_s.tr('?!', '')}"
end
def cache def file_on_head(type)
@cache ||= RepositoryCache.new(path_with_namespace, @project.id) if head = tree(:head)
head.blobs.find do |file|
Gitlab::FileDetector.type_of(file.name) == type
end
end
end end
def head_exists? private
exists? && !empty? && !rugged.head_unborn?
def refs_directory_exists?
return false unless path_with_namespace
File.exist?(File.join(path_to_repo, 'refs'))
end end
def file_on_head(regex) def cache
tree(:head).blobs.find { |file| file.name =~ regex } @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
end end
def tags_sorted_by_committed_date def tags_sorted_by_committed_date
......
...@@ -6,6 +6,7 @@ class Snippet < ActiveRecord::Base ...@@ -6,6 +6,7 @@ class Snippet < ActiveRecord::Base
include Referable include Referable
include Sortable include Sortable
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)
......
require_relative 'base_service'
## ##
# Branch can be deleted either by DeleteBranchService # Branch can be deleted either by DeleteBranchService
# or by GitPushService. # or by GitPushService.
......
require_relative 'base_service'
class CreateBranchService < BaseService class CreateBranchService < BaseService
def execute(branch_name, ref, source_project: @project) def execute(branch_name, ref, source_project: @project)
valid_branch = Gitlab::GitRefValidator.validate(branch_name) valid_branch = Gitlab::GitRefValidator.validate(branch_name)
......
require_relative 'base_service'
class CreateDeploymentService < BaseService class CreateDeploymentService < BaseService
def execute(deployable = nil) def execute(deployable = nil)
return unless executable? return unless executable?
......
require_relative 'base_service'
class CreateReleaseService < BaseService class CreateReleaseService < BaseService
def execute(tag_name, release_description) def execute(tag_name, release_description)
repository = project.repository repository = project.repository
......
require_relative 'base_service'
class CreateTagService < BaseService class CreateTagService < BaseService
def execute(tag_name, target, message, release_description = nil) def execute(tag_name, target, message, release_description = nil)
valid_tag = Gitlab::GitRefValidator.validate(tag_name) valid_tag = Gitlab::GitRefValidator.validate(tag_name)
......
require_relative 'base_service'
class DeleteBranchService < BaseService class DeleteBranchService < BaseService
def execute(branch_name) def execute(branch_name)
repository = project.repository repository = project.repository
......
require_relative 'base_service'
class DeleteMergedBranchesService < BaseService class DeleteMergedBranchesService < BaseService
def async_execute def async_execute
DeleteMergedBranchesWorker.perform_async(project.id, current_user.id) DeleteMergedBranchesWorker.perform_async(project.id, current_user.id)
......
require_relative 'base_service'
class DeleteTagService < BaseService class DeleteTagService < BaseService
def execute(tag_name) def execute(tag_name)
repository = project.repository repository = project.repository
......
require_relative "base_service"
module Files module Files
class CreateDirService < Files::BaseService class CreateDirService < Files::BaseService
def commit def commit
......
require_relative "base_service"
module Files module Files
class CreateService < Files::BaseService class CreateService < Files::BaseService
def commit def commit
......
require_relative "base_service"
module Files module Files
class DeleteService < Files::BaseService class DeleteService < Files::BaseService
def commit def commit
......
require_relative "base_service"
module Files module Files
class MultiService < Files::BaseService class MultiService < Files::BaseService
class FileChangedError < StandardError; end class FileChangedError < StandardError; end
......
require_relative "base_service"
module Files module Files
class UpdateService < Files::BaseService class UpdateService < Files::BaseService
class FileChangedError < StandardError; end class FileChangedError < StandardError; 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
...@@ -51,12 +51,32 @@ class GitPushService < BaseService ...@@ -51,12 +51,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
...@@ -70,7 +90,6 @@ class GitPushService < BaseService ...@@ -70,7 +90,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 Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute
ProjectCacheWorker.perform_async(@project.id)
if push_remove_branch? if push_remove_branch?
AfterBranchDeleteService AfterBranchDeleteService
......
...@@ -60,7 +60,15 @@ module MergeRequests ...@@ -60,7 +60,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
...@@ -165,16 +173,5 @@ module MergeRequests ...@@ -165,16 +173,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
require_relative 'base_service'
require_relative 'reopen_service'
require_relative 'close_service'
module MergeRequests module MergeRequests
class UpdateService < MergeRequests::BaseService class UpdateService < MergeRequests::BaseService
def execute(merge_request) def execute(merge_request)
......
require_relative 'base_service'
class UpdateReleaseService < BaseService class UpdateReleaseService < BaseService
def execute(tag_name, release_description) def execute(tag_name, release_description)
repository = project.repository repository = project.repository
......
...@@ -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' } - restricted_level_checkboxes('restricted-visibility-help').each do |level|
.btn-group{ data: data_attrs } .checkbox
- restricted_level_checkboxes('restricted-visibility-help').each do |level|
= 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.
......
...@@ -3,24 +3,27 @@ ...@@ -3,24 +3,27 @@
- 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?
= render 'shared/issuable/nav', type: :issues .top-area
.nav-controls = render 'shared/issuable/nav', type: :issues
- if current_user .nav-controls
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do - if current_user
= icon('rss') = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
%span.icon-label = icon('rss')
Subscribe %span.icon-label
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" Subscribe
= 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')
.col-sm-8.col-xs-12.inner-content .col-sm-8.col-xs-12.inner-content
%h4 %h4
Introducing Cycle Analytics Introducing Cycle Analytics
%p %p
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
...@@ -9,10 +9,10 @@ ...@@ -9,10 +9,10 @@
- if @project.archived? - if @project.archived?
= render 'projects/merge_requests/widget/open/archived' = render 'projects/merge_requests/widget/open/archived'
- 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
%span.identifier .header-text-content
Milestone ##{@milestone.iid} %span.identifier
- if @milestone.expires_at Milestone ##{@milestone.iid}
%span.creator - if @milestone.expires_at
&middot; %span.creator
= @milestone.expires_at &middot;
.pull-right = @milestone.expires_at
.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.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="790 253 425 254" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="25" height="8.9423" x="25" y="88.4231" rx="2"/><mask id="h" width="25" height="8.9423" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M16 29.8013h43V91.404H16z"/><mask id="i" width="43" height="61.6026" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M57 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5747 24.863c.1564 1.0866-.253 1.2572-.912.384L66 86.436l-9-6.9552"/><mask id="j" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><path id="d" d="M.2496 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5748 24.863c.1562 1.0866-.2532 1.2572-.9123.384L9.2495 86.436l-9-6.9552"/><mask id="k" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><path id="e" d="M16 29.8013L35.786 1.4556c.9466-1.3562 2.4792-1.3594 3.428 0L59 29.8013"/><mask id="l" width="43" height="29.364" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><rect id="f" width="26.2653" height="35.5088" x="6.3673" rx="13.1327"/><mask id="m" width="26.2653" height="35.5088" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><rect id="g" width="16.8367" height="22.386" x="4.0816" rx="8.4184"/><mask id="n" width="16.8367" height="22.386" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(792.000000, 255.000000)"><g fill="#FDE5D8"><path d="M225.4372 59.5866c-.059.5897-.1323 1.2698-.2203 2.0305-.252 2.1764-.5717 4.559-.9653 7.07-.1283.8185.4312 1.586 1.2496 1.7143.8185.1283 1.586-.4312 1.7142-1.2497.4-2.5528.7253-4.975.9815-7.1898.0898-.7762.1646-1.4715.2252-2.0762.0366-.365.0604-.62.0722-.7557.0717-.8254-.539-1.5526-1.3645-1.6244-.8254-.0717-1.5526.539-1.6244 1.3645-.0106.1228-.0332.365-.0684.7166zM219.8738 87.9413c-.2563.7878.1745 1.6342.9622 1.8906.7878.2562 1.6342-.1745 1.8906-.9623.975-2.9962 1.849-6.2827 2.6287-9.797.1794-.8086-.3308-1.6097-1.1395-1.789-.8088-.1795-1.61.3306-1.7893 1.1394-.76 3.4256-1.6096 6.6206-2.5527 9.5183zM209.9266 103.166c-.781.2766-1.1897 1.134-.913 1.9148.2765.781 1.1338 1.1897 1.9147.913 2.9792-1.0552 5.5414-3.679 7.7796-7.6272.4084-.7207.1554-1.636-.5653-2.0447-.7207-.4086-1.636-.1556-2.0446.565-1.9152 3.3786-3.9945 5.508-6.1714 6.279zM190.439 107.5834c-.7636.3214-1.122 1.201-.8005 1.9645.3215.7634 1.201 1.1217 1.9645.8003 3.1204-1.314 6.2717-2.3243 9.258-2.9816.809-.178 1.3205-.9783 1.1424-1.7874-.178-.809-.9783-1.3205-1.7874-1.1424-3.1666.697-6.4914 1.763-9.777 3.1464zM173.231 118.6257c-.6005.5706-.6248 1.52-.0542 2.1206s1.52.625 2.1206.0543c2.282-2.1682 4.8656-4.162 7.6758-5.946.6994-.444.9064-1.371.4624-2.0704-.444-.6994-1.371-.9064-2.0704-.4624-2.9698 1.8854-5.707 3.998-8.1342 6.304zM162.4543 136.2492c-.2022.8034.2852 1.6185 1.0885 1.8207.8034.202 1.6186-.2853 1.8208-1.0886.7688-3.0547 2.0416-5.9768 3.781-8.7486.4403-.7018.2284-1.6276-.4733-2.068-.7017-.4402-1.6275-.2283-2.068.4734-1.9026 3.0322-3.3016 6.2438-4.149 9.611zM162.1894 156.693c.1036.822.854 1.4042 1.676 1.3006.8218-.1037 1.404-.854 1.3004-1.676-.367-2.9097-.5796-6.1364-.6444-9.8167-.0146-.8284-.698-1.488-1.5262-1.4734-.8283.0146-1.488.698-1.4733 1.5262.0665 3.783.286 7.1162.6674 10.1393zM168.408 176.1653c.3876.7322 1.2953 1.0117 2.0275.6242.7322-.3875 1.0117-1.2952.6242-2.0274-1.6733-3.162-2.9028-5.9954-3.8477-8.943-.2528-.789-1.0973-1.2235-1.8862-.9706-.789.2528-1.2234 1.0974-.9706 1.8863 1.0025 3.1275 2.3014 6.121 4.053 9.4306zM175.9738 188.9357c1.056 1.7165 1.8892 3.0806 2.7307 4.474.4283.709 1.3503.9368 2.0595.5085.709-.4283.9368-1.3503.5085-2.0595-.8464-1.4014-1.6836-2.772-2.7434-4.4948.0808.131-1.9545-3.1733-2.486-4.0405-.4328-.7063-1.3563-.928-2.0627-.495-.7063.4327-.928 1.3563-.495 2.0626.5334.8707 2.5708 4.1785 2.4885 4.0447zM184.83 211.3822c.011.8284.6912 1.491 1.5196 1.4803.8283-.0108 1.491-.691 1.4803-1.5194-.046-3.519-.6604-6.996-1.8367-10.3262-.276-.7812-1.1328-1.1908-1.914-.915-.781.276-1.1906 1.133-.9147 1.914 1.0668 3.0206 1.624 6.1733 1.6655 9.3664zM179.3467 229.4095c-.459.6896-.2723 1.6208.4173 2.08.6896.459 1.6208.272 2.08-.4175 1.966-2.9533 3.4756-6.124 4.4877-9.4165.2434-.7918-.2012-1.631-.993-1.8745-.792-.2434-1.6312.2012-1.8746.993-.9264 3.014-2.3108 5.922-4.1173 8.6355z"/></g><g transform="translate(336.866969, 147.225953) rotate(-300.000000) translate(-336.866969, -147.225953) translate(299.366969, 69.725953)"><path stroke="#FDE5D8" stroke-width="3" d="M19 154l10-52.6603m16 0L55 154" stroke-linecap="round"/><rect width="3" height="38.75" x="35" y="99.3526" fill="#FDE5D8" rx="1.5"/><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#h)" xlink:href="#a"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#i)" xlink:href="#b"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#j)" xlink:href="#c"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#k)" transform="translate(9.124810, 78.967887) scale(-1, 1) translate(-9.124810, -78.967887)" xlink:href="#d"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#l)" xlink:href="#e"/><ellipse cx="28.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="34.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="40.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="46.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="10.5" ry="10.4327"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="5.5" ry="5.4647"/></g><path fill="#EEE" d="M96.0426 37.2106c-.1512 1.6874.0814 3.815.997 6.146.2046.5207.7936.7774 1.3155.5733.522-.2043.7793-.792.5747-1.313-.7912-2.0142-.99-3.832-.865-5.226.0102-.1143.0195-.186.0238-.2113.092-.552-.2814-1.0738-.8344-1.1658-.553-.092-1.076.2808-1.168.8326-.0126.075-.0285.1975-.0434.364zM107.5302 52.8934c.4913.239 1.098.0626 1.355-.394.2572-.4566.0674-1.0205-.4238-1.2595-1.8668-.9083-3.4584-1.9152-4.7943-3.0075-.4162-.3404-1.0506-.3026-1.4168.0843-.3663.387-.3256.9766.0907 1.317 1.4583 1.1925 3.1828 2.2835 5.1893 3.2596zM120.661 58.9533c.5467.171 1.1257-.1425 1.2933-.7003.1675-.5577-.1397-1.1484-.6864-1.3194-3.0283-.9472-4.1984-1.3178-5.915-1.8824-.544-.179-1.1274.126-1.3028.6813-.1754.5552.1235 1.1504.6677 1.3294 1.729.5686 2.9053.941 5.943 1.8913zM132.5954 62.881c.449.246 1.022.0983 1.2798-.33.258-.4282.103-.975-.3458-1.221-1.4942-.819-3.1928-1.545-5.2675-2.2746-.486-.1708-1.025.0664-1.204.53-.179.4634.0697.9776.5555 1.1484 1.9832.6973 3.5892 1.3838 4.982 2.1472zM141.9774 73.383c.205.4938.809.742 1.3485.5543.5395-.1878.8106-.7404.6055-1.2344-.8504-2.0482-1.853-3.7962-3.0375-5.3046-.337-.429-.99-.527-1.4588-.2184-.4687.3085-.5755.9064-.2386 1.3354 1.0743 1.368 1.9926 2.9692 2.7808 4.8675zM144.609 87.025c.0183.5535.5682.99 1.2283.9746.66-.0153 1.1805-.4764 1.1622-1.03-.0725-2.2033-.2693-4.206-.622-6.1198-.1008-.5473-.7115-.9225-1.3642-.838-.6526.0846-1.1.597-.999 1.1442.336 1.8248.5248 3.745.5947 5.869z"/><path fill="#E5E5E5" d="M144.1423 95.7297c-.0863 2.5442-.1214 3.769-.1422 5.2548-.0076.5523.3963 1.007.9022 1.0154.506.0083.9223-.4326.93-.985.0205-1.4668.0554-2.6812.1412-5.2113l.026-.7667c.0185-.552-.3764-1.016-.882-1.0363-.5056-.0203-.9306.411-.949.963l-.026.766zM144.939 115.201c.1196.5447.6727.8925 1.2355.7768.5628-.1157.922-.651.8026-1.1957-.417-1.9-.7104-3.84-.8976-5.8637-.0513-.5545-.5574-.964-1.1305-.9142-.573.0497-.996.5396-.9448 1.0942.1944 2.1015.4998 4.121.9348 6.103zM149.995 127.5248c.296.454.9528.61 1.4668.3485.514-.2614.6907-.8413.3947-1.2952-1.0787-1.6535-2.0046-3.3145-2.7896-4.9916-.2266-.484-.8547-.7143-1.403-.5142-.548.2-.809.7546-.5823 1.2387.8208 1.7534 1.788 3.4886 2.9134 5.2138zM154.8088 135.226c1.0587 1.232 2.242 2.4097 3.543 3.531.404.3482 1.0276.3186 1.393-.066.3657-.3843.3346-.978-.0692-1.3262-1.2296-1.0597-2.345-2.17-3.3402-3.328-.195-.227-.3872-.4542-.5764-.6813-.3385-.4063-.9588-.4744-1.3856-.1522-.4267.3223-.4983.913-.1598 1.3192.1954.2346.3938.469.5952.7034zM170.634 146.9026c.4806.242 1.0517.0176 1.2758-.501.224-.5188.0162-1.1354-.4642-1.3773-1.7563-.8842-3.422-1.8432-4.9857-2.8726-.4527-.298-1.0434-.1435-1.3195.3452-.276.4885-.133 1.126.3198 1.424 1.6256 1.0704 3.354 2.0655 5.1738 2.9816z"/><path fill="#EEE" d="M184.7334 151.9698c.5527.1412 1.1072-.2262 1.2385-.8206.1312-.5944-.2104-1.1908-.763-1.332-2.001-.5114-3.9602-1.1002-5.8632-1.763-.5405-.1883-1.1205.1303-1.2955.7115-.175.5813.1212 1.205.6616 1.3934 1.9557.6813 3.9676 1.286 6.0214 1.8108zM197.9337 153.9977c.5532.04 1.0297-.445 1.0643-1.083.0346-.6383-.3857-1.188-.939-1.228-1.973-.1424-3.952-.3682-5.9206-.676-.5492-.086-1.0547.358-1.1292.9917-.0744.6336.3105 1.2168.8597 1.3027 2.0164.3154 4.0433.5467 6.0647.6927zM212.1213 152.6062c.5493-.055.9392-.4576.871-.8994-.0684-.442-.569-.7555-1.1184-.7006-1.9168.1917-3.893.3194-5.9104.382-.553.0173-.9842.392-.9628.8368.0213.445.487.7916 1.0402.7744 2.0737-.0645 4.1064-.1957 6.0803-.3932zM226.3665 149.949c.5293-.22.7755-.8162.5497-1.332-.2257-.5155-.838-.7553-1.3672-.5354-1.7815.74-3.7143 1.3827-5.7772 1.923-.5558.1454-.8852.7023-.7358 1.2436.1494.5414.721.8623 1.2768.7168 2.1547-.5643 4.1797-1.2376 6.0537-2.016zM237.8486 140.4168c.292-.4344.1488-1.006-.3202-1.2766-.469-.2706-1.086-.1378-1.3782.2967-.9575 1.4237-2.225 2.7337-3.7847 3.9202-.427.3248-.4888.9087-.138 1.3042.3505.3955.981.4528 1.408.128 1.723-1.3107 3.1363-2.7714 4.213-4.3726zM245.6725 130.6874c.3987-.3503.439-.9587.09-1.3588-.3492-.4-.9554-.4405-1.3542-.0902-1.5048 1.3222-2.8978 2.7094-4.1698 4.1635-.3497.3995-.3102 1.008.088 1.3587.3983.3508 1.0046.3113 1.3542-.0884 1.2153-1.389 2.5487-2.717 3.9918-3.985zM257.4814 122.8697c.476-.2568.657-.8577.4047-1.342-.2523-.4843-.8428-.6687-1.3188-.4118-1.7682.9542-3.4795 1.973-5.1228 3.0587-.4518.2985-.5803.9133-.287 1.373.2934.46.8975.5906 1.3494.292 1.5938-1.0528 3.2557-2.0423 4.9746-2.97zM270.276 116.9216c.5503-.1682.8513-.724.6723-1.241-.179-.5173-.77-.8003-1.3204-.632-1.9296.5898-3.932 1.2728-5.975 2.054-.536.205-.7936.7797-.5754 1.2835.218.504.8294.746 1.3654.541 1.9947-.7628 3.95-1.4298 5.833-2.0054z"/><circle cx="145" cy="90" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><circle cx="238" cy="138" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><path stroke="#B5A7DD" stroke-width="3" d="M20.0605 56s-17.4698 33-12 53c5.4697 20 17 32 38 44S78.5 148 107 159s29 43 29 43" stroke-linecap="round" stroke-dasharray="8 10"/><g stroke="#EEE" stroke-width="3" transform="translate(108.000000, 173.000000)"><path fill="#FFF" d="M154 77c0-42.526-34.474-77-77-77S0 34.474 0 77" stroke-linecap="round"/><circle cx="108" cy="41" r="16"/><circle cx="42.5" cy="30.5" r="8.5"/><circle cx="22" cy="58" r="5"/></g><g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(19.897959, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(12.602041, 6.000000) scale(-1, 1) translate(-12.602041, -6.000000) translate(6.102041, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(0.000000, 10.491228)"><g fill="#FC8A51" transform="translate(29.448980, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><g fill="#FC8A51" transform="translate(5.051020, 21.298246) scale(-1, 1) translate(-5.051020, -21.298246) translate(0.551020, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><use stroke="#FC8A51" stroke-width="6" mask="url(#m)" xlink:href="#f"/><path fill="#FC8A51" d="M7.1633 12.9123H31.041v3H7.1632z"/></g></g><g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(12.755102, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(7.744898, 4.000000) scale(-1, 1) translate(-7.744898, -4.000000) translate(3.244898, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(0.000000, 6.614035)"><g fill="#EEE" transform="translate(18.877551, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><g fill="#EEE" transform="translate(3.122449, 13.622807) scale(-1, 1) translate(-3.122449, -13.622807) translate(0.122449, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><use stroke="#EEE" stroke-width="4" mask="url(#n)" xlink:href="#g"/><path fill="#EEE" d="M4.5918 8.1404h15.306v2H4.592z"/></g></g><g fill="#FFF" transform="translate(0.000000, 103.000000)"><circle cx="8.5" cy="8.5" r="8.5" stroke="#B5A7DD" stroke-width="4"/><circle cx="171.5" cy="20.5" r="6.5"/></g><g><g transform="translate(39.000000, 142.000000)"><ellipse cx="12.5" cy="12.5" fill="#FFF" stroke="#6B4FBB" stroke-width="4" rx="12.5" ry="12.5"/><path fill="#FC8A51" d="M10.7322 13.475l-1.7665-1.7667c-.5873-.5873-1.5368-.587-2.1226-.0012-.5897.59-.585 1.5362.0013 2.1226l2.826 2.826.0007.0007.0006.0006c.5898.5897 1.534.587 2.118.003l6.3704-6.3703c.577-.577.5826-1.5323-.003-2.118-.59-.59-1.5343-.5873-2.1183-.0033l-5.3065 5.3065z"/></g></g><circle cx="171.5" cy="122.5" r="6.5" fill="#FFF" stroke="#FC8A51" stroke-width="3"/><circle cx="22" cy="52" r="6" fill="#FFF" stroke="#B5A7DD" stroke-width="3"/><path fill="#FFF" stroke="#B5A7DD" stroke-width="3.6" d="M188.151 141.596c8.7045-7.7456 11.0126-20.9255 4.8625-31.5777-7.0208-12.1604-22.4055-16.422-34.363-9.5183-11.9572 6.9036-15.959 22.358-8.9382 34.5183 6.2353 10.8 19.068 15.3695 30.2375 11.4206l10.8992 18.8778c1.3167 2.2807 4.2302 3.063 6.5078 1.748 2.273-1.3122 3.0567-4.2295 1.74-6.51l-10.9458-18.9587zm-8.4343-4.6086c7.8576-4.5366 10.4874-14.6923 5.8738-22.6834-4.6137-7.991-14.7237-10.7915-22.5814-6.255-7.8575 4.5368-10.4873 14.6925-5.8737 22.6836 4.6137 7.991 14.7237 10.7915 22.5814 6.2548z"/></g></svg>
<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>
<svg width="366px" height="229px" viewBox="784 258 366 229" 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="35" y="39" width="24" height="21" rx="10"></rect>
<mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="24" height="21" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<rect id="path-3" x="64.8662386" y="58.3882666" width="10" height="71" rx="5"></rect>
<mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="71" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<rect id="path-5" x="18.1550472" y="58.3882666" width="10" height="71" rx="5"></rect>
<mask id="mask-6" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="71" fill="white">
<use xlink:href="#path-5"></use>
</mask>
<rect id="path-7" x="24" y="56" width="46" height="10" rx="5"></rect>
<mask id="mask-8" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="46" height="10" fill="white">
<use xlink:href="#path-7"></use>
</mask>
<rect id="path-9" x="42" y="60" width="10" height="68" rx="5"></rect>
<mask id="mask-10" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="68" fill="white">
<use xlink:href="#path-9"></use>
</mask>
<rect id="path-11" x="69" y="12" width="12" height="12" rx="3"></rect>
<mask id="mask-12" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="12" height="12" fill="white">
<use xlink:href="#path-11"></use>
</mask>
<rect id="path-13" x="40" y="18" width="14" height="22" rx="6"></rect>
<mask id="mask-14" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="14" height="22" fill="white">
<use xlink:href="#path-13"></use>
</mask>
<rect id="path-15" x="41" y="8" width="34" height="20" rx="3"></rect>
<mask id="mask-16" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="34" height="20" fill="white">
<use xlink:href="#path-15"></use>
</mask>
<path d="M8,8.00793008 C8,6.34669617 9.34984627,5.0321392 11.0036812,5.07151622 L46.9963188,5.92848378 C48.6552061,5.9679811 50,7.34177063 50,8.99109042 L50,27.0089096 C50,28.6608432 48.6501537,30.0321392 46.9963188,30.0715162 L11.0036812,30.9284838 C9.34479389,30.9679811 8,29.6568766 8,27.9920699 L8,8.00793008 Z" id="path-17"></path>
<mask id="mask-18" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="42" height="25.858699" fill="white">
<use xlink:href="#path-17"></use>
</mask>
<rect id="path-19" x="-7.10542736e-15" y="1.77635684e-14" width="16" height="36" rx="3"></rect>
<mask id="mask-20" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="16" height="36" fill="white">
<use xlink:href="#path-19"></use>
</mask>
</defs>
<g id="Group-7" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(786.000000, 259.000000)">
<g id="Group-5" transform="translate(132.727922, 71.000000)">
<use id="Rectangle-21" stroke="#EEEEEE" mask="url(#mask-2)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-1"></use>
<use id="Rectangle-16-Copy" stroke="#EEEEEE" mask="url(#mask-4)" stroke-width="8" fill="#FFFFFF" transform="translate(69.866239, 93.888267) rotate(-20.000000) translate(-69.866239, -93.888267) " xlink:href="#path-3"></use>
<use id="Rectangle-16-Copy-2" stroke="#EEEEEE" mask="url(#mask-6)" stroke-width="8" fill="#FFFFFF" transform="translate(23.155047, 93.888267) scale(-1, 1) rotate(-20.000000) translate(-23.155047, -93.888267) " xlink:href="#path-5"></use>
<use id="Rectangle-15" stroke="#EEEEEE" mask="url(#mask-8)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-7"></use>
<use id="Rectangle-16" stroke="#EEEEEE" mask="url(#mask-10)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-9"></use>
<g id="Group" transform="translate(45.500000, 33.000000) rotate(20.000000) translate(-45.500000, -33.000000) translate(5.000000, 13.000000)">
<use id="Rectangle-4" stroke="#EEEEEE" mask="url(#mask-12)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-11"></use>
<use id="Rectangle-20" stroke="#EEEEEE" mask="url(#mask-14)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-13"></use>
<use id="Rectangle-2" stroke="#EEEEEE" mask="url(#mask-16)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-15"></use>
<use id="Rectangle" stroke="#EEEEEE" mask="url(#mask-18)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-17"></use>
<rect id="Rectangle-17" fill="#EEEEEE" x="21" y="7" width="3" height="22"></rect>
<rect id="Rectangle-17-Copy" fill="#EEEEEE" x="64" y="8" width="3" height="17"></rect>
<circle id="Oval-9" fill="#B5A7DD" cx="40" cy="18" r="2"></circle>
<circle id="Oval-9-Copy-4" fill="#EEEEEE" cx="47" cy="33" r="2"></circle>
<use id="Rectangle-19" stroke="#EEEEEE" mask="url(#mask-20)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-19"></use>
</g>
</g>
<path d="M265.128496,225.286991 C247.289192,194.617726 214.068171,174 176.031622,174 C137.847583,174 104.51649,194.77793 86.7279221,225.644211" id="Oval-10" stroke="#EEEEEE" stroke-width="4" stroke-linecap="round" fill="#FFFFFF"></path>
<circle id="Oval-11" stroke="#FDE5D8" stroke-width="4" fill="#FFFFFF" cx="24.5" cy="25.5" r="24.5"></circle>
<path d="M24,1.00292933 C24,0.449026756 24.4438648,0 25,0 C25.5522847,0 26,0.437881351 26,1.00292933 L26,5.99707067 C26,6.55097324 25.5561352,7 25,7 C24.4477153,7 24,6.56211865 24,5.99707067 L24,1.00292933 Z M48.46461,17.3244238 C48.9914026,17.1532585 49.5556142,17.4366422 49.7274694,17.9655581 C49.8981348,18.4908122 49.6200365,19.0519274 49.0826439,19.2265369 L44.3329333,20.7698114 C43.8061406,20.9409767 43.241929,20.6575931 43.0700738,20.1286771 C42.8994084,19.6034231 43.1775067,19.0423078 43.7148993,18.8676984 L48.46461,17.3244238 Z M40.5019265,45.6352697 C40.8275022,46.0833863 40.7323394,46.7075538 40.2824166,47.0344419 C39.8356088,47.3590667 39.2160194,47.2679737 38.8838925,46.8108402 L35.9484099,42.770495 C35.6228341,42.3223784 35.717997,41.6982109 36.1679198,41.3713229 C36.6147275,41.0466981 37.234317,41.1377911 37.5664439,41.5949245 L40.5019265,45.6352697 Z M11.1161075,46.8108402 C10.7905317,47.2589568 10.1675063,47.3613299 9.71758344,47.0344419 C9.27077569,46.709817 9.16594665,46.0924031 9.49807352,45.6352697 L12.4335561,41.5949245 C12.7591319,41.1468079 13.3821574,41.0444348 13.8320802,41.3713229 C14.278888,41.6959477 14.383717,42.3133616 14.0515901,42.770495 L11.1161075,46.8108402 Z M0.917356057,19.2265369 C0.390563404,19.0553716 0.100675355,18.4944741 0.272530576,17.9655581 C0.44319595,17.4403041 0.997997482,17.1498144 1.53539005,17.3244238 L6.28510071,18.8676984 C6.81189336,19.0388637 7.10178141,19.5997611 6.92992619,20.1286771 C6.75926082,20.6539311 6.20445928,20.9444208 5.66706672,20.7698114 L0.917356057,19.2265369 Z" id="Rectangle-23" fill="#FDE5D8"></path>
<rect id="Rectangle-18" fill="#FC6D26" x="24" y="14" width="3" height="12" rx="1.5"></rect>
<rect id="Rectangle-22" fill="#FC6D26" x="24" y="24" width="12" height="3" rx="1.5"></rect>
<circle id="Oval-11" fill="#6B4FBB" cx="25.5" cy="25.5" r="2.5"></circle>
<path d="M358.949747,6.87474747 L357.453009,7.20729654 C356.9128,7.32732164 356.570654,6.9935311 356.692198,6.44648557 L357.024747,4.94974747 L356.692198,3.45300937 C356.572173,2.91279997 356.905964,2.57065443 357.453009,2.69219839 L358.949747,3.02474747 L360.446486,2.69219839 C360.986695,2.5721733 361.328841,2.90596384 361.207297,3.45300937 L360.874747,4.94974747 L361.207297,6.44648557 C361.327322,6.98669496 360.993531,7.32884051 360.446486,7.20729654 L358.949747,6.87474747 Z" id="Star-Copy-5" fill="#6B4FBB" transform="translate(358.949747, 4.949747) rotate(-315.000000) translate(-358.949747, -4.949747) "></path>
<path d="M113.949747,32.8747475 L112.453009,33.2072965 C111.9128,33.3273216 111.570654,32.9935311 111.692198,32.4464856 L112.024747,30.9497475 L111.692198,29.4530094 C111.572173,28.9128 111.905964,28.5706544 112.453009,28.6921984 L113.949747,29.0247475 L115.446486,28.6921984 C115.986695,28.5721733 116.328841,28.9059638 116.207297,29.4530094 L115.874747,30.9497475 L116.207297,32.4464856 C116.327322,32.986695 115.993531,33.3288405 115.446486,33.2072965 L113.949747,32.8747475 Z" id="Star-Copy-7" fill="#B5A7DD" transform="translate(113.949747, 30.949747) rotate(-315.000000) translate(-113.949747, -30.949747) "></path>
<path d="M329.949747,211.874747 L328.453009,212.207297 C327.9128,212.327322 327.570654,211.993531 327.692198,211.446486 L328.024747,209.949747 L327.692198,208.453009 C327.572173,207.9128 327.905964,207.570654 328.453009,207.692198 L329.949747,208.024747 L331.446486,207.692198 C331.986695,207.572173 332.328841,207.905964 332.207297,208.453009 L331.874747,209.949747 L332.207297,211.446486 C332.327322,211.986695 331.993531,212.328841 331.446486,212.207297 L329.949747,211.874747 Z" id="Star-Copy-6" fill="#B5A7DD" opacity="0.5" transform="translate(329.949747, 209.949747) rotate(-315.000000) translate(-329.949747, -209.949747) "></path>
<path d="M265.363961,54.838961 L263.153969,55.3299826 C262.617155,55.4492534 262.280283,55.1035008 262.397939,54.5739526 L262.888961,52.363961 L262.397939,50.1539694 C262.278669,49.6171548 262.624421,49.2802831 263.153969,49.3979395 L265.363961,49.888961 L267.573953,49.3979395 C268.110767,49.2786686 268.447639,49.6244213 268.329983,50.1539694 L267.838961,52.363961 L268.329983,54.5739526 C268.449253,55.1107673 268.103501,55.4476389 267.573953,55.3299826 L265.363961,54.838961 Z" id="Star-Copy-9" fill="#FC6D26" transform="translate(265.363961, 52.363961) rotate(-315.000000) translate(-265.363961, -52.363961) "></path>
<path d="M56.363961,142.838961 L54.1539694,143.329983 C53.6171548,143.449253 53.2802831,143.103501 53.3979395,142.573953 L53.888961,140.363961 L53.3979395,138.153969 C53.2786686,137.617155 53.6244213,137.280283 54.1539694,137.397939 L56.363961,137.888961 L58.5739526,137.397939 C59.1107673,137.278669 59.4476389,137.624421 59.3299826,138.153969 L58.838961,140.363961 L59.3299826,142.573953 C59.4492534,143.110767 59.1035008,143.447639 58.5739526,143.329983 L56.363961,142.838961 Z" id="Star-Copy-8" fill="#6B4FBB" transform="translate(56.363961, 140.363961) rotate(-315.000000) translate(-56.363961, -140.363961) "></path>
<g id="Group-6" transform="translate(311.872633, 125.094458) rotate(-345.000000) translate(-311.872633, -125.094458) translate(290.872633, 115.094458)">
<circle id="Oval-12" stroke="#FDE5D8" stroke-width="4" fill="#FFFFFF" cx="21" cy="10" r="10"></circle>
<ellipse id="Oval-13" fill="#FDE5D8" cx="21" cy="10" rx="21" ry="2"></ellipse>
</g>
</g>
</svg>
<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,32 +3,38 @@ ...@@ -3,32 +3,38 @@
.context.prepend-top-default .context.prepend-top-default
.milestone-summary .milestone-summary
%h4 Progress %h4 Progress
%strong= milestone.issues_visible_to_user(current_user).size
issues:
%span.milestone-stat
%strong= milestone.issues_visible_to_user(current_user).opened.size
open and
%strong= milestone.issues_visible_to_user(current_user).closed.size
closed
%strong= milestone.merge_requests.size
merge requests:
%span.milestone-stat
%strong= milestone.merge_requests.opened.size
open and
%strong= milestone.merge_requests.merged.size
merged
%span.milestone-stat
%strong== #{milestone.percent_complete(current_user)}%
complete
%span.milestone-stat .milestone-stats-and-buttons
%span.remaining-days= milestone_remaining_days(milestone) .milestone-stats
%span.pull-right.tab-issues-buttons %span.milestone-stat.with-drilldown
- if project && can?(current_user, :create_issue, project) %strong= milestone.issues_visible_to_user(current_user).size
= link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do issues:
New Issue %span.milestone-stat
= link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped" %strong= milestone.issues_visible_to_user(current_user).opened.size
%span.pull-right.tab-merge-requests-buttons.hidden open and
= link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped" %strong= milestone.issues_visible_to_user(current_user).closed.size
closed
%span.milestone-stat.with-drilldown
%strong= milestone.merge_requests.size
merge requests:
%span.milestone-stat
%strong= milestone.merge_requests.opened.size
open and
%strong= milestone.merge_requests.merged.size
merged
%span.milestone-stat
%strong== #{milestone.percent_complete(current_user)}%
complete
%span.milestone-stat
%span.remaining-days= milestone_remaining_days(milestone)
= milestone_progress_bar(milestone) .milestone-progress-buttons
%span.tab-issues-buttons
- 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", title: "New Issue" do
New Issue
= link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
%span.tab-merge-requests-buttons.hidden
= link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn"
= milestone_progress_bar(milestone)
...@@ -2,7 +2,9 @@ class NewNoteWorker ...@@ -2,7 +2,9 @@ class NewNoteWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
def perform(note_id) # Keep extra parameter to preserve backwards compatibility with
# old `NewNoteWorker` jobs (can remove later)
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id) if note = Note.find_by(id: note_id)
NotificationService.new.new_note(note) NotificationService.new.new_note(note)
Notes::PostProcessService.new(note).execute Notes::PostProcessService.new(note).execute
......
# 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
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 = [])
project = Project.find_by(id: project_id)
# Overwrite Sidekiq's implementation so we only schedule when actually needed. return unless project && project.repository.exists?
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) update_repository_size(project)
if try_obtain_lease_for(project_id) project.update_commit_count
Rails.logger.
info("Obtained ProjectCacheWorker lease for project #{project_id}")
else
Rails.logger.
info("Could not obtain ProjectCacheWorker lease for project #{project_id}")
return
end
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: Add deployment command to ChatOps
merge_request: 7619
author:
---
title: Fix 500 error when group name ends with git
merge_request: 7630
author:
---
title: Send credentials (currently for registry only) with build data to GitLab Runner
merge_request: 7474
author:
---
title: Fix undefined error in CI linter
merge_request: 7650
author:
---
title: Added permissions per stage to cycle analytics endpoint
merge_request:
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: 'Remove unnecessary require_relative calls from service classes'
merge_request: '7601'
author: Semyon Pupkov
---
title: Rework cache invalidation so only changed data is refreshed
merge_request: 7360
author:
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
- [Rake tasks](rake_tasks.md) for development - [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase - [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md) - [Sidekiq debugging](sidekiq_debugging.md)
- [Object state models](object_state_models.md)
## Databases ## Databases
......
# Object state models
## Diagrams
[GitLab object state models](https://drive.google.com/drive/u/3/folders/0B5tDlHAM4iZINmpvYlJXcDVqMGc)
---
## Legend
![legend](img/state-model-legend.png)
---
## Issue
[`app/models/issue.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/models/issue.rb)
![issue](img/state-model-issue.png)
---
## Merge request
[`app/models/merge_request.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/models/merge_request.rb)
![merge request](img/state-model-merge-request.png)
\ No newline at end of file
# Mattermost slash commands
> Introduced in GitLab 8.14
Mattermost commands give users an extra interface to perform common operations
from the chat environment. This allows one to, for example, create an issue as
soon as the idea was discussed in Mattermost.
## Prerequisites
Mattermost 3.4 and up is required.
If you have the Omnibus GitLab package installed, Mattermost is already bundled
in it. All you have to do is configure it. Read more in the
[Omnibus GitLab Mattermost documentation][omnimmdocs].
## Configuration
The configuration consists of two parts. First you need to enable the slash
commands in Mattermost and then enable the service in GitLab.
### Step 1. Enable custom slash commands in Mattermost
The first thing to do in Mattermost is to enable custom slash commands from
the administrator console.
1. Log in with an account that has admin privileges and navigate to the system
console.
![Mattermost go to console](img/mattermost_goto_console.png)
---
1. Click **Custom integrations** and set **Enable Custom Slash Commands** to
true.
![Mattermost console](img/mattermost_console_integrations.png)
---
1. Click **Save** at the bottom to save the changes.
### Step 2. Open the Mattermost slash commands service in GitLab
1. Open a new tab for GitLab and go to your project's settings
**Services ➔ Mattermost command**. A screen will appear with all the values you
need to copy in Mattermost as described in the next step. Leave the window open.
>**Note:**
GitLab will propose some values for the Mattermost settings. The only one
required to copy-paste as-is is the **Request URL**, all the others are just
suggestions.
![Mattermost setup instructions](img/mattermost_config_help.png)
---
1. Proceed to the next step and create a slash command in Mattermost with the
above values.
### Step 3. Create a new custom slash command in Mattermost
Now that you have enabled the custom slash commands in Mattermost and opened
the Mattermost slash commands service in GitLab, it's time to copy these values
in a new slash command.
1. Back to Mattermost, under your team page settings, you should see the
**Integrations** option.
![Mattermost team integrations](img/mattermost_team_integrations.png)
---
1. Go to the **Slash Commands** integration and add a new one by clicking the
**Add Slash Command** button.
![Mattermost add command](img/mattermost_add_slash_command.png)
---
1. Fill in the options for the custom command as described in
[step 2](#step-2-open-the-mattermost-slash-commands-service-in-gitlab).
>**Note:**
If you plan on connecting multiple projects, pick a slash command trigger
word that relates to your projects such as `/gitlab-project-name` or even
just `/project-name`. Only use `/gitlab` if you will only connect a single
project to your Mattermost team.
![Mattermost add command configuration](img/mattermost_slash_command_configuration.png)
1. After you setup all the values, copy the token (we will use it below) and
click **Done**.
![Mattermost slash command token](img/mattermost_slash_command_token.png)
### Step 4. Copy the Mattermost token into the Mattermost slash command service
1. In GitLab, paste the Mattermost token you copied in the previous step and
check the **Active** checkbox.
![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png)
1. Click **Save changes** for the changes to take effect.
---
You are now set to start using slash commands in Mattermost that talk to the
GitLab project you configured.
## Authorizing Mattermost to interact with GitLab
The first time a user will interact with the newly created slash commands,
Mattermost will trigger an authorization process.
![Mattermost bot authorize](img/mattermost_bot_auth.png)
This will connect your Mattermost user with your GitLab user. You can
see all authorized chat accounts in your profile's page under **Chat**.
When the authorization process is complete, you can start interacting with
GitLab using the Mattermost commands.
## Available slash commands
The available slash commands so far are:
| Command | Description | Example |
| ------- | ----------- | ------- |
| `/<trigger> issue create <title>\n<description>` | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | `/trigger issue create We need to change the homepage` |
| `/<trigger> issue show <issue-number>` | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | `/trigger issue show 42` |
| `/<trigger> deploy <environment> to <environment>` | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | `/trigger deploy staging to production` |
To see a list of available commands that can interact with GitLab, type the
trigger word followed by `help`:
```
/my-project help
```
![Mattermost bot available commands](img/mattermost_bot_available_commands.png)
## Permissions
The permissions to run the [available commands](#available-commands) derive from
the [permissions you have on the project](../user/permissions.md#project).
## Further reading
- [Mattermost slash commands documentation][mmslashdocs]
- [Omnibus GitLab Mattermost][omnimmdocs]
[omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/
[mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html
[ciyaml]: ../ci/yaml/README.md
...@@ -42,6 +42,7 @@ further configuration instructions and details. Contributions are welcome. ...@@ -42,6 +42,7 @@ further configuration instructions and details. Contributions are welcome.
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
| [JIRA](jira.md) | JIRA issue tracker | | [JIRA](jira.md) | JIRA issue tracker |
| JetBrains TeamCity CI | A continuous integration and build server | | JetBrains TeamCity CI | A continuous integration and build server |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| PivotalTracker | Project Management Software (Source Commits Endpoint) | | PivotalTracker | Project Management Software (Source Commits Endpoint) |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
| [Redmine](redmine.md) | Redmine issue tracker | | [Redmine](redmine.md) | Redmine issue tracker |
......
...@@ -62,7 +62,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -62,7 +62,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end end
step 'I click link "New Issue"' do step 'I click link "New Issue"' do
click_link "New Issue" page.has_link?('New Issue') ? click_link('New Issue') : click_link('New issue')
end end
step 'I click "author" dropdown' do step 'I click "author" dropdown' do
......
...@@ -3,6 +3,9 @@ module API ...@@ -3,6 +3,9 @@ module API
class ProjectSnippets < Grape::API class ProjectSnippets < Grape::API
before { authenticate! } before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do resource :projects do
helpers do helpers do
def handle_project_member_errors(errors) def handle_project_member_errors(errors)
...@@ -18,111 +21,108 @@ module API ...@@ -18,111 +21,108 @@ module API
end end
end end
# Get a project snippets desc 'Get all project snippets' do
# success Entities::ProjectSnippet
# Parameters: end
# id (required) - The ID of a project
# Example Request:
# GET /projects/:id/snippets
get ":id/snippets" do get ":id/snippets" do
present paginate(snippets_for_current_user), with: Entities::ProjectSnippet present paginate(snippets_for_current_user), with: Entities::ProjectSnippet
end end
# Get a project snippet desc 'Get a single project snippet' do
# success Entities::ProjectSnippet
# Parameters: end
# id (required) - The ID of a project params do
# snippet_id (required) - The ID of a project snippet requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
# Example Request: end
# GET /projects/:id/snippets/:snippet_id
get ":id/snippets/:snippet_id" do get ":id/snippets/:snippet_id" do
@snippet = snippets_for_current_user.find(params[:snippet_id]) snippet = snippets_for_current_user.find(params[:snippet_id])
present @snippet, with: Entities::ProjectSnippet present snippet, with: Entities::ProjectSnippet
end end
# Create a new project snippet desc 'Create a new project snippet' do
# success Entities::ProjectSnippet
# Parameters: end
# id (required) - The ID of a project params do
# title (required) - The title of a snippet requires :title, type: String, desc: 'The title of the snippet'
# file_name (required) - The name of a snippet file requires :file_name, type: String, desc: 'The file name of the snippet'
# code (required) - The content of a snippet requires :code, type: String, desc: 'The content of the snippet'
# visibility_level (required) - The snippet's visibility requires :visibility_level, type: Integer,
# Example Request: values: [Gitlab::VisibilityLevel::PRIVATE,
# POST /projects/:id/snippets Gitlab::VisibilityLevel::INTERNAL,
Gitlab::VisibilityLevel::PUBLIC],
desc: 'The visibility level of the snippet'
end
post ":id/snippets" do post ":id/snippets" do
authorize! :create_project_snippet, user_project authorize! :create_project_snippet, user_project
required_attributes! [:title, :file_name, :code, :visibility_level] snippet_params = declared_params
snippet_params[:content] = snippet_params.delete(:code)
attrs = attributes_for_keys [:title, :file_name, :visibility_level] snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
attrs[:content] = params[:code] if params[:code].present?
@snippet = CreateSnippetService.new(user_project, current_user,
attrs).execute
if @snippet.errors.any? if snippet.persisted?
render_validation_error!(@snippet) present snippet, with: Entities::ProjectSnippet
else else
present @snippet, with: Entities::ProjectSnippet render_validation_error!(snippet)
end end
end end
# Update an existing project snippet desc 'Update an existing project snippet' do
# success Entities::ProjectSnippet
# Parameters: end
# id (required) - The ID of a project params do
# snippet_id (required) - The ID of a project snippet requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
# title (optional) - The title of a snippet optional :title, type: String, desc: 'The title of the snippet'
# file_name (optional) - The name of a snippet file optional :file_name, type: String, desc: 'The file name of the snippet'
# code (optional) - The content of a snippet optional :code, type: String, desc: 'The content of the snippet'
# visibility_level (optional) - The snippet's visibility optional :visibility_level, type: Integer,
# Example Request: values: [Gitlab::VisibilityLevel::PRIVATE,
# PUT /projects/:id/snippets/:snippet_id Gitlab::VisibilityLevel::INTERNAL,
Gitlab::VisibilityLevel::PUBLIC],
desc: 'The visibility level of the snippet'
at_least_one_of :title, :file_name, :code, :visibility_level
end
put ":id/snippets/:snippet_id" do put ":id/snippets/:snippet_id" do
@snippet = snippets_for_current_user.find(params[:snippet_id]) snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id))
authorize! :update_project_snippet, @snippet not_found!('Snippet') unless snippet
authorize! :update_project_snippet, snippet
snippet_params = declared_params(include_missing: false)
snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present?
attrs = attributes_for_keys [:title, :file_name, :visibility_level] UpdateSnippetService.new(user_project, current_user, snippet,
attrs[:content] = params[:code] if params[:code].present? snippet_params).execute
UpdateSnippetService.new(user_project, current_user, @snippet, if snippet.persisted?
attrs).execute present snippet, with: Entities::ProjectSnippet
if @snippet.errors.any?
render_validation_error!(@snippet)
else else
present @snippet, with: Entities::ProjectSnippet render_validation_error!(snippet)
end end
end end
# Delete a project snippet desc 'Delete a project snippet'
# params do
# Parameters: requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
# id (required) - The ID of a project end
# snippet_id (required) - The ID of a project snippet
# Example Request:
# DELETE /projects/:id/snippets/:snippet_id
delete ":id/snippets/:snippet_id" do delete ":id/snippets/:snippet_id" do
begin snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
@snippet = snippets_for_current_user.find(params[:snippet_id]) not_found!('Snippet') unless snippet
authorize! :update_project_snippet, @snippet
@snippet.destroy authorize! :admin_project_snippet, snippet
rescue snippet.destroy
not_found!('Snippet')
end
end end
# Get a raw project snippet desc 'Get a raw project snippet'
# params do
# Parameters: requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
# id (required) - The ID of a project end
# snippet_id (required) - The ID of a project snippet
# Example Request:
# GET /projects/:id/snippets/:snippet_id/raw
get ":id/snippets/:snippet_id/raw" do get ":id/snippets/:snippet_id/raw" do
@snippet = snippets_for_current_user.find(params[:snippet_id]) snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
not_found!('Snippet') unless snippet
env['api.format'] = :txt env['api.format'] = :txt
content_type 'text/plain' content_type 'text/plain'
present @snippet.content present snippet.content
end end
end end
end end
......
...@@ -140,7 +140,8 @@ module API ...@@ -140,7 +140,8 @@ module API
User.where(username: params[:username]). User.where(username: params[:username]).
where.not(id: user.id).count > 0 where.not(id: user.id).count > 0
identity_attrs = params.slice(:provider, :extern_uid) user_params = declared_params(include_missing: false)
identity_attrs = user_params.slice(:provider, :extern_uid)
if identity_attrs.any? if identity_attrs.any?
identity = user.identities.find_by(provider: identity_attrs[:provider]) identity = user.identities.find_by(provider: identity_attrs[:provider])
...@@ -154,10 +155,10 @@ module API ...@@ -154,10 +155,10 @@ module API
end end
# Delete already handled parameters # Delete already handled parameters
params.delete(:extern_uid) user_params.delete(:extern_uid)
params.delete(:provider) user_params.delete(:provider)
if user.update_attributes(declared_params(include_missing: false)) if user.update_attributes(user_params)
present user, with: Entities::UserFull present user, with: Entities::UserFull
else else
render_validation_error!(user) render_validation_error!(user)
......
...@@ -32,6 +32,10 @@ module Ci ...@@ -32,6 +32,10 @@ module Ci
expose :artifacts_file, using: ArtifactFile, if: ->(build, _) { build.artifacts? } expose :artifacts_file, using: ArtifactFile, if: ->(build, _) { build.artifacts? }
end end
class BuildCredentials < Grape::Entity
expose :type, :url, :username, :password
end
class BuildDetails < Build class BuildDetails < Build
expose :commands expose :commands
expose :repo_url expose :repo_url
...@@ -50,6 +54,8 @@ module Ci ...@@ -50,6 +54,8 @@ module Ci
expose :variables expose :variables
expose :depends_on_builds, using: Build expose :depends_on_builds, using: Build
expose :credentials, using: BuildCredentials
end end
class Runner < Grape::Entity class Runner < Grape::Entity
......
...@@ -4,6 +4,7 @@ module Gitlab ...@@ -4,6 +4,7 @@ module Gitlab
COMMANDS = [ COMMANDS = [
Gitlab::ChatCommands::IssueShow, Gitlab::ChatCommands::IssueShow,
Gitlab::ChatCommands::IssueCreate, Gitlab::ChatCommands::IssueCreate,
Gitlab::ChatCommands::Deploy,
].freeze ].freeze
def execute def execute
......
module Gitlab
module ChatCommands
class Deploy < BaseCommand
include Gitlab::Routing.url_helpers
def self.match(text)
/\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text)
end
def self.help_message
'deploy <environment> to <target-environment>'
end
def self.available?(project)
project.builds_enabled?
end
def self.allowed?(project, user)
can?(user, :create_deployment, project)
end
def execute(match)
from = match[:from]
to = match[:to]
actions = find_actions(from, to)
return unless actions.present?
if actions.one?
play!(from, to, actions.first)
else
Result.new(:error, 'Too many actions defined')
end
end
private
def play!(from, to, action)
new_action = action.play(current_user)
Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.")
end
def find_actions(from, to)
environment = project.environments.find_by(name: from)
return unless environment
environment.actions_for(to).select(&:starts_environment?)
end
def url(subject)
polymorphic_url(
[ subject.project.namespace.becomes(Namespace), subject.project, subject ])
end
end
end
end
module Gitlab
module ChatCommands
Result = Struct.new(:type, :message)
end
end
module Gitlab
module Ci
module Build
module Credentials
class Base
def type
self.class.name.demodulize.underscore
end
end
end
end
end
end
module Gitlab
module Ci
module Build
module Credentials
class Factory
def initialize(build)
@build = build
end
def create!
credentials.select(&:valid?)
end
private
def credentials
providers.map { |provider| provider.new(@build) }
end
def providers
[Registry]
end
end
end
end
end
end
module Gitlab
module Ci
module Build
module Credentials
class Registry < Base
attr_reader :username, :password
def initialize(build)
@username = 'gitlab-ci-token'
@password = build.token
end
def url
Gitlab.config.registry.host_port
end
def valid?
Gitlab.config.registry.enabled
end
end
end
end
end
end
...@@ -108,7 +108,7 @@ module Gitlab ...@@ -108,7 +108,7 @@ module Gitlab
self.class.nodes.each_key do |key| self.class.nodes.each_key do |key|
global_entry = deps[key] global_entry = deps[key]
job_entry = @entries[key] job_entry = self[key]
if global_entry.specified? && !job_entry.specified? if global_entry.specified? && !job_entry.specified?
@entries[key] = global_entry @entries[key] = global_entry
......
module Gitlab
module CycleAnalytics
class Permissions
STAGE_PERMISSIONS = {
issue: :read_issue,
code: :read_merge_request,
test: :read_build,
review: :read_merge_request,
staging: :read_build,
production: :read_issue,
}.freeze
def self.get(*args)
new(*args).get
end
def initialize(user:, project:)
@user = user
@project = project
@stage_permission_hash = {}
end
def get
::CycleAnalytics::STAGES.each do |stage|
@stage_permission_hash[stage] = authorized_stage?(stage)
end
@stage_permission_hash
end
private
def authorized_stage?(stage)
return false unless authorize_project(:read_cycle_analytics)
STAGE_PERMISSIONS[stage] ? authorize_project(STAGE_PERMISSIONS[stage]) : true
end
def authorize_project(permission)
Ability.allowed?(@user, permission, @project)
end
end
end
end
require 'set'
module Gitlab
# Module that can be used to detect if a path points to a special file such as
# a README or a CONTRIBUTING file.
module FileDetector
PATTERNS = {
readme: /\Areadme/i,
changelog: /\A(changelog|history|changes|news)/i,
license: /\A(licen[sc]e|copying)(\..+|\z)/i,
contributing: /\Acontributing/i,
version: 'version',
gitignore: '.gitignore',
koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml',
avatar: /\Alogo\.(png|jpg|gif)\z/
}
# Returns an Array of file types based on the given paths.
#
# This method can be used to check if a list of file paths (e.g. of changed
# files) involve any special files such as a README or a LICENSE file.
#
# Example:
#
# types_in_paths(%w{README.md foo/bar.txt}) # => [:readme]
def self.types_in_paths(paths)
types = Set.new
paths.each do |path|
type = type_of(path)
types << type if type
end
types.to_a
end
# Returns the type of a file path, or nil if none could be detected.
#
# Returned types are Symbols such as `:readme`, `:version`, etc.
#
# Example:
#
# type_of('README.md') # => :readme
# type_of('VERSION') # => :version
def self.type_of(path)
name = File.basename(path)
PATTERNS.each do |type, search|
did_match = if search.is_a?(Regexp)
name =~ search
else
name.casecmp(search) == 0
end
return type if did_match
end
nil
end
end
end
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
# `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation # `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation
# will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation. # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
NAMESPACE_REGEX_STR_SIMPLE = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze NAMESPACE_REGEX_STR_SIMPLE = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR_SIMPLE})(?<!\.git|\.atom)".freeze NAMESPACE_REGEX_STR = '(?:' + NAMESPACE_REGEX_STR_SIMPLE + ')(?<!\.git|\.atom)'.freeze
def namespace_regex def namespace_regex
@namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
......
...@@ -24,20 +24,22 @@ module Mattermost ...@@ -24,20 +24,22 @@ module Mattermost
end end
end end
def present(resource) def present(subject)
return not_found unless resource return not_found unless subject
if resource.respond_to?(:count) if subject.is_a?(Gitlab::ChatCommands::Result)
if resource.count > 1 show_result(subject)
return multiple_resources(resource) elsif subject.respond_to?(:count)
elsif resource.count == 0 if subject.many?
return not_found multiple_resources(subject)
elsif subject.none?
not_found
else else
resource = resource.first single_resource(subject)
end end
else
single_resource(subject)
end end
single_resource(resource)
end end
def access_denied def access_denied
...@@ -46,6 +48,15 @@ module Mattermost ...@@ -46,6 +48,15 @@ module Mattermost
private private
def show_result(result)
case result.type
when :success
in_channel_response(result.message)
else
ephemeral_response(result.message)
end
end
def not_found def not_found
ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
end end
...@@ -54,7 +65,7 @@ module Mattermost ...@@ -54,7 +65,7 @@ module Mattermost
return error(resource) if resource.errors.any? || !resource.persisted? return error(resource) if resource.errors.any? || !resource.persisted?
message = "### #{title(resource)}" message = "### #{title(resource)}"
message << "\n\n#{resource.description}" if resource.description message << "\n\n#{resource.description}" if resource.try(:description)
in_channel_response(message) in_channel_response(message)
end end
...@@ -74,7 +85,10 @@ module Mattermost ...@@ -74,7 +85,10 @@ module Mattermost
end end
def title(resource) def title(resource)
"[#{resource.to_reference} #{resource.title}](#{url(resource)})" reference = resource.try(:to_reference) || resource.try(:id)
title = resource.try(:title) || resource.try(:name)
"[#{reference} #{title}](#{url(resource)})"
end end
def header_with_list(header, items) def header_with_list(header, items)
......
require 'spec_helper'
describe Projects::CycleAnalyticsController do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
sign_in(user)
project.team << [user, :master]
end
describe 'cycle analytics not set up flag' do
context 'with no data' do
it 'is true' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param)
expect(response).to be_success
expect(assigns(:cycle_analytics_no_data)).to eq(true)
end
end
context 'with data' do
before do
issue = create(:issue, project: project, created_at: 4.days.ago)
milestone = create(:milestone, project: project, created_at: 5.days.ago)
issue.update(milestone: milestone)
create_merge_request_closing_issue(issue)
end
it 'is false' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param)
expect(response).to be_success
expect(assigns(:cycle_analytics_no_data)).to eq(false)
end
end
end
end
...@@ -59,6 +59,12 @@ FactoryGirl.define do ...@@ -59,6 +59,12 @@ FactoryGirl.define do
self.when 'manual' self.when 'manual'
end end
trait :teardown_environment do
options do
{ environment: { action: 'stop' } }
end
end
trait :allowed_to_fail do trait :allowed_to_fail do
allow_failure true allow_failure true
end end
......
...@@ -3,8 +3,8 @@ require 'rails_helper' ...@@ -3,8 +3,8 @@ require 'rails_helper'
describe 'Filter issues', feature: true do describe 'Filter issues', feature: true do
include WaitForAjax include WaitForAjax
let!(:project) { create(:project) }
let!(:group) { create(:group) } let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
let!(:user) { create(:user)} let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) } let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) } let!(:label) { create(:label, project: project) }
...@@ -127,7 +127,7 @@ describe 'Filter issues', feature: true do ...@@ -127,7 +127,7 @@ describe 'Filter issues', feature: true do
expect(page).to have_content wontfix.title expect(page).to have_content wontfix.title
end end
find('body').click find('.dropdown-menu-close-icon').click
expect(find('.filtered-labels')).to have_content(wontfix.title) expect(find('.filtered-labels')).to have_content(wontfix.title)
...@@ -135,7 +135,7 @@ describe 'Filter issues', feature: true do ...@@ -135,7 +135,7 @@ describe 'Filter issues', feature: true do
wait_for_ajax wait_for_ajax
find('.dropdown-menu-labels a', text: label.title).click find('.dropdown-menu-labels a', text: label.title).click
find('body').click find('.dropdown-menu-close-icon').click
expect(find('.filtered-labels')).to have_content(wontfix.title) expect(find('.filtered-labels')).to have_content(wontfix.title)
expect(find('.filtered-labels')).to have_content(label.title) expect(find('.filtered-labels')).to have_content(label.title)
...@@ -150,8 +150,8 @@ describe 'Filter issues', feature: true do ...@@ -150,8 +150,8 @@ describe 'Filter issues', feature: true do
it "selects and unselects `won't fix`" do it "selects and unselects `won't fix`" do
find('.dropdown-menu-labels a', text: wontfix.title).click find('.dropdown-menu-labels a', text: wontfix.title).click
find('.dropdown-menu-labels a', text: wontfix.title).click find('.dropdown-menu-labels a', text: wontfix.title).click
# Close label dropdown to load
find('body').click find('.dropdown-menu-close-icon').click
expect(page).not_to have_css('.filtered-labels') expect(page).not_to have_css('.filtered-labels')
end end
end end
......
...@@ -371,10 +371,12 @@ describe 'Issues', feature: true do ...@@ -371,10 +371,12 @@ describe 'Issues', feature: true do
describe 'when I want to reset my incoming email token' do describe 'when I want to reset my incoming email token' do
let(:project1) { create(:project, namespace: @user.namespace) } let(:project1) { create(:project, namespace: @user.namespace) }
let(:issue) { create(:issue, project: project1) }
before do before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
project1.team << [@user, :master] project1.team << [@user, :master]
project1.issues << issue
visit namespace_project_issues_path(@user.namespace, project1) visit namespace_project_issues_path(@user.namespace, project1)
end end
...@@ -576,7 +578,10 @@ describe 'Issues', feature: true do ...@@ -576,7 +578,10 @@ describe 'Issues', feature: true do
describe 'new issue by email' do describe 'new issue by email' do
shared_examples 'show the email in the modal' do shared_examples 'show the email in the modal' do
let(:issue) { create(:issue, project: project) }
before do before do
project.issues << issue
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
......
require 'spec_helper'
describe 'Deleted source branch', feature: true, js: true do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
before do
login_as user
merge_request.project.team << [user, :master]
merge_request.update!(source_branch: 'this-branch-does-not-exist')
visit namespace_project_merge_request_path(
merge_request.project.namespace,
merge_request.project, merge_request
)
end
it 'shows a message about missing source branch' do
expect(page).to have_content(
'Source branch this-branch-does-not-exist does not exist'
)
end
it 'hides Discussion, Commits and Changes tabs' do
within '.merge-request-details' do
expect(page).to have_no_content('Discussion')
expect(page).to have_no_content('Commits')
expect(page).to have_no_content('Changes')
end
end
end
...@@ -3,11 +3,12 @@ require 'spec_helper' ...@@ -3,11 +3,12 @@ require 'spec_helper'
feature 'Merge Request versions', js: true, feature: true do feature 'Merge Request versions', js: true, feature: true do
let(:merge_request) { create(:merge_request, importing: true) } let(:merge_request) { create(:merge_request, importing: true) }
let(:project) { merge_request.source_project } let(:project) { merge_request.source_project }
let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) }
let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
before do before do
login_as :admin login_as :admin
merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
end end
...@@ -53,7 +54,7 @@ feature 'Merge Request versions', js: true, feature: true do ...@@ -53,7 +54,7 @@ feature 'Merge Request versions', js: true, feature: true do
project.namespace, project.namespace,
project, project,
merge_request.iid, merge_request.iid,
diff_id: 2, diff_id: merge_request_diff3.id,
start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
) )
end end
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
describe('Activities', () => { describe('Activities', () => {
beforeEach(() => { beforeEach(() => {
fixture.load(fixtureTemplate); fixture.load(fixtureTemplate);
new Activities(); new gl.Activities();
}); });
for(let i = 0; i < filters.length; i++) { for(let i = 0; i < filters.length; i++) {
......
...@@ -135,7 +135,7 @@ describe('Environment item', () => { ...@@ -135,7 +135,7 @@ describe('Environment item', () => {
}); });
it('should render environment name', () => { it('should render environment name', () => {
expect(component.$el.querySelector('.environment-name').textContent).toEqual(environment.name); expect(component.$el.querySelector('.environment-name').textContent).toContain(environment.name);
}); });
describe('With deployment', () => { describe('With deployment', () => {
......
/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */ /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */
/*= require merge_request_widget */ /*= require merge_request_widget */
/*= require lib/utils/timeago.js */ /*= require lib/utils/timeago */
/*= require lib/utils/datetime_utility */
(function() { (function() {
describe('MergeRequestWidget', function() { describe('MergeRequestWidget', function() {
...@@ -54,6 +55,57 @@ ...@@ -54,6 +55,57 @@
}); });
}); });
describe('renderEnvironments', function() {
describe('should render correct timeago', function() {
beforeEach(function() {
this.environments = [{
id: 'test-environment-id',
url: 'testurl',
deployed_at: new Date().toISOString(),
deployed_at_formatted: true
}];
});
function getTimeagoText(template) {
var el = document.createElement('html');
el.innerHTML = template;
return el.querySelector('.js-environment-timeago').innerText.trim();
}
it('should render less than a minute ago text', function() {
spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
expect(getTimeagoText(template)).toBe('less than a minute ago.');
});
this.class.renderEnvironments(this.environments);
});
it('should render about an hour ago text', function() {
var oneHourAgo = new Date();
oneHourAgo.setHours(oneHourAgo.getHours() - 1);
this.environments[0].deployed_at = oneHourAgo.toISOString();
spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
expect(getTimeagoText(template)).toBe('about an hour ago.');
});
this.class.renderEnvironments(this.environments);
});
it('should render about 2 hours ago text', function() {
var twoHoursAgo = new Date();
twoHoursAgo.setHours(twoHoursAgo.getHours() - 2);
this.environments[0].deployed_at = twoHoursAgo.toISOString();
spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
expect(getTimeagoText(template)).toBe('about 2 hours ago.');
});
this.class.renderEnvironments(this.environments);
});
});
});
return describe('getCIStatus', function() { return describe('getCIStatus', function() {
beforeEach(function() { beforeEach(function() {
this.ciStatusData = { this.ciStatusData = {
......
//= require lib/utils/pretty_time
(() => {
const PrettyTime = gl.PrettyTime;
describe('PrettyTime methods', function () {
describe('parseSeconds', function () {
it('should correctly parse a negative value', function () {
const parser = PrettyTime.parseSeconds;
const zeroSeconds = parser(-1000);
expect(zeroSeconds.minutes).toBe(16);
expect(zeroSeconds.hours).toBe(0);
expect(zeroSeconds.days).toBe(0);
expect(zeroSeconds.weeks).toBe(0);
});
it('should correctly parse a zero value', function () {
const parser = PrettyTime.parseSeconds;
const zeroSeconds = parser(0);
expect(zeroSeconds.minutes).toBe(0);
expect(zeroSeconds.hours).toBe(0);
expect(zeroSeconds.days).toBe(0);
expect(zeroSeconds.weeks).toBe(0);
});
it('should correctly parse a small non-zero second values', function () {
const parser = PrettyTime.parseSeconds;
const subOneMinute = parser(10);
expect(subOneMinute.minutes).toBe(0);
expect(subOneMinute.hours).toBe(0);
expect(subOneMinute.days).toBe(0);
expect(subOneMinute.weeks).toBe(0);
const aboveOneMinute = parser(100);
expect(aboveOneMinute.minutes).toBe(1);
expect(aboveOneMinute.hours).toBe(0);
expect(aboveOneMinute.days).toBe(0);
expect(aboveOneMinute.weeks).toBe(0);
const manyMinutes = parser(1000);
expect(manyMinutes.minutes).toBe(16);
expect(manyMinutes.hours).toBe(0);
expect(manyMinutes.days).toBe(0);
expect(manyMinutes.weeks).toBe(0);
});
it('should correctly parse large second values', function () {
const parser = PrettyTime.parseSeconds;
const aboveOneHour = parser(4800);
expect(aboveOneHour.minutes).toBe(20);
expect(aboveOneHour.hours).toBe(1);
expect(aboveOneHour.days).toBe(0);
expect(aboveOneHour.weeks).toBe(0);
const aboveOneDay = parser(110000);
expect(aboveOneDay.minutes).toBe(33);
expect(aboveOneDay.hours).toBe(6);
expect(aboveOneDay.days).toBe(3);
expect(aboveOneDay.weeks).toBe(0);
const aboveOneWeek = parser(25000000);
expect(aboveOneWeek.minutes).toBe(26);
expect(aboveOneWeek.hours).toBe(0);
expect(aboveOneWeek.days).toBe(3);
expect(aboveOneWeek.weeks).toBe(173);
});
});
describe('stringifyTime', function () {
it('should stringify values with all non-zero units', function () {
const timeObject = {
weeks: 1,
days: 4,
hours: 7,
minutes: 20,
};
const timeString = PrettyTime.stringifyTime(timeObject);
expect(timeString).toBe('1w 4d 7h 20m');
});
it('should stringify values with some non-zero units', function () {
const timeObject = {
weeks: 0,
days: 4,
hours: 0,
minutes: 20,
};
const timeString = PrettyTime.stringifyTime(timeObject);
expect(timeString).toBe('4d 20m');
});
it('should stringify values with no non-zero units', function () {
const timeObject = {
weeks: 0,
days: 0,
hours: 0,
minutes: 0,
};
const timeString = PrettyTime.stringifyTime(timeObject);
expect(timeString).toBe('0m');
});
});
describe('abbreviateTime', function () {
it('should abbreviate stringified times for weeks', function () {
const fullTimeString = '1w 3d 4h 5m';
expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('1w');
});
it('should abbreviate stringified times for non-weeks', function () {
const fullTimeString = '0w 3d 4h 5m';
expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('3d');
});
});
});
})(window.gl || (window.gl = {}));
//= require jquery
//= require smart_interval
(() => {
const DEFAULT_MAX_INTERVAL = 100;
const DEFAULT_STARTING_INTERVAL = 5;
const DEFAULT_SHORT_TIMEOUT = 75;
const DEFAULT_LONG_TIMEOUT = 1000;
const DEFAULT_INCREMENT_FACTOR = 2;
function createDefaultSmartInterval(config) {
const defaultParams = {
callback: () => {},
startingInterval: DEFAULT_STARTING_INTERVAL,
maxInterval: DEFAULT_MAX_INTERVAL,
incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
delayStartBy: 0,
lazyStart: false,
};
if (config) {
_.extend(defaultParams, config);
}
return new gl.SmartInterval(defaultParams);
}
describe('SmartInterval', function () {
describe('Increment Interval', function () {
beforeEach(function () {
this.smartInterval = createDefaultSmartInterval();
});
it('should increment the interval delay', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
const intervalConfig = this.smartInterval.cfg;
const iterationCount = 4;
const maxIntervalAfterIterations = intervalConfig.startingInterval *
Math.pow(intervalConfig.incrementByFactorOf, (iterationCount - 1)); // 40
const currentInterval = interval.getCurrentInterval();
// Provide some flexibility for performance of testing environment
expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy();
done();
}, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40)
});
it('should not increment past maxInterval', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
const currentInterval = interval.getCurrentInterval();
expect(currentInterval).toBe(interval.cfg.maxInterval);
done();
}, DEFAULT_LONG_TIMEOUT);
});
});
describe('Public methods', function () {
beforeEach(function () {
this.smartInterval = createDefaultSmartInterval();
});
it('should cancel an interval', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
interval.cancel();
const intervalId = interval.state.intervalId;
const currentInterval = interval.getCurrentInterval();
const intervalLowerLimit = interval.cfg.startingInterval;
expect(intervalId).toBeUndefined();
expect(currentInterval).toBe(intervalLowerLimit);
done();
}, DEFAULT_SHORT_TIMEOUT);
});
it('should resume an interval', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
interval.cancel();
interval.resume();
const intervalId = interval.state.intervalId;
expect(intervalId).toBeTruthy();
done();
}, DEFAULT_SHORT_TIMEOUT);
});
});
describe('DOM Events', function () {
beforeEach(function () {
// This ensures DOM and DOM events are initialized for these specs.
fixture.set('<div></div>');
this.smartInterval = createDefaultSmartInterval();
});
it('should pause when page is not visible', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
interval.state.pageVisibility = 'hidden';
interval.handleVisibilityChange();
expect(interval.state.intervalId).toBeUndefined();
done();
}, DEFAULT_SHORT_TIMEOUT);
});
it('should resume when page is becomes visible at the previous interval', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
interval.state.pageVisibility = 'hidden';
interval.handleVisibilityChange();
expect(interval.state.intervalId).toBeUndefined();
// simulates triggering of visibilitychange event
interval.state.pageVisibility = 'visible';
interval.handleVisibilityChange();
expect(interval.state.intervalId).toBeTruthy();
done();
}, DEFAULT_SHORT_TIMEOUT);
});
it('should cancel on page unload', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
$(document).trigger('page:before-unload');
expect(interval.state.intervalId).toBeUndefined();
expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
done();
}, DEFAULT_SHORT_TIMEOUT);
});
});
});
})(window.gl || (window.gl = {}));
/* eslint-disable */
//= vue
//= vue-resource
//= require jquery
//= require subbable_resource
/*
* Test that each rest verb calls the publish and subscribe function and passes the correct value back
*
*
* */
((global) => {
describe('Subbable Resource', function () {
describe('PubSub', function () {
beforeEach(function () {
this.MockResource = new global.SubbableResource('https://example.com');
});
it('should successfully add a single subscriber', function () {
const callback = () => {};
this.MockResource.subscribe(callback);
expect(this.MockResource.subscribers.length).toBe(1);
expect(this.MockResource.subscribers[0]).toBe(callback);
});
it('should successfully add multiple subscribers', function () {
const callbackOne = () => {};
const callbackTwo = () => {};
const callbackThree = () => {};
this.MockResource.subscribe(callbackOne);
this.MockResource.subscribe(callbackTwo);
this.MockResource.subscribe(callbackThree);
expect(this.MockResource.subscribers.length).toBe(3);
});
it('should successfully publish an update to a single subscriber', function () {
const state = { myprop: 1 };
const callbacks = {
one: (data) => expect(data.myprop).toBe(2),
two: (data) => expect(data.myprop).toBe(2),
three: (data) => expect(data.myprop).toBe(2)
};
const spyOne = spyOn(callbacks, 'one');
const spyTwo = spyOn(callbacks, 'two');
const spyThree = spyOn(callbacks, 'three');
this.MockResource.subscribe(callbacks.one);
this.MockResource.subscribe(callbacks.two);
this.MockResource.subscribe(callbacks.three);
state.myprop++;
this.MockResource.publish(state);
expect(spyOne).toHaveBeenCalled();
expect(spyTwo).toHaveBeenCalled();
expect(spyThree).toHaveBeenCalled();
});
});
});
})(window.gl || (window.gl = {}));
...@@ -4,9 +4,9 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -4,9 +4,9 @@ describe Gitlab::ChatCommands::Command, service: true do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
subject { described_class.new(project, user, params).execute }
describe '#execute' do describe '#execute' do
subject { described_class.new(project, user, params).execute }
context 'when no command is available' do context 'when no command is available' do
let(:params) { { text: 'issue show 1' } } let(:params) { { text: 'issue show 1' } }
let(:project) { create(:project, has_external_issue_tracker: true) } let(:project) { create(:project, has_external_issue_tracker: true) }
...@@ -51,5 +51,44 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -51,5 +51,44 @@ describe Gitlab::ChatCommands::Command, service: true do
expect(subject[:text]).to match(/\/issues\/\d+/) expect(subject[:text]).to match(/\/issues\/\d+/)
end end
end end
context 'when trying to do deployment' do
let(:params) { { text: 'deploy staging to production' } }
let!(:build) { create(:ci_build, project: project) }
let!(:staging) { create(:environment, name: 'staging', project: project) }
let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
let!(:manual) do
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production')
end
context 'and user can not create deployment' do
it 'returns action' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to start_with('Whoops! That action is not allowed')
end
end
context 'and user does have deployment permission' do
before do
project.team << [user, :developer]
end
it 'returns action' do
expect(subject[:text]).to include('Deployment from staging to production started')
expect(subject[:response_type]).to be(:in_channel)
end
context 'when duplicate action exists' do
let!(:manual2) do
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production')
end
it 'returns error' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to include('Too many actions defined')
end
end
end
end
end end
end end
require 'spec_helper'
describe Gitlab::ChatCommands::Deploy, service: true do
describe '#execute' do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:regex_match) { described_class.match('deploy staging to production') }
before do
project.team << [user, :master]
end
subject do
described_class.new(project, user).execute(regex_match)
end
context 'if no environment is defined' do
it 'returns nil' do
expect(subject).to be_nil
end
end
context 'with environment' do
let!(:staging) { create(:environment, name: 'staging', project: project) }
let!(:build) { create(:ci_build, project: project) }
let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
context 'without actions' do
it 'returns nil' do
expect(subject).to be_nil
end
end
context 'with action' do
let!(:manual1) do
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production')
end
it 'returns success result' do
expect(subject.type).to eq(:success)
expect(subject.message).to include('Deployment from staging to production started')
end
context 'when duplicate action exists' do
let!(:manual2) do
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production')
end
it 'returns error' do
expect(subject.type).to eq(:error)
expect(subject.message).to include('Too many actions defined')
end
end
context 'when teardown action exists' do
let!(:teardown) do
create(:ci_build, :manual, :teardown_environment,
project: project, pipeline: build.pipeline,
name: 'teardown', environment: 'production')
end
it 'returns success result' do
expect(subject.type).to eq(:success)
expect(subject.message).to include('Deployment from staging to production started')
end
end
end
end
end
describe 'self.match' do
it 'matches the environment' do
match = described_class.match('deploy staging to production')
expect(match[:from]).to eq('staging')
expect(match[:to]).to eq('production')
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Build::Credentials::Factory do
let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
subject { Gitlab::Ci::Build::Credentials::Factory.new(build).create! }
class TestProvider
def initialize(build); end
end
before do
allow_any_instance_of(Gitlab::Ci::Build::Credentials::Factory).to receive(:providers).and_return([TestProvider])
end
context 'when provider is valid' do
before do
allow_any_instance_of(TestProvider).to receive(:valid?).and_return(true)
end
it 'generates an array of credentials objects' do
is_expected.to be_kind_of(Array)
is_expected.not_to be_empty
expect(subject.first).to be_kind_of(TestProvider)
end
end
context 'when provider is not valid' do
before do
allow_any_instance_of(TestProvider).to receive(:valid?).and_return(false)
end
it 'generates an array without specific credential object' do
is_expected.to be_kind_of(Array)
is_expected.to be_empty
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Build::Credentials::Registry do
let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
let(:registry_url) { 'registry.example.com:5005' }
subject { Gitlab::Ci::Build::Credentials::Registry.new(build) }
before do
stub_container_registry_config(host_port: registry_url)
end
it 'contains valid DockerRegistry credentials' do
expect(subject).to be_kind_of(Gitlab::Ci::Build::Credentials::Registry)
expect(subject.username).to eq 'gitlab-ci-token'
expect(subject.password).to eq build.token
expect(subject.url).to eq registry_url
expect(subject.type).to eq 'registry'
end
describe '.valid?' do
subject { Gitlab::Ci::Build::Credentials::Registry.new(build).valid? }
context 'when registry is enabled' do
before do
stub_container_registry_config(enabled: true)
end
it { is_expected.to be_truthy }
end
context 'when registry is disabled' do
before do
stub_container_registry_config(enabled: false)
end
it { is_expected.to be_falsey }
end
end
end
...@@ -13,7 +13,7 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -13,7 +13,7 @@ describe Gitlab::Ci::Config::Entry::Global do
end end
end end
context 'when hash is valid' do context 'when configuration is valid' do
context 'when some entries defined' do context 'when some entries defined' do
let(:hash) do let(:hash) do
{ before_script: ['ls', 'pwd'], { before_script: ['ls', 'pwd'],
...@@ -225,29 +225,44 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -225,29 +225,44 @@ describe Gitlab::Ci::Config::Entry::Global do
end end
end end
context 'when hash is not valid' do context 'when configuration is not valid' do
before { global.compose! } before { global.compose! }
let(:hash) do context 'when before script is not an array' do
{ before_script: 'ls' } let(:hash) do
end { before_script: 'ls' }
end
describe '#valid?' do describe '#valid?' do
it 'is not valid' do it 'is not valid' do
expect(global).not_to be_valid expect(global).not_to be_valid
end
end end
end
describe '#errors' do describe '#errors' do
it 'reports errors from child nodes' do it 'reports errors from child nodes' do
expect(global.errors) expect(global.errors)
.to include 'before_script config should be an array of strings' .to include 'before_script config should be an array of strings'
end
end
describe '#before_script_value' do
it 'returns nil' do
expect(global.before_script_value).to be_nil
end
end end
end end
describe '#before_script_value' do context 'when job does not have commands' do
it 'returns nil' do let(:hash) do
expect(global.before_script_value).to be_nil { before_script: ['echo 123'], rspec: { stage: 'test' } }
end
describe '#errors' do
it 'reports errors about missing script' do
expect(global.errors)
.to include "jobs:rspec script can't be blank"
end
end end
end end
end end
...@@ -281,7 +296,7 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -281,7 +296,7 @@ describe Gitlab::Ci::Config::Entry::Global do
{ cache: { key: 'a' }, rspec: { script: 'ls' } } { cache: { key: 'a' }, rspec: { script: 'ls' } }
end end
context 'when node exists' do context 'when entry exists' do
it 'returns correct entry' do it 'returns correct entry' do
expect(global[:cache]) expect(global[:cache])
.to be_an_instance_of Gitlab::Ci::Config::Entry::Cache .to be_an_instance_of Gitlab::Ci::Config::Entry::Cache
...@@ -289,7 +304,7 @@ describe Gitlab::Ci::Config::Entry::Global do ...@@ -289,7 +304,7 @@ describe Gitlab::Ci::Config::Entry::Global do
end end
end end
context 'when node does not exist' do context 'when entry does not exist' do
it 'always return unspecified node' do it 'always return unspecified node' do
expect(global[:some][:unknown][:node]) expect(global[:some][:unknown][:node])
.not_to be_specified .not_to be_specified
......
require 'spec_helper'
describe Gitlab::CycleAnalytics::Permissions do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
subject { described_class.get(user: user, project: project) }
context 'user with no relation to the project' do
it 'has no permissions to issue stage' do
expect(subject[:issue]).to eq(false)
end
it 'has no permissions to test stage' do
expect(subject[:test]).to eq(false)
end
it 'has no permissions to staging stage' do
expect(subject[:staging]).to eq(false)
end
it 'has no permissions to production stage' do
expect(subject[:production]).to eq(false)
end
it 'has no permissions to code stage' do
expect(subject[:code]).to eq(false)
end
it 'has no permissions to review stage' do
expect(subject[:review]).to eq(false)
end
it 'has no permissions to plan stage' do
expect(subject[:plan]).to eq(false)
end
end
context 'user is master' do
before do
project.team << [user, :master]
end
it 'has permissions to issue stage' do
expect(subject[:issue]).to eq(true)
end
it 'has permissions to test stage' do
expect(subject[:test]).to eq(true)
end
it 'has permissions to staging stage' do
expect(subject[:staging]).to eq(true)
end
it 'has permissions to production stage' do
expect(subject[:production]).to eq(true)
end
it 'has permissions to code stage' do
expect(subject[:code]).to eq(true)
end
it 'has permissions to review stage' do
expect(subject[:review]).to eq(true)
end
it 'has permissions to plan stage' do
expect(subject[:plan]).to eq(true)
end
end
context 'user has no build permissions' do
before do
project.team << [user, :guest]
end
it 'has permissions to issue stage' do
expect(subject[:issue]).to eq(true)
end
it 'has no permissions to test stage' do
expect(subject[:test]).to eq(false)
end
it 'has no permissions to staging stage' do
expect(subject[:staging]).to eq(false)
end
end
context 'user has no merge request permissions' do
before do
project.team << [user, :guest]
end
it 'has permissions to issue stage' do
expect(subject[:issue]).to eq(true)
end
it 'has no permissions to code stage' do
expect(subject[:code]).to eq(false)
end
it 'has no permissions to review stage' do
expect(subject[:review]).to eq(false)
end
end
context 'user has no issue permissions' do
before do
project.team << [user, :developer]
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
end
it 'has permissions to code stage' do
expect(subject[:code]).to eq(true)
end
it 'has no permissions to issue stage' do
expect(subject[:issue]).to eq(false)
end
it 'has no permissions to production stage' do
expect(subject[:production]).to eq(false)
end
end
end
require 'spec_helper'
describe Gitlab::FileDetector do
describe '.types_in_paths' do
it 'returns the file types for the given paths' do
expect(described_class.types_in_paths(%w(README.md CHANGELOG VERSION VERSION))).
to eq(%i{readme changelog version})
end
it 'does not include unrecognized file paths' do
expect(described_class.types_in_paths(%w(README.md foo.txt))).
to eq(%i{readme})
end
end
describe '.type_of' do
it 'returns the type of a README file' do
expect(described_class.type_of('README.md')).to eq(:readme)
end
it 'returns the type of a changelog file' do
%w(CHANGELOG HISTORY CHANGES NEWS).each do |file|
expect(described_class.type_of(file)).to eq(:changelog)
end
end
it 'returns the type of a license file' do
%w(LICENSE LICENCE COPYING).each do |file|
expect(described_class.type_of(file)).to eq(:license)
end
end
it 'returns the type of a version file' do
expect(described_class.type_of('VERSION')).to eq(:version)
end
it 'returns the type of a .gitignore file' do
expect(described_class.type_of('.gitignore')).to eq(:gitignore)
end
it 'returns the type of a Koding config file' do
expect(described_class.type_of('.koding.yml')).to eq(:koding)
end
it 'returns the type of a GitLab CI config file' do
expect(described_class.type_of('.gitlab-ci.yml')).to eq(:gitlab_ci)
end
it 'returns the type of an avatar' do
%w(logo.gif logo.png logo.jpg).each do |file|
expect(described_class.type_of(file)).to eq(:avatar)
end
end
it 'returns nil for an unknown file' do
expect(described_class.type_of('foo.txt')).to be_nil
end
end
end
...@@ -9,6 +9,7 @@ describe Environment, models: true do ...@@ -9,6 +9,7 @@ describe Environment, models: true do
it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) } it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) } it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
...@@ -187,4 +188,15 @@ describe Environment, models: true do ...@@ -187,4 +188,15 @@ describe Environment, models: true do
it { is_expected.to be false } it { is_expected.to be false }
end end
end end
describe '#actions_for' do
let(:deployment) { create(:deployment, environment: environment) }
let(:pipeline) { deployment.deployable.pipeline }
let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_BUILD_REF_NAME' )}
let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )}
it 'returns a list of actions with matching environment' do
expect(environment.actions_for('review/master')).to contain_exactly(review_action)
end
end
end end
...@@ -1572,7 +1572,7 @@ describe Project, models: true do ...@@ -1572,7 +1572,7 @@ describe Project, models: true do
end end
it 'expires the avatar cache' do it 'expires the avatar cache' do
expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch) expect(project.repository).to receive(:expire_avatar_cache)
project.change_head(project.default_branch) project.change_head(project.default_branch)
end end
......
...@@ -464,11 +464,7 @@ describe Repository, models: true do ...@@ -464,11 +464,7 @@ describe Repository, models: true do
end end
end end
describe "#changelog" do describe "#changelog", caching: true do
before do
repository.send(:cache).expire(:changelog)
end
it 'accepts changelog' do it 'accepts changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')]) expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')])
...@@ -500,17 +496,16 @@ describe Repository, models: true do ...@@ -500,17 +496,16 @@ describe Repository, models: true do
end end
end end
describe "#license_blob" do describe "#license_blob", caching: true do
before do before do
repository.send(:cache).expire(:license_blob)
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end end
it 'handles when HEAD points to non-existent ref' do it 'handles when HEAD points to non-existent ref' do
repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
rugged = double('rugged')
expect(rugged).to receive(:head_unborn?).and_return(true) allow(repository).to receive(:file_on_head).
expect(repository).to receive(:rugged).and_return(rugged) and_raise(Rugged::ReferenceError)
expect(repository.license_blob).to be_nil expect(repository.license_blob).to be_nil
end end
...@@ -537,22 +532,18 @@ describe Repository, models: true do ...@@ -537,22 +532,18 @@ describe Repository, models: true do
end end
end end
describe '#license_key' do describe '#license_key', caching: true do
before do before do
repository.send(:cache).expire(:license_key)
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end end
it 'handles when HEAD points to non-existent ref' do it 'returns nil when no license is detected' do
repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
rugged = double('rugged')
expect(rugged).to receive(:head_unborn?).and_return(true)
expect(repository).to receive(:rugged).and_return(rugged)
expect(repository.license_key).to be_nil expect(repository.license_key).to be_nil
end end
it 'returns nil when no license is detected' do it 'returns nil when the repository does not exist' do
expect(repository).to receive(:exists?).and_return(false)
expect(repository.license_key).to be_nil expect(repository.license_key).to be_nil
end end
...@@ -569,7 +560,7 @@ describe Repository, models: true do ...@@ -569,7 +560,7 @@ describe Repository, models: true do
end end
end end
describe "#gitlab_ci_yml" do describe "#gitlab_ci_yml", caching: true do
it 'returns valid file' do it 'returns valid file' do
files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')] files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
expect(repository.tree).to receive(:blobs).and_return(files) expect(repository.tree).to receive(:blobs).and_return(files)
...@@ -583,7 +574,7 @@ describe Repository, models: true do ...@@ -583,7 +574,7 @@ describe Repository, models: true do
end end
it 'returns nil for empty repository' do it 'returns nil for empty repository' do
expect(repository).to receive(:empty?).and_return(true) allow(repository).to receive(:file_on_head).and_raise(Rugged::ReferenceError)
expect(repository.gitlab_ci_yml).to be_nil expect(repository.gitlab_ci_yml).to be_nil
end end
end end
...@@ -778,7 +769,6 @@ describe Repository, models: true do ...@@ -778,7 +769,6 @@ describe Repository, models: true do
expect(repository).not_to receive(:expire_emptiness_caches) expect(repository).not_to receive(:expire_emptiness_caches)
expect(repository).to receive(:expire_branches_cache) expect(repository).to receive(:expire_branches_cache)
expect(repository).to receive(:expire_has_visible_content_cache) expect(repository).to receive(:expire_has_visible_content_cache)
expect(repository).to receive(:expire_branch_count_cache)
repository.update_branch_with_hooks(user, 'new-feature') { new_rev } repository.update_branch_with_hooks(user, 'new-feature') { new_rev }
end end
...@@ -797,7 +787,6 @@ describe Repository, models: true do ...@@ -797,7 +787,6 @@ describe Repository, models: true do
expect(empty_repository).to receive(:expire_emptiness_caches) expect(empty_repository).to receive(:expire_emptiness_caches)
expect(empty_repository).to receive(:expire_branches_cache) expect(empty_repository).to receive(:expire_branches_cache)
expect(empty_repository).to receive(:expire_has_visible_content_cache) expect(empty_repository).to receive(:expire_has_visible_content_cache)
expect(empty_repository).to receive(:expire_branch_count_cache)
empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!',
'Updates file content', 'master', false) 'Updates file content', 'master', false)
...@@ -811,8 +800,7 @@ describe Repository, models: true do ...@@ -811,8 +800,7 @@ describe Repository, models: true do
end end
it 'returns false when a repository does not exist' do it 'returns false when a repository does not exist' do
expect(repository.raw_repository).to receive(:rugged). allow(repository).to receive(:refs_directory_exists?).and_return(false)
and_raise(Gitlab::Git::Repository::NoRepository)
expect(repository.exists?).to eq(false) expect(repository.exists?).to eq(false)
end end
...@@ -916,34 +904,6 @@ describe Repository, models: true do ...@@ -916,34 +904,6 @@ describe Repository, models: true do
end end
end end
describe '#expire_cache' do
it 'expires all caches' do
expect(repository).to receive(:expire_branch_cache)
repository.expire_cache
end
it 'expires the caches for a specific branch' do
expect(repository).to receive(:expire_branch_cache).with('master')
repository.expire_cache('master')
end
it 'expires the emptiness caches for an empty repository' do
expect(repository).to receive(:empty?).and_return(true)
expect(repository).to receive(:expire_emptiness_caches)
repository.expire_cache
end
it 'does not expire the emptiness caches for a non-empty repository' do
expect(repository).to receive(:empty?).and_return(false)
expect(repository).not_to receive(:expire_emptiness_caches)
repository.expire_cache
end
end
describe '#expire_root_ref_cache' do describe '#expire_root_ref_cache' do
it 'expires the root reference cache' do it 'expires the root reference cache' do
repository.root_ref repository.root_ref
...@@ -1003,12 +963,23 @@ describe Repository, models: true do ...@@ -1003,12 +963,23 @@ describe Repository, models: true do
describe '#expire_emptiness_caches' do describe '#expire_emptiness_caches' do
let(:cache) { repository.send(:cache) } let(:cache) { repository.send(:cache) }
it 'expires the caches' do it 'expires the caches for an empty repository' do
allow(repository).to receive(:empty?).and_return(true)
expect(cache).to receive(:expire).with(:empty?) expect(cache).to receive(:expire).with(:empty?)
expect(repository).to receive(:expire_has_visible_content_cache) expect(repository).to receive(:expire_has_visible_content_cache)
repository.expire_emptiness_caches repository.expire_emptiness_caches
end end
it 'does not expire the cache for a non-empty repository' do
allow(repository).to receive(:empty?).and_return(false)
expect(cache).not_to receive(:expire).with(:empty?)
expect(repository).not_to receive(:expire_has_visible_content_cache)
repository.expire_emptiness_caches
end
end end
describe :skip_merged_commit do describe :skip_merged_commit do
...@@ -1120,24 +1091,12 @@ describe Repository, models: true do ...@@ -1120,24 +1091,12 @@ describe Repository, models: true do
repository.before_delete repository.before_delete
end end
it 'flushes the tag count cache' do
expect(repository).to receive(:expire_tag_count_cache)
repository.before_delete
end
it 'flushes the branches cache' do it 'flushes the branches cache' do
expect(repository).to receive(:expire_branches_cache) expect(repository).to receive(:expire_branches_cache)
repository.before_delete repository.before_delete
end end
it 'flushes the branch count cache' do
expect(repository).to receive(:expire_branch_count_cache)
repository.before_delete
end
it 'flushes the root ref cache' do it 'flushes the root ref cache' do
expect(repository).to receive(:expire_root_ref_cache) expect(repository).to receive(:expire_root_ref_cache)
...@@ -1162,36 +1121,18 @@ describe Repository, models: true do ...@@ -1162,36 +1121,18 @@ describe Repository, models: true do
allow(repository).to receive(:exists?).and_return(true) allow(repository).to receive(:exists?).and_return(true)
end end
it 'flushes the caches that depend on repository data' do
expect(repository).to receive(:expire_cache)
repository.before_delete
end
it 'flushes the tags cache' do it 'flushes the tags cache' do
expect(repository).to receive(:expire_tags_cache) expect(repository).to receive(:expire_tags_cache)
repository.before_delete repository.before_delete
end end
it 'flushes the tag count cache' do
expect(repository).to receive(:expire_tag_count_cache)
repository.before_delete
end
it 'flushes the branches cache' do it 'flushes the branches cache' do
expect(repository).to receive(:expire_branches_cache) expect(repository).to receive(:expire_branches_cache)
repository.before_delete repository.before_delete
end end
it 'flushes the branch count cache' do
expect(repository).to receive(:expire_branch_count_cache)
repository.before_delete
end
it 'flushes the root ref cache' do it 'flushes the root ref cache' do
expect(repository).to receive(:expire_root_ref_cache) expect(repository).to receive(:expire_root_ref_cache)
...@@ -1222,8 +1163,9 @@ describe Repository, models: true do ...@@ -1222,8 +1163,9 @@ describe Repository, models: true do
describe '#before_push_tag' do describe '#before_push_tag' do
it 'flushes the cache' do it 'flushes the cache' do
expect(repository).to receive(:expire_cache) expect(repository).to receive(:expire_statistics_caches)
expect(repository).to receive(:expire_tag_count_cache) expect(repository).to receive(:expire_emptiness_caches)
expect(repository).to receive(:expire_tags_cache)
repository.before_push_tag repository.before_push_tag
end end
...@@ -1240,17 +1182,23 @@ describe Repository, models: true do ...@@ -1240,17 +1182,23 @@ describe Repository, models: true do
describe '#after_import' do describe '#after_import' do
it 'flushes and builds the cache' do it 'flushes and builds the cache' do
expect(repository).to receive(:expire_content_cache) expect(repository).to receive(:expire_content_cache)
expect(repository).to receive(:build_cache) expect(repository).to receive(:expire_tags_cache)
expect(repository).to receive(:expire_branches_cache)
repository.after_import repository.after_import
end end
end end
describe '#after_push_commit' do describe '#after_push_commit' do
it 'flushes the cache' do it 'expires statistics caches' do
expect(repository).to receive(:expire_cache).with('master', '123') expect(repository).to receive(:expire_statistics_caches).
and_call_original
repository.after_push_commit('master', '123') expect(repository).to receive(:expire_branch_cache).
with('master').
and_call_original
repository.after_push_commit('master')
end end
end end
...@@ -1302,7 +1250,8 @@ describe Repository, models: true do ...@@ -1302,7 +1250,8 @@ describe Repository, models: true do
describe '#before_remove_tag' do describe '#before_remove_tag' do
it 'flushes the tag cache' do it 'flushes the tag cache' do
expect(repository).to receive(:expire_tag_count_cache) expect(repository).to receive(:expire_tags_cache).and_call_original
expect(repository).to receive(:expire_statistics_caches).and_call_original
repository.before_remove_tag repository.before_remove_tag
end end
...@@ -1320,23 +1269,23 @@ describe Repository, models: true do ...@@ -1320,23 +1269,23 @@ describe Repository, models: true do
end end
end end
describe '#expire_branch_count_cache' do describe '#expire_branches_cache' do
let(:cache) { repository.send(:cache) }
it 'expires the cache' do it 'expires the cache' do
expect(cache).to receive(:expire).with(:branch_count) expect(repository).to receive(:expire_method_caches).
with(%i(branch_names branch_count)).
and_call_original
repository.expire_branch_count_cache repository.expire_branches_cache
end end
end end
describe '#expire_tag_count_cache' do describe '#expire_tags_cache' do
let(:cache) { repository.send(:cache) }
it 'expires the cache' do it 'expires the cache' do
expect(cache).to receive(:expire).with(:tag_count) expect(repository).to receive(:expire_method_caches).
with(%i(tag_names tag_count)).
and_call_original
repository.expire_tag_count_cache repository.expire_tags_cache
end end
end end
...@@ -1412,170 +1361,316 @@ describe Repository, models: true do ...@@ -1412,170 +1361,316 @@ describe Repository, models: true do
describe '#avatar' do describe '#avatar' do
it 'returns nil if repo does not exist' do it 'returns nil if repo does not exist' do
expect(repository).to receive(:exists?).and_return(false) expect(repository).to receive(:file_on_head).
and_raise(Rugged::ReferenceError)
expect(repository.avatar).to eq(nil) expect(repository.avatar).to eq(nil)
end end
it 'returns the first avatar file found in the repository' do it 'returns the first avatar file found in the repository' do
expect(repository).to receive(:blob_at_branch). expect(repository).to receive(:file_on_head).
with('master', 'logo.png'). with(:avatar).
and_return(true) and_return(double(:tree, path: 'logo.png'))
expect(repository.avatar).to eq('logo.png') expect(repository.avatar).to eq('logo.png')
end end
it 'caches the output' do it 'caches the output' do
allow(repository).to receive(:blob_at_branch). expect(repository).to receive(:file_on_head).
with('master', 'logo.png'). with(:avatar).
and_return(true) once.
and_return(double(:tree, path: 'logo.png'))
expect(repository.avatar).to eq('logo.png')
expect(repository).not_to receive(:blob_at_branch) 2.times { expect(repository.avatar).to eq('logo.png') }
expect(repository.avatar).to eq('logo.png')
end end
end end
describe '#expire_avatar_cache' do describe '#expire_exists_cache' do
let(:cache) { repository.send(:cache) } let(:cache) { repository.send(:cache) }
before do it 'expires the cache' do
allow(repository).to receive(:cache).and_return(cache) expect(cache).to receive(:expire).with(:exists?)
repository.expire_exists_cache
end end
end
context 'without a branch or revision' do describe "#keep_around" do
it 'flushes the cache' do it "does not fail if we attempt to reference bad commit" do
expect(cache).to receive(:expire).with(:avatar) expect(repository.kept_around?('abc1234')).to be_falsey
end
repository.expire_avatar_cache it "stores a reference to the specified commit sha so it isn't garbage collected" do
end repository.keep_around(sample_commit.id)
expect(repository.kept_around?(sample_commit.id)).to be_truthy
end
it "attempting to call keep_around on truncated ref does not fail" do
repository.keep_around(sample_commit.id)
ref = repository.send(:keep_around_ref_name, sample_commit.id)
path = File.join(repository.path, ref)
# Corrupt the reference
File.truncate(path, 0)
expect(repository.kept_around?(sample_commit.id)).to be_falsey
repository.keep_around(sample_commit.id)
expect(repository.kept_around?(sample_commit.id)).to be_falsey
File.delete(path)
end
end
describe '#update_ref!' do
it 'can create a ref' do
repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
expect(repository.find_branch('foobar')).not_to be_nil
end end
context 'with a branch' do it 'raises CommitError when the ref update fails' do
it 'does not flush the cache if the branch is not the default branch' do expect do
expect(cache).not_to receive(:expire) repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
end.to raise_error(Repository::CommitError)
end
end
describe '#contribution_guide', caching: true do
it 'returns and caches the output' do
expect(repository).to receive(:file_on_head).
with(:contributing).
and_return(Gitlab::Git::Tree.new(path: 'CONTRIBUTING.md')).
once
repository.expire_avatar_cache('cats') 2.times do
expect(repository.contribution_guide).
to be_an_instance_of(Gitlab::Git::Tree)
end end
end
end
it 'flushes the cache if the branch equals the default branch' do describe '#gitignore', caching: true do
expect(cache).to receive(:expire).with(:avatar) it 'returns and caches the output' do
expect(repository).to receive(:file_on_head).
with(:gitignore).
and_return(Gitlab::Git::Tree.new(path: '.gitignore')).
once
repository.expire_avatar_cache(repository.root_ref) 2.times do
expect(repository.gitignore).to be_an_instance_of(Gitlab::Git::Tree)
end end
end end
end
context 'with a branch and revision' do describe '#koding_yml', caching: true do
let(:commit) { double(:commit) } it 'returns and caches the output' do
expect(repository).to receive(:file_on_head).
with(:koding).
and_return(Gitlab::Git::Tree.new(path: '.koding.yml')).
once
before do 2.times do
allow(repository).to receive(:commit).and_return(commit) expect(repository.koding_yml).to be_an_instance_of(Gitlab::Git::Tree)
end end
end
end
it 'does not flush the cache if the commit does not change any logos' do describe '#readme', caching: true do
diff = double(:diff, new_path: 'test.txt') context 'with a non-existing repository' do
it 'returns nil' do
expect(repository).to receive(:tree).with(:head).and_return(nil)
expect(commit).to receive(:raw_diffs).and_return([diff]) expect(repository.readme).to be_nil
expect(cache).not_to receive(:expire) end
end
repository.expire_avatar_cache(repository.root_ref, '123') context 'with an existing repository' do
it 'returns the README' do
expect(repository.readme).to be_an_instance_of(Gitlab::Git::Blob)
end end
end
end
it 'flushes the cache if the commit changes any of the logos' do describe '#expire_statistics_caches' do
diff = double(:diff, new_path: Repository::AVATAR_FILES[0]) it 'expires the caches' do
expect(repository).to receive(:expire_method_caches).
with(%i(size commit_count))
repository.expire_statistics_caches
end
end
expect(commit).to receive(:raw_diffs).and_return([diff]) describe '#expire_method_caches' do
expect(cache).to receive(:expire).with(:avatar) it 'expires the caches of the given methods' do
expect_any_instance_of(RepositoryCache).to receive(:expire).with(:readme)
expect_any_instance_of(RepositoryCache).to receive(:expire).with(:gitignore)
repository.expire_avatar_cache(repository.root_ref, '123') repository.expire_method_caches(%i(readme gitignore))
end
end end
end end
describe '#expire_exists_cache' do describe '#expire_all_method_caches' do
let(:cache) { repository.send(:cache) } it 'expires the caches of all methods' do
expect(repository).to receive(:expire_method_caches).
with(Repository::CACHED_METHODS)
repository.expire_all_method_caches
end
end
describe '#expire_avatar_cache' do
it 'expires the cache' do it 'expires the cache' do
expect(cache).to receive(:expire).with(:exists?) expect(repository).to receive(:expire_method_caches).with(%i(avatar))
repository.expire_exists_cache repository.expire_avatar_cache
end end
end end
describe '#build_cache' do describe '#file_on_head' do
let(:cache) { repository.send(:cache) } context 'with a non-existing repository' do
it 'returns nil' do
expect(repository).to receive(:tree).with(:head).and_return(nil)
it 'builds the caches if they do not already exist' do expect(repository.file_on_head(:readme)).to be_nil
cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags end
end
expect(cache).to receive(:exist?). context 'with a repository that has no blobs' do
exactly(cache_keys.length). it 'returns nil' do
times. expect_any_instance_of(Tree).to receive(:blobs).and_return([])
and_return(false)
expect(repository.file_on_head(:readme)).to be_nil
end
end
context 'with an existing repository' do
it 'returns a Gitlab::Git::Tree' do
expect(repository.file_on_head(:readme)).
to be_an_instance_of(Gitlab::Git::Tree)
end
end
end
describe '#head_tree' do
context 'with an existing repository' do
it 'returns a Tree' do
expect(repository.head_tree).to be_an_instance_of(Tree)
end
end
context 'with a non-existing repository' do
it 'returns nil' do
expect(repository).to receive(:head_commit).and_return(nil)
expect(repository.head_tree).to be_nil
end
end
end
cache_keys.each do |key| describe '#tree' do
expect(repository).to receive(key) context 'using a non-existing repository' do
before do
allow(repository).to receive(:head_commit).and_return(nil)
end
it 'returns nil' do
expect(repository.tree(:head)).to be_nil
end end
repository.build_cache it 'returns nil when using a path' do
expect(repository.tree(:head, 'README.md')).to be_nil
end
end end
it 'does not build any caches that already exist' do context 'using an existing repository' do
cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags it 'returns a Tree' do
expect(repository.tree(:head)).to be_an_instance_of(Tree)
end
end
end
expect(cache).to receive(:exist?). describe '#size' do
exactly(cache_keys.length). context 'with a non-existing repository' do
times. it 'returns 0' do
and_return(true) expect(repository).to receive(:exists?).and_return(false)
cache_keys.each do |key| expect(repository.size).to eq(0.0)
expect(repository).not_to receive(key)
end end
end
repository.build_cache context 'with an existing repository' do
it 'returns the repository size as a Float' do
expect(repository.size).to be_an_instance_of(Float)
end
end end
end end
describe "#keep_around" do describe '#commit_count' do
it "does not fail if we attempt to reference bad commit" do context 'with a non-existing repository' do
expect(repository.kept_around?('abc1234')).to be_falsey it 'returns 0' do
expect(repository).to receive(:root_ref).and_return(nil)
expect(repository.commit_count).to eq(0)
end
end end
it "stores a reference to the specified commit sha so it isn't garbage collected" do context 'with an existing repository' do
repository.keep_around(sample_commit.id) it 'returns the commit count' do
expect(repository.commit_count).to be_an_instance_of(Fixnum)
end
end
end
expect(repository.kept_around?(sample_commit.id)).to be_truthy describe '#cache_method_output', caching: true do
context 'with a non-existing repository' do
let(:value) do
repository.cache_method_output(:cats, fallback: 10) do
raise Rugged::ReferenceError
end
end
it 'returns a fallback value' do
expect(value).to eq(10)
end
it 'does not cache the data' do
value
expect(repository.instance_variable_defined?(:@cats)).to eq(false)
expect(repository.send(:cache).exist?(:cats)).to eq(false)
end
end end
it "attempting to call keep_around on truncated ref does not fail" do context 'with an existing repository' do
repository.keep_around(sample_commit.id) it 'caches the output' do
ref = repository.send(:keep_around_ref_name, sample_commit.id) object = double
path = File.join(repository.path, ref)
# Corrupt the reference
File.truncate(path, 0)
expect(repository.kept_around?(sample_commit.id)).to be_falsey expect(object).to receive(:number).once.and_return(10)
repository.keep_around(sample_commit.id) 2.times do
val = repository.cache_method_output(:cats) { object.number }
expect(repository.kept_around?(sample_commit.id)).to be_falsey expect(val).to eq(10)
end
File.delete(path) expect(repository.send(:cache).exist?(:cats)).to eq(true)
expect(repository.instance_variable_get(:@cats)).to eq(10)
end
end end
end end
describe '#update_ref!' do describe '#refresh_method_caches' do
it 'can create a ref' do it 'refreshes the caches of the given types' do
repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) expect(repository).to receive(:expire_method_caches).
with(%i(readme license_blob license_key))
expect(repository.find_branch('foobar')).not_to be_nil expect(repository).to receive(:readme)
end expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key)
it 'raises CommitError when the ref update fails' do repository.refresh_method_caches(%i(readme license))
expect do
repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
end.to raise_error(Repository::CommitError)
end end
end end
end end
...@@ -14,7 +14,7 @@ describe API::API, api: true do ...@@ -14,7 +14,7 @@ describe API::API, api: true do
describe "GET /projects/:id/repository/branches" do describe "GET /projects/:id/repository/branches" do
it "returns an array of project branches" do it "returns an array of project branches" do
project.repository.expire_cache project.repository.expire_all_method_caches
get api("/projects/#{project.id}/repository/branches", user) get api("/projects/#{project.id}/repository/branches", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
......
...@@ -3,10 +3,12 @@ require 'rails_helper' ...@@ -3,10 +3,12 @@ require 'rails_helper'
describe API::API, api: true do describe API::API, api: true do
include ApiHelpers include ApiHelpers
let(:project) { create(:empty_project, :public) }
let(:admin) { create(:admin) }
describe 'GET /projects/:project_id/snippets/:id' do describe 'GET /projects/:project_id/snippets/:id' do
# TODO (rspeicher): Deprecated; remove in 9.0 # TODO (rspeicher): Deprecated; remove in 9.0
it 'always exposes expires_at as nil' do it 'always exposes expires_at as nil' do
admin = create(:admin)
snippet = create(:project_snippet, author: admin) snippet = create(:project_snippet, author: admin)
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin) get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
...@@ -17,9 +19,9 @@ describe API::API, api: true do ...@@ -17,9 +19,9 @@ describe API::API, api: true do
end end
describe 'GET /projects/:project_id/snippets/' do describe 'GET /projects/:project_id/snippets/' do
let(:user) { create(:user) }
it 'returns all snippets available to team member' do it 'returns all snippets available to team member' do
project = create(:project, :public)
user = create(:user)
project.team << [user, :developer] project.team << [user, :developer]
public_snippet = create(:project_snippet, :public, project: project) public_snippet = create(:project_snippet, :public, project: project)
internal_snippet = create(:project_snippet, :internal, project: project) internal_snippet = create(:project_snippet, :internal, project: project)
...@@ -34,8 +36,6 @@ describe API::API, api: true do ...@@ -34,8 +36,6 @@ describe API::API, api: true do
end end
it 'hides private snippets from regular user' do it 'hides private snippets from regular user' do
project = create(:project, :public)
user = create(:user)
create(:project_snippet, :private, project: project) create(:project_snippet, :private, project: project)
get api("/projects/#{project.id}/snippets/", user) get api("/projects/#{project.id}/snippets/", user)
...@@ -45,16 +45,16 @@ describe API::API, api: true do ...@@ -45,16 +45,16 @@ describe API::API, api: true do
end end
describe 'POST /projects/:project_id/snippets/' do describe 'POST /projects/:project_id/snippets/' do
it 'creates a new snippet' do let(:params) do
admin = create(:admin) {
project = create(:project)
params = {
title: 'Test Title', title: 'Test Title',
file_name: 'test.rb', file_name: 'test.rb',
code: 'puts "hello world"', code: 'puts "hello world"',
visibility_level: Gitlab::VisibilityLevel::PUBLIC visibility_level: Gitlab::VisibilityLevel::PUBLIC
} }
end
it 'creates a new snippet' do
post api("/projects/#{project.id}/snippets/", admin), params post api("/projects/#{project.id}/snippets/", admin), params
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
...@@ -64,12 +64,20 @@ describe API::API, api: true do ...@@ -64,12 +64,20 @@ describe API::API, api: true do
expect(snippet.file_name).to eq(params[:file_name]) expect(snippet.file_name).to eq(params[:file_name])
expect(snippet.visibility_level).to eq(params[:visibility_level]) expect(snippet.visibility_level).to eq(params[:visibility_level])
end end
it 'returns 400 for missing parameters' do
params.delete(:title)
post api("/projects/#{project.id}/snippets/", admin), params
expect(response).to have_http_status(400)
end
end end
describe 'PUT /projects/:project_id/snippets/:id/' do describe 'PUT /projects/:project_id/snippets/:id/' do
let(:snippet) { create(:project_snippet, author: admin) }
it 'updates snippet' do it 'updates snippet' do
admin = create(:admin)
snippet = create(:project_snippet, author: admin)
new_content = 'New content' new_content = 'New content'
put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content
...@@ -78,9 +86,24 @@ describe API::API, api: true do ...@@ -78,9 +86,24 @@ describe API::API, api: true do
snippet.reload snippet.reload
expect(snippet.content).to eq(new_content) expect(snippet.content).to eq(new_content)
end end
it 'returns 404 for invalid snippet id' do
put api("/projects/#{snippet.project.id}/snippets/1234", admin), title: 'foo'
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it 'returns 400 for missing parameters' do
put api("/projects/#{project.id}/snippets/1234", admin)
expect(response).to have_http_status(400)
end
end end
describe 'DELETE /projects/:project_id/snippets/:id/' do describe 'DELETE /projects/:project_id/snippets/:id/' do
let(:snippet) { create(:project_snippet, author: admin) }
it 'deletes snippet' do it 'deletes snippet' do
admin = create(:admin) admin = create(:admin)
snippet = create(:project_snippet, author: admin) snippet = create(:project_snippet, author: admin)
...@@ -89,18 +112,31 @@ describe API::API, api: true do ...@@ -89,18 +112,31 @@ describe API::API, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns 404 for invalid snippet id' do
delete api("/projects/#{snippet.project.id}/snippets/1234", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end end
describe 'GET /projects/:project_id/snippets/:id/raw' do describe 'GET /projects/:project_id/snippets/:id/raw' do
it 'returns raw text' do let(:snippet) { create(:project_snippet, author: admin) }
admin = create(:admin)
snippet = create(:project_snippet, author: admin)
it 'returns raw text' do
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin) get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.content_type).to eq 'text/plain' expect(response.content_type).to eq 'text/plain'
expect(response.body).to eq(snippet.content) expect(response.body).to eq(snippet.content)
end end
it 'returns 404 for invalid snippet id' do
delete api("/projects/#{snippet.project.id}/snippets/1234", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end end
end end
...@@ -17,6 +17,10 @@ describe Ci::API::API do ...@@ -17,6 +17,10 @@ describe Ci::API::API do
let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' } let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' }
before do
stub_container_registry_config(enabled: false)
end
shared_examples 'no builds available' do shared_examples 'no builds available' do
context 'when runner sends version in User-Agent' do context 'when runner sends version in User-Agent' do
context 'for stable version' do context 'for stable version' do
...@@ -53,6 +57,41 @@ describe Ci::API::API do ...@@ -53,6 +57,41 @@ describe Ci::API::API do
it 'updates runner info' do it 'updates runner info' do
expect { register_builds }.to change { runner.reload.contacted_at } expect { register_builds }.to change { runner.reload.contacted_at }
end end
context 'registry credentials' do
let(:registry_credentials) do
{ 'type' => 'registry',
'url' => 'registry.example.com:5005',
'username' => 'gitlab-ci-token',
'password' => build.token }
end
context 'when registry is enabled' do
before do
stub_container_registry_config(enabled: true, host_port: 'registry.example.com:5005')
end
it 'sends registry credentials key' do
register_builds info: { platform: :darwin }
expect(json_response).to have_key('credentials')
expect(json_response['credentials']).to include(registry_credentials)
end
end
context 'when registry is disabled' do
before do
stub_container_registry_config(enabled: false, host_port: 'registry.example.com:5005')
end
it 'does not send registry credentials' do
register_builds info: { platform: :darwin }
expect(json_response).to have_key('credentials')
expect(json_response['credentials']).not_to include(registry_credentials)
end
end
end
end end
context 'when builds are finished' do context 'when builds are finished' do
......
...@@ -261,7 +261,7 @@ describe "Authentication", "routing" do ...@@ -261,7 +261,7 @@ describe "Authentication", "routing" do
end end
describe "Groups", "routing" do describe "Groups", "routing" do
let(:name) { 'complex.group-name' } let(:name) { 'complex.group-namegit' }
it "to #show" do it "to #show" do
expect(get("/groups/#{name}")).to route_to('groups#show', id: name) expect(get("/groups/#{name}")).to route_to('groups#show', id: name)
......
...@@ -27,27 +27,14 @@ describe GitPushService, services: true do ...@@ -27,27 +27,14 @@ describe GitPushService, services: true do
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
it 'flushes general cached data' do it 'calls the after_push_commit hook' do
expect(project.repository).to receive(:expire_cache). expect(project.repository).to receive(:after_push_commit).with('master')
with('master', newrev)
subject subject
end end
it 'flushes the visible content cache' do it 'calls the after_create_branch hook' do
expect(project.repository).to receive(:expire_has_visible_content_cache) expect(project.repository).to receive(:after_create_branch)
subject
end
it 'flushes the branches cache' do
expect(project.repository).to receive(:expire_branches_cache)
subject
end
it 'flushes the branch count cache' do
expect(project.repository).to receive(:expire_branch_count_cache)
subject subject
end end
...@@ -56,21 +43,8 @@ describe GitPushService, services: true do ...@@ -56,21 +43,8 @@ describe GitPushService, services: true do
context 'existing branch' do context 'existing branch' do
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
it 'flushes general cached data' do it 'calls the after_push_commit hook' do
expect(project.repository).to receive(:expire_cache). expect(project.repository).to receive(:after_push_commit).with('master')
with('master', newrev)
subject
end
it 'does not flush the branches cache' do
expect(project.repository).not_to receive(:expire_branches_cache)
subject
end
it 'does not flush the branch count cache' do
expect(project.repository).not_to receive(:expire_branch_count_cache)
subject subject
end end
...@@ -81,27 +55,14 @@ describe GitPushService, services: true do ...@@ -81,27 +55,14 @@ describe GitPushService, services: true do
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
it 'flushes the visible content cache' do it 'calls the after_push_commit hook' do
expect(project.repository).to receive(:expire_has_visible_content_cache) expect(project.repository).to receive(:after_push_commit).with('master')
subject
end
it 'flushes the branches cache' do
expect(project.repository).to receive(:expire_branches_cache)
subject
end
it 'flushes the branch count cache' do
expect(project.repository).to receive(:expire_branch_count_cache)
subject subject
end end
it 'flushes general cached data' do it 'calls the after_remove_branch hook' do
expect(project.repository).to receive(:expire_cache). expect(project.repository).to receive(:after_remove_branch)
with('master', newrev)
subject subject
end end
...@@ -598,6 +559,51 @@ describe GitPushService, services: true do ...@@ -598,6 +559,51 @@ describe GitPushService, services: true do
end end
end end
describe '#update_caches' do
let(:service) do
described_class.new(project,
user,
oldrev: sample_commit.parent_id,
newrev: sample_commit.id,
ref: 'refs/heads/master')
end
context 'on the default branch' do
before do
allow(service).to receive(:is_default_branch?).and_return(true)
end
it 'flushes the caches of any special files that have been changed' do
commit = double(:commit)
diff = double(:diff, new_path: 'README.md')
expect(commit).to receive(:raw_diffs).with(deltas_only: true).
and_return([diff])
service.push_commits = [commit]
expect(ProjectCacheWorker).to receive(:perform_async).
with(project.id, %i(readme))
service.update_caches
end
end
context 'on a non-default branch' do
before do
allow(service).to receive(:is_default_branch?).and_return(false)
end
it 'does not flush any conditional caches' do
expect(ProjectCacheWorker).to receive(:perform_async).
with(project.id, []).
and_call_original
service.update_caches
end
end
end
def execute_service(project, user, oldrev, newrev, ref) def execute_service(project, user, oldrev, newrev, ref)
service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref ) service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref )
service.execute service.execute
......
...@@ -18,7 +18,7 @@ describe GitTagPushService, services: true do ...@@ -18,7 +18,7 @@ describe GitTagPushService, services: true do
end end
it 'flushes general cached data' do it 'flushes general cached data' do
expect(project.repository).to receive(:expire_cache) expect(project.repository).to receive(:before_push_tag)
subject subject
end end
...@@ -28,12 +28,6 @@ describe GitTagPushService, services: true do ...@@ -28,12 +28,6 @@ describe GitTagPushService, services: true do
subject subject
end end
it 'flushes the tag count cache' do
expect(project.repository).to receive(:expire_tag_count_cache)
subject
end
end end
describe "Git Tag Push Data" do describe "Git Tag Push Data" do
......
...@@ -227,16 +227,6 @@ describe MergeRequests::RefreshService, services: true do ...@@ -227,16 +227,6 @@ describe MergeRequests::RefreshService, services: true do
end end
end end
context 'when the source branch is deleted' do
it 'does not create a MergeRequestDiff record' do
refresh_service = service.new(@project, @user)
expect do
refresh_service.execute(@oldrev, Gitlab::Git::BLANK_SHA, 'refs/heads/master')
end.not_to change { MergeRequestDiff.count }
end
end
def reload_mrs def reload_mrs
@merge_request.reload @merge_request.reload
@fork_merge_request.reload @fork_merge_request.reload
......
...@@ -543,7 +543,10 @@ describe SystemNoteService, services: true do ...@@ -543,7 +543,10 @@ describe SystemNoteService, services: true do
let(:comment_url) { jira_api_comment_url(jira_issue.id) } let(:comment_url) { jira_api_comment_url(jira_issue.id) }
let(:success_message) { "JiraService SUCCESS: Successfully posted to http://jira.example.net." } let(:success_message) { "JiraService SUCCESS: Successfully posted to http://jira.example.net." }
before { stub_jira_urls(jira_issue.id) } before do
stub_jira_urls(jira_issue.id)
jira_service_settings
end
noteable_types = ["merge_requests", "commit"] noteable_types = ["merge_requests", "commit"]
...@@ -569,16 +572,16 @@ describe SystemNoteService, services: true do ...@@ -569,16 +572,16 @@ describe SystemNoteService, services: true do
end end
end end
context 'in JIRA issue tracker' do describe "new reference" do
before { jira_service_settings } context 'for commits' do
it "creates comment" do
describe "new reference" do result = described_class.cross_reference(jira_issue, commit, author)
subject { described_class.cross_reference(jira_issue, commit, author) }
it { is_expected.to eq(success_message) } expect(result).to eq(success_message)
end
it "creates remote link" do it "creates remote link" do
subject described_class.cross_reference(jira_issue, commit, author)
expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with( expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
body: hash_including( body: hash_including(
...@@ -593,18 +596,18 @@ describe SystemNoteService, services: true do ...@@ -593,18 +596,18 @@ describe SystemNoteService, services: true do
).once ).once
end end
end end
end
context 'in commit' do context 'for issues' do
context 'in JIRA issue tracker' do let(:issue) { create(:issue, project: project) }
before { jira_service_settings }
subject { described_class.cross_reference(jira_issue, issue, author) } it "creates comment" do
result = described_class.cross_reference(jira_issue, issue, author)
it { is_expected.to eq(success_message) } expect(result).to eq(success_message)
end
it "creates remote link" do it "creates remote link" do
subject described_class.cross_reference(jira_issue, issue, author)
expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with( expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
body: hash_including( body: hash_including(
...@@ -619,6 +622,32 @@ describe SystemNoteService, services: true do ...@@ -619,6 +622,32 @@ describe SystemNoteService, services: true do
).once ).once
end end
end end
context 'for snippets' do
let(:snippet) { create(:snippet, project: project) }
it "creates comment" do
result = described_class.cross_reference(jira_issue, snippet, author)
expect(result).to eq(success_message)
end
it "creates remote link" do
described_class.cross_reference(jira_issue, snippet, author)
expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
body: hash_including(
GlobalID: "GitLab",
object: {
url: namespace_project_snippet_url(project.namespace, project, snippet),
title: "GitLab: Mentioned on snippet - #{snippet.title}",
icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
status: { resolved: false }
}
)
).once
end
end
end end
describe "existing reference" do describe "existing reference" do
...@@ -627,9 +656,11 @@ describe SystemNoteService, services: true do ...@@ -627,9 +656,11 @@ describe SystemNoteService, services: true do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)]) allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)])
end end
subject { described_class.cross_reference(jira_issue, commit, author) } it "does not return success message" do
result = described_class.cross_reference(jira_issue, commit, author)
it { is_expected.not_to eq(success_message) } expect(result).not_to eq(success_message)
end
it 'does not try to create comment and remote link' do it 'does not try to create comment and remote link' do
subject subject
......
...@@ -2,62 +2,78 @@ require 'spec_helper' ...@@ -2,62 +2,78 @@ require 'spec_helper'
describe ProjectCacheWorker do describe ProjectCacheWorker do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:worker) { described_class.new }
subject { described_class.new } describe '#perform' do
before do
describe '.perform_async' do allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
it 'schedules the job when no lease exists' do and_return(true)
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?). end
and_return(false)
expect_any_instance_of(described_class).to receive(:perform) context 'with a non-existing project' do
it 'does nothing' do
expect(worker).not_to receive(:update_repository_size)
described_class.perform_async(project.id) worker.perform(-1)
end
end end
it 'does not schedule the job when a lease exists' do context 'with an existing project without a repository' do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?). it 'does nothing' do
and_return(true) allow_any_instance_of(Repository).to receive(:exists?).and_return(false)
expect_any_instance_of(described_class).not_to receive(:perform) expect(worker).not_to receive(:update_repository_size)
described_class.perform_async(project.id) worker.perform(project.id)
end
end end
end
describe '#perform' do context 'with an existing project' do
context 'when an exclusive lease can be obtained' do it 'updates the repository size' do
before do expect(worker).to receive(:update_repository_size).and_call_original
allow(subject).to receive(:try_obtain_lease_for).with(project.id).
and_return(true)
end
it 'updates project cache data' do worker.perform(project.id)
expect_any_instance_of(Repository).to receive(:size) end
expect_any_instance_of(Repository).to receive(:commit_count)
expect_any_instance_of(Project).to receive(:update_repository_size) it 'updates the commit count' do
expect_any_instance_of(Project).to receive(:update_commit_count) expect_any_instance_of(Project).to receive(:update_commit_count).
and_call_original
subject.perform(project.id) worker.perform(project.id)
end end
it 'handles missing repository data' do it 'refreshes the method caches' do
expect_any_instance_of(Repository).to receive(:exists?).and_return(false) expect_any_instance_of(Repository).to receive(:refresh_method_caches).
expect_any_instance_of(Repository).not_to receive(:size) with(%i(readme)).
and_call_original
subject.perform(project.id) worker.perform(project.id, %i(readme))
end end
end end
end
context 'when an exclusive lease can not be obtained' do describe '#update_repository_size' do
it 'does nothing' do context 'when a lease could not be obtained' do
allow(subject).to receive(:try_obtain_lease_for).with(project.id). it 'does not update the repository size' do
allow(worker).to receive(:try_obtain_lease_for).
with(project.id, :update_repository_size).
and_return(false) and_return(false)
expect(subject).not_to receive(:update_caches) expect(project).not_to receive(:update_repository_size)
worker.update_repository_size(project)
end
end
context 'when a lease could be obtained' do
it 'updates the repository size' do
allow(worker).to receive(:try_obtain_lease_for).
with(project.id, :update_repository_size).
and_return(true)
expect(project).to receive(:update_repository_size).and_call_original
subject.perform(project.id) worker.update_repository_size(project)
end end
end end
end end
......
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