Merge branch 'master' into go-go-gadget-webpack
* master: (67 commits) Add some API endpoints for time tracking. use destructuring syntax instead add changelog yml file correct User_agent placement in robots.txt Fixing typo Fix Project#update_repository_size to convert MB to Bytes properly Remove repository trait from factories that don't need it in features Add the `:repository` trait to `:project` factories in Cucumber steps Add a `:repository` trait to the `:empty_project` factory Update clipboard_button text: Copy commit SHA to clipboard Fix search bar filter dropdown scrollbars get rid of log fix UI behaviour - only make new calls when button is clicked and dropdown is not displayed better UI fix - simple solution Disable all cops in .rubocop_todo.yml fix spec refactored a bunch of stuff based on feedback fix serializer fix bug retrieving medians fix specs ...
This commit is contained in:
commit
0d2ae3e7c1
|
@ -62,7 +62,7 @@ Lint/UnusedMethodArgument:
|
|||
# Offense count: 93
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/BlockLength:
|
||||
Max: 288
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 3
|
||||
# Cop supports --auto-correct.
|
||||
|
@ -125,7 +125,7 @@ RSpec/MessageSpies:
|
|||
|
||||
# Offense count: 3036
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 37
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 2133
|
||||
RSpec/NamedSubject:
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
/* global ResolveCount */
|
||||
|
||||
function requireAll(context) { return context.keys().map(context); }
|
||||
|
||||
window.Vue = require('vue');
|
||||
window.Vue.use(require('vue-resource'));
|
||||
requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/));
|
||||
requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/));
|
||||
requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/));
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
require('./time_tracking/time_tracking_bundle');
|
|
@ -0,0 +1,41 @@
|
|||
/* global Vue */
|
||||
require('../../../lib/utils/pretty_time');
|
||||
|
||||
(() => {
|
||||
Vue.component('time-tracking-collapsed-state', {
|
||||
name: 'time-tracking-collapsed-state',
|
||||
props: [
|
||||
'showComparisonState',
|
||||
'showSpentOnlyState',
|
||||
'showEstimateOnlyState',
|
||||
'showNoTimeTrackingState',
|
||||
'timeSpentHumanReadable',
|
||||
'timeEstimateHumanReadable',
|
||||
'stopwatchSvg',
|
||||
],
|
||||
methods: {
|
||||
abbreviateTime(timeStr) {
|
||||
return gl.utils.prettyTime.abbreviateTime(timeStr);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class='sidebar-collapsed-icon'>
|
||||
<div v-html='stopwatchSvg'></div>
|
||||
<div class='time-tracking-collapsed-summary'>
|
||||
<div class='compare' v-if='showComparisonState'>
|
||||
<span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
|
||||
</div>
|
||||
<div class='estimate-only' v-if='showEstimateOnlyState'>
|
||||
<span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
|
||||
</div>
|
||||
<div class='spend-only' v-if='showSpentOnlyState'>
|
||||
<span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span>
|
||||
</div>
|
||||
<div class='no-tracking' v-if='showNoTimeTrackingState'>
|
||||
<span class='no-value'>None</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,69 @@
|
|||
/* global Vue */
|
||||
require('../../../lib/utils/pretty_time');
|
||||
|
||||
(() => {
|
||||
const prettyTime = gl.utils.prettyTime;
|
||||
|
||||
Vue.component('time-tracking-comparison-pane', {
|
||||
name: 'time-tracking-comparison-pane',
|
||||
props: [
|
||||
'timeSpent',
|
||||
'timeEstimate',
|
||||
'timeSpentHumanReadable',
|
||||
'timeEstimateHumanReadable',
|
||||
],
|
||||
computed: {
|
||||
parsedRemaining() {
|
||||
const diffSeconds = this.timeEstimate - this.timeSpent;
|
||||
return prettyTime.parseSeconds(diffSeconds);
|
||||
},
|
||||
timeRemainingHumanReadable() {
|
||||
return prettyTime.stringifyTime(this.parsedRemaining);
|
||||
},
|
||||
timeRemainingTooltip() {
|
||||
const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
|
||||
return `${prefix} ${this.timeRemainingHumanReadable}`;
|
||||
},
|
||||
/* Diff values for comparison meter */
|
||||
timeRemainingMinutes() {
|
||||
return this.timeEstimate - this.timeSpent;
|
||||
},
|
||||
timeRemainingPercent() {
|
||||
return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`;
|
||||
},
|
||||
timeRemainingStatusClass() {
|
||||
return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
|
||||
},
|
||||
/* Parsed time values */
|
||||
parsedEstimate() {
|
||||
return prettyTime.parseSeconds(this.timeEstimate);
|
||||
},
|
||||
parsedSpent() {
|
||||
return prettyTime.parseSeconds(this.timeSpent);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class='time-tracking-comparison-pane'>
|
||||
<div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay'
|
||||
:aria-valuenow='timeRemainingTooltip'
|
||||
:title='timeRemainingTooltip'
|
||||
:data-original-title='timeRemainingTooltip'
|
||||
:class='timeRemainingStatusClass'>
|
||||
<div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'>
|
||||
<div :style='{ width: timeRemainingPercent }' class='meter-fill'></div>
|
||||
</div>
|
||||
<div class='compare-display-container'>
|
||||
<div class='compare-display pull-left'>
|
||||
<span class='compare-label'>Spent</span>
|
||||
<span class='compare-value spent'>{{ timeSpentHumanReadable }}</span>
|
||||
</div>
|
||||
<div class='compare-display estimated pull-right'>
|
||||
<span class='compare-label'>Est</span>
|
||||
<span class='compare-value'>{{ timeEstimateHumanReadable }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,13 @@
|
|||
/* global Vue */
|
||||
(() => {
|
||||
Vue.component('time-tracking-estimate-only-pane', {
|
||||
name: 'time-tracking-estimate-only-pane',
|
||||
props: ['timeEstimateHumanReadable'],
|
||||
template: `
|
||||
<div class='time-tracking-estimate-only-pane'>
|
||||
<span class='bold'>Estimated:</span>
|
||||
{{ timeEstimateHumanReadable }}
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,24 @@
|
|||
/* global Vue */
|
||||
(() => {
|
||||
Vue.component('time-tracking-help-state', {
|
||||
name: 'time-tracking-help-state',
|
||||
props: ['docsUrl'],
|
||||
template: `
|
||||
<div class='time-tracking-help-state'>
|
||||
<div class='time-tracking-info'>
|
||||
<h4>Track time with slash commands</h4>
|
||||
<p>Slash commands can be used in the issues description and comment boxes.</p>
|
||||
<p>
|
||||
<code>/estimate</code>
|
||||
will update the estimated time with the latest command.
|
||||
</p>
|
||||
<p>
|
||||
<code>/spend</code>
|
||||
will update the sum of the time spent.
|
||||
</p>
|
||||
<a class='btn btn-default learn-more-button' :href='docsUrl'>Learn more</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,11 @@
|
|||
/* global Vue */
|
||||
(() => {
|
||||
Vue.component('time-tracking-no-tracking-pane', {
|
||||
name: 'time-tracking-no-tracking-pane',
|
||||
template: `
|
||||
<div class='time-tracking-no-tracking-pane'>
|
||||
<span class='no-value'>No estimate or time spent</span>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,13 @@
|
|||
/* global Vue */
|
||||
(() => {
|
||||
Vue.component('time-tracking-spent-only-pane', {
|
||||
name: 'time-tracking-spent-only-pane',
|
||||
props: ['timeSpentHumanReadable'],
|
||||
template: `
|
||||
<div class='time-tracking-spend-only-pane'>
|
||||
<span class='bold'>Spent:</span>
|
||||
{{ timeSpentHumanReadable }}
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,119 @@
|
|||
/* global Vue */
|
||||
|
||||
require('./help_state');
|
||||
require('./collapsed_state');
|
||||
require('./spent_only_pane');
|
||||
require('./no_tracking_pane');
|
||||
require('./estimate_only_pane');
|
||||
require('./comparison_pane');
|
||||
|
||||
(() => {
|
||||
Vue.component('issuable-time-tracker', {
|
||||
name: 'issuable-time-tracker',
|
||||
props: [
|
||||
'time_estimate',
|
||||
'time_spent',
|
||||
'human_time_estimate',
|
||||
'human_time_spent',
|
||||
'stopwatchSvg',
|
||||
'docsUrl',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
showHelp: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
timeSpent() {
|
||||
return this.time_spent;
|
||||
},
|
||||
timeEstimate() {
|
||||
return this.time_estimate;
|
||||
},
|
||||
timeEstimateHumanReadable() {
|
||||
return this.human_time_estimate;
|
||||
},
|
||||
timeSpentHumanReadable() {
|
||||
return this.human_time_spent;
|
||||
},
|
||||
hasTimeSpent() {
|
||||
return !!this.timeSpent;
|
||||
},
|
||||
hasTimeEstimate() {
|
||||
return !!this.timeEstimate;
|
||||
},
|
||||
showComparisonState() {
|
||||
return this.hasTimeEstimate && this.hasTimeSpent;
|
||||
},
|
||||
showEstimateOnlyState() {
|
||||
return this.hasTimeEstimate && !this.hasTimeSpent;
|
||||
},
|
||||
showSpentOnlyState() {
|
||||
return this.hasTimeSpent && !this.hasTimeEstimate;
|
||||
},
|
||||
showNoTimeTrackingState() {
|
||||
return !this.hasTimeEstimate && !this.hasTimeSpent;
|
||||
},
|
||||
showHelpState() {
|
||||
return !!this.showHelp;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleHelpState(show) {
|
||||
this.showHelp = show;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class='time_tracker time-tracking-component-wrap' v-cloak>
|
||||
<time-tracking-collapsed-state
|
||||
:show-comparison-state='showComparisonState'
|
||||
:show-help-state='showHelpState'
|
||||
:show-spent-only-state='showSpentOnlyState'
|
||||
:show-estimate-only-state='showEstimateOnlyState'
|
||||
:time-spent-human-readable='timeSpentHumanReadable'
|
||||
:time-estimate-human-readable='timeEstimateHumanReadable'
|
||||
:stopwatch-svg='stopwatchSvg'>
|
||||
</time-tracking-collapsed-state>
|
||||
<div class='title hide-collapsed'>
|
||||
Time tracking
|
||||
<div class='help-button pull-right'
|
||||
v-if='!showHelpState'
|
||||
@click='toggleHelpState(true)'>
|
||||
<i class='fa fa-question-circle'></i>
|
||||
</div>
|
||||
<div class='close-help-button pull-right'
|
||||
v-if='showHelpState'
|
||||
@click='toggleHelpState(false)'>
|
||||
<i class='fa fa-close'></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class='time-tracking-content hide-collapsed'>
|
||||
<time-tracking-estimate-only-pane
|
||||
v-if='showEstimateOnlyState'
|
||||
:time-estimate-human-readable='timeEstimateHumanReadable'>
|
||||
</time-tracking-estimate-only-pane>
|
||||
<time-tracking-spent-only-pane
|
||||
v-if='showSpentOnlyState'
|
||||
:time-spent-human-readable='timeSpentHumanReadable'>
|
||||
</time-tracking-spent-only-pane>
|
||||
<time-tracking-no-tracking-pane
|
||||
v-if='showNoTimeTrackingState'>
|
||||
</time-tracking-no-tracking-pane>
|
||||
<time-tracking-comparison-pane
|
||||
v-if='showComparisonState'
|
||||
:time-estimate='timeEstimate'
|
||||
:time-spent='timeSpent'
|
||||
:time-spent-human-readable='timeSpentHumanReadable'
|
||||
:time-estimate-human-readable='timeEstimateHumanReadable'>
|
||||
</time-tracking-comparison-pane>
|
||||
<transition name='help-state-toggle'>
|
||||
<time-tracking-help-state
|
||||
v-if='showHelpState'
|
||||
:docs-url='docsUrl'>
|
||||
</time-tracking-help-state>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,62 @@
|
|||
/* global Vue */
|
||||
|
||||
require('./components/time_tracker');
|
||||
require('../../smart_interval');
|
||||
require('../../subbable_resource');
|
||||
|
||||
(() => {
|
||||
/* This Vue instance represents what will become the parent instance for the
|
||||
* sidebar. It will be responsible for managing `issuable` state and propagating
|
||||
* changes to sidebar components. We will want to create a separate service to
|
||||
* interface with the server at that point.
|
||||
*/
|
||||
|
||||
class IssuableTimeTracking {
|
||||
constructor(issuableJSON) {
|
||||
const parsedIssuable = JSON.parse(issuableJSON);
|
||||
return this.initComponent(parsedIssuable);
|
||||
}
|
||||
|
||||
initComponent(parsedIssuable) {
|
||||
this.parentInstance = new Vue({
|
||||
el: '#issuable-time-tracker',
|
||||
data: {
|
||||
issuable: parsedIssuable,
|
||||
},
|
||||
methods: {
|
||||
fetchIssuable() {
|
||||
return gl.IssuableResource.get.call(gl.IssuableResource, {
|
||||
type: 'GET',
|
||||
url: gl.IssuableResource.endpoint,
|
||||
});
|
||||
},
|
||||
updateState(data) {
|
||||
this.issuable = data;
|
||||
},
|
||||
subscribeToUpdates() {
|
||||
gl.IssuableResource.subscribe(data => this.updateState(data));
|
||||
},
|
||||
listenForSlashCommands() {
|
||||
$(document).on('ajax:success', '.gfm-form', (e, data) => {
|
||||
const subscribedCommands = ['spend_time', 'time_estimate'];
|
||||
const changedCommands = data.commands_changes;
|
||||
|
||||
if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
|
||||
this.fetchIssuable();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchIssuable();
|
||||
},
|
||||
mounted() {
|
||||
this.subscribeToUpdates();
|
||||
this.listenForSlashCommands();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
gl.IssuableTimeTracking = IssuableTimeTracking;
|
||||
})(window.gl || (window.gl = {}));
|
|
@ -4,13 +4,13 @@
|
|||
* stringifyTime condensed or non-condensed, abbreviateTimelengths)
|
||||
* */
|
||||
|
||||
class PrettyTime {
|
||||
|
||||
const utils = window.gl.utils = gl.utils || {};
|
||||
const prettyTime = utils.prettyTime = {
|
||||
/*
|
||||
* Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
|
||||
* Seconds can be negative or positive, zero or non-zero.
|
||||
*/
|
||||
static parseSeconds(seconds) {
|
||||
parseSeconds(seconds) {
|
||||
const DAYS_PER_WEEK = 5;
|
||||
const HOURS_PER_DAY = 8;
|
||||
const MINUTES_PER_HOUR = 60;
|
||||
|
@ -24,7 +24,7 @@
|
|||
minutes: 1,
|
||||
};
|
||||
|
||||
let unorderedMinutes = PrettyTime.secondsToMinutes(seconds);
|
||||
let unorderedMinutes = prettyTime.secondsToMinutes(seconds);
|
||||
|
||||
return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
|
||||
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
|
||||
|
@ -33,35 +33,33 @@
|
|||
|
||||
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) {
|
||||
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) {
|
||||
abbreviateTime(timeStr) {
|
||||
return timeStr.split(' ')
|
||||
.filter(unitStr => unitStr.charAt(0) !== '0')[0];
|
||||
}
|
||||
},
|
||||
|
||||
static secondsToMinutes(seconds) {
|
||||
secondsToMinutes(seconds) {
|
||||
return Math.abs(seconds / 60);
|
||||
}
|
||||
}
|
||||
|
||||
gl.PrettyTime = PrettyTime;
|
||||
},
|
||||
};
|
||||
})(window.gl || (window.gl = {}));
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
window.Vue = require('vue');
|
||||
window.Vue.use(require('vue-resource'));
|
|
@ -3,6 +3,7 @@
|
|||
/* global GLForm */
|
||||
/* global Autosave */
|
||||
/* global ResolveService */
|
||||
/* global mrRefreshWidgetUrl */
|
||||
|
||||
require('./autosave');
|
||||
window.autosize = require('vendor/autosize');
|
||||
|
@ -245,6 +246,16 @@ require('vendor/task_list');
|
|||
};
|
||||
|
||||
|
||||
Notes.prototype.handleCreateChanges = function(note) {
|
||||
if (typeof note === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) {
|
||||
$.get(mrRefreshWidgetUrl);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Render note in main comments area.
|
||||
|
||||
|
@ -430,6 +441,7 @@ require('vendor/task_list');
|
|||
*/
|
||||
|
||||
Notes.prototype.addNote = function(xhr, note, status) {
|
||||
this.handleCreateChanges(note);
|
||||
return this.renderNote(note);
|
||||
};
|
||||
|
||||
|
|
|
@ -5,18 +5,19 @@
|
|||
gl.VueStage = Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
count: 0,
|
||||
builds: '',
|
||||
spinner: '<span class="fa fa-spinner fa-spin"></span>',
|
||||
};
|
||||
},
|
||||
props: ['stage', 'svgs', 'match'],
|
||||
methods: {
|
||||
fetchBuilds() {
|
||||
if (this.count > 0) return null;
|
||||
fetchBuilds(e) {
|
||||
const areaExpanded = e.currentTarget.attributes['aria-expanded'];
|
||||
|
||||
if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
|
||||
|
||||
return this.$http.get(this.stage.dropdown_path)
|
||||
.then((response) => {
|
||||
this.count += 1;
|
||||
this.builds = JSON.parse(response.body).html;
|
||||
}, () => {
|
||||
const flash = new Flash('Something went wrong on our end.');
|
||||
|
@ -39,7 +40,7 @@
|
|||
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
|
||||
},
|
||||
svg() {
|
||||
const icon = this.stage.status.icon;
|
||||
const { icon } = this.stage.status;
|
||||
const stageIcon = icon.replace(/icon/i, 'stage_icon');
|
||||
return this.svgs[this.match(stageIcon)];
|
||||
},
|
||||
|
@ -50,18 +51,25 @@
|
|||
template: `
|
||||
<div>
|
||||
<button
|
||||
@click='fetchBuilds'
|
||||
@click='fetchBuilds($event)'
|
||||
:class="triggerButtonClass"
|
||||
:title='stage.title'
|
||||
data-placement="top"
|
||||
data-toggle="dropdown"
|
||||
type="button">
|
||||
type="button"
|
||||
>
|
||||
<span v-html="svg"></span>
|
||||
<i class="fa fa-caret-down "></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
|
||||
<div class="arrow-up"></div>
|
||||
<div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" v-html="buildsOrSpinner"></div>
|
||||
<div
|
||||
@click=''
|
||||
:class="dropdownClass"
|
||||
class="js-builds-dropdown-list scrollable-menu"
|
||||
v-html="buildsOrSpinner"
|
||||
>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
|
||||
.filter-dropdown {
|
||||
max-height: 215px;
|
||||
overflow-x: scroll;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.filter-dropdown-item {
|
||||
|
@ -86,7 +86,7 @@
|
|||
text-align: left;
|
||||
padding: 8px 16px;
|
||||
text-overflow: ellipsis;
|
||||
overflow-y: hidden;
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
|
||||
.fa {
|
||||
|
|
|
@ -236,9 +236,13 @@ header.header-sidebar-pinned {
|
|||
@media (min-width: $screen-md-min) {
|
||||
padding-right: $gutter_width;
|
||||
|
||||
.merge-request-tabs-holder.affix {
|
||||
&:not(.with-overlay) .merge-request-tabs-holder.affix {
|
||||
right: $gutter_width;
|
||||
}
|
||||
|
||||
&.with-overlay .merge-request-tabs-holder.affix {
|
||||
right: $sidebar_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
||||
&.with-overlay {
|
||||
|
|
|
@ -5,7 +5,7 @@ $sidebar_collapsed_width: 62px;
|
|||
$sidebar_width: 220px;
|
||||
$gutter_collapsed_width: 62px;
|
||||
$gutter_width: 290px;
|
||||
$gutter_inner_width: 258px;
|
||||
$gutter_inner_width: 250px;
|
||||
$sidebar-transition-duration: .15s;
|
||||
$sidebar-breakpoint: 1024px;
|
||||
|
||||
|
@ -56,6 +56,7 @@ $black-transparent: rgba(0, 0, 0, 0.3);
|
|||
$border-white-light: darken($white-light, $darken-border-factor);
|
||||
$border-white-normal: darken($white-normal, $darken-border-factor);
|
||||
|
||||
$border-gray-light: darken($gray-light, $darken-border-factor);
|
||||
$border-gray-normal: darken($gray-normal, $darken-border-factor);
|
||||
$border-gray-dark: darken($white-normal, $darken-border-factor);
|
||||
|
||||
|
@ -85,6 +86,7 @@ $warning-message-border: #f0e2bb;
|
|||
*/
|
||||
$border-color: #e5e5e5;
|
||||
$focus-border-color: #3aabf0;
|
||||
$sidebar-collapsed-icon-color: #999;
|
||||
$well-expand-item: #e8f2f7;
|
||||
$well-inner-border: #eef0f2;
|
||||
$well-light-border: #f1f1f1;
|
||||
|
@ -280,6 +282,7 @@ $dropdown-hover-color: #3b86ff;
|
|||
*/
|
||||
$btn-active-gray: #ececec;
|
||||
$btn-active-gray-light: e4e7ed;
|
||||
$btn-white-active: #848484;
|
||||
|
||||
/*
|
||||
* Badges
|
||||
|
@ -433,6 +436,7 @@ $help-shortcut-header-color: #333;
|
|||
*/
|
||||
$issues-today-bg: #f3fff2;
|
||||
$issues-today-border: #e1e8d5;
|
||||
$compare-display-color: #888;
|
||||
|
||||
/*
|
||||
* jQuery UI
|
||||
|
|
|
@ -473,3 +473,102 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time_tracker {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
|
||||
|
||||
.sidebar-collapsed-icon {
|
||||
|
||||
> .stopwatch-svg {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: $sidebar-collapsed-icon-color;
|
||||
}
|
||||
|
||||
&:hover svg {
|
||||
fill: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.help-button,
|
||||
.close-help-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.compare-meter {
|
||||
&.within_estimate {
|
||||
.meter-fill {
|
||||
background: $gl-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.over_estimate {
|
||||
.meter-fill {
|
||||
background: $red-light;
|
||||
}
|
||||
|
||||
.time-remaining,
|
||||
.compare-value.spent {
|
||||
color: $red-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.meter-container {
|
||||
background: $border-gray-light;
|
||||
border-radius: 3px;
|
||||
|
||||
.meter-fill {
|
||||
max-width: 100%;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background: $gl-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.compare-display-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 5px;
|
||||
|
||||
.compare-display {
|
||||
font-size: 13px;
|
||||
color: $compare-display-color;
|
||||
|
||||
.compare-value {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time-tracking-help-state {
|
||||
background: $white-light;
|
||||
margin: 16px -20px 0;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid $border-gray-light;
|
||||
border-bottom: 1px solid $border-gray-light;
|
||||
|
||||
a:hover {
|
||||
color: $btn-white-active;
|
||||
}
|
||||
}
|
||||
|
||||
.help-state-toggle-enter-active {
|
||||
transition: all .8s ease;
|
||||
}
|
||||
|
||||
.help-state-toggle-leave-active {
|
||||
transition: all .5s ease;
|
||||
}
|
||||
|
||||
.help-state-toggle-enter,
|
||||
.help-state-toggle-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,8 +44,8 @@
|
|||
|
||||
.pipeline-info,
|
||||
.pipeline-commit,
|
||||
.pipeline-actions,
|
||||
.pipeline-stages {
|
||||
.pipeline-stages,
|
||||
.pipeline-actions {
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
|
@ -185,6 +185,7 @@
|
|||
|
||||
.stage-cell {
|
||||
font-size: 0;
|
||||
padding: 10px 4px;
|
||||
|
||||
> .stage-container > div > button > span > svg,
|
||||
> .stage-container > button > svg {
|
||||
|
@ -202,8 +203,8 @@
|
|||
position: relative;
|
||||
margin-right: 6px;
|
||||
|
||||
.tooltip {
|
||||
white-space: nowrap;
|
||||
.tooltip-inner {
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
|
@ -348,6 +349,7 @@
|
|||
padding: $gl-padding;
|
||||
white-space: nowrap;
|
||||
transition: max-height 0.3s, padding 0.3s;
|
||||
overflow: auto;
|
||||
|
||||
.stage-column-list,
|
||||
.builds-container > ul {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
module CycleAnalyticsParams
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def options(params)
|
||||
@options ||= { from: start_date(params), current_user: current_user }
|
||||
end
|
||||
|
||||
def start_date(params)
|
||||
params[:start_date] == '30' ? 30.days.ago : 90.days.ago
|
||||
end
|
||||
|
|
|
@ -25,9 +25,18 @@ class Projects::CompareController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def create
|
||||
if params[:from].blank? || params[:to].blank?
|
||||
flash[:alert] = "You must select from and to branches"
|
||||
from_to_vars = {
|
||||
from: params[:from].presence,
|
||||
to: params[:to].presence
|
||||
}
|
||||
redirect_to namespace_project_compare_index_path(@project.namespace, @project, from_to_vars)
|
||||
else
|
||||
redirect_to namespace_project_compare_path(@project.namespace, @project,
|
||||
params[:from], params[:to])
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
|
|
@ -9,56 +9,52 @@ module Projects
|
|||
before_action :authorize_read_merge_request!, only: [:code, :review]
|
||||
|
||||
def issue
|
||||
render_events(events.issue_events)
|
||||
render_events(cycle_analytics[:issue].events)
|
||||
end
|
||||
|
||||
def plan
|
||||
render_events(events.plan_events)
|
||||
render_events(cycle_analytics[:plan].events)
|
||||
end
|
||||
|
||||
def code
|
||||
render_events(events.code_events)
|
||||
render_events(cycle_analytics[:code].events)
|
||||
end
|
||||
|
||||
def test
|
||||
options[:branch] = events_params[:branch_name]
|
||||
options(events_params)[:branch] = events_params[:branch_name]
|
||||
|
||||
render_events(events.test_events)
|
||||
render_events(cycle_analytics[:test].events)
|
||||
end
|
||||
|
||||
def review
|
||||
render_events(events.review_events)
|
||||
render_events(cycle_analytics[:review].events)
|
||||
end
|
||||
|
||||
def staging
|
||||
render_events(events.staging_events)
|
||||
render_events(cycle_analytics[:staging].events)
|
||||
end
|
||||
|
||||
def production
|
||||
render_events(events.production_events)
|
||||
render_events(cycle_analytics[:production].events)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_events(events_list)
|
||||
def render_events(events)
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json { render json: { events: events_list } }
|
||||
format.json { render json: { events: events } }
|
||||
end
|
||||
end
|
||||
|
||||
def events
|
||||
@events ||= Gitlab::CycleAnalytics::Events.new(project: project, options: options)
|
||||
end
|
||||
|
||||
def options
|
||||
@options ||= { from: start_date(events_params), current_user: current_user }
|
||||
def cycle_analytics
|
||||
@cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params))
|
||||
end
|
||||
|
||||
def events_params
|
||||
return {} unless params[:events].present?
|
||||
|
||||
params[:events].slice(:start_date, :branch_name)
|
||||
params[:events].permit(:start_date, :branch_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,11 +6,9 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
|
|||
before_action :authorize_read_cycle_analytics!
|
||||
|
||||
def show
|
||||
@cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params))
|
||||
@cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params))
|
||||
|
||||
stats_values, cycle_analytics_json = generate_cycle_analytics_data
|
||||
|
||||
@cycle_analytics_no_data = stats_values.blank?
|
||||
@cycle_analytics_no_data = @cycle_analytics.no_stats?
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
@ -23,50 +21,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
|
|||
def cycle_analytics_params
|
||||
return {} unless params[:cycle_analytics].present?
|
||||
|
||||
{ start_date: params[:cycle_analytics][:start_date] }
|
||||
params[:cycle_analytics].permit(:start_date)
|
||||
end
|
||||
|
||||
def generate_cycle_analytics_data
|
||||
stats_values = []
|
||||
|
||||
cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"],
|
||||
[:plan, "Plan", "Related Commits", "Time before an issue starts implementation"],
|
||||
[:code, "Code", "Related Merge Requests", "Time spent coding"],
|
||||
[:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"],
|
||||
[: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_legend, stage_description)|
|
||||
value = @cycle_analytics.send(stage_method).presence
|
||||
|
||||
stats_values << value.abs if value
|
||||
|
||||
stats << {
|
||||
title: stage_text,
|
||||
description: stage_description,
|
||||
legend: stage_legend,
|
||||
value: value && !value.zero? ? distance_of_time_in_words(value) : nil
|
||||
}
|
||||
|
||||
stats
|
||||
end
|
||||
|
||||
issues = @cycle_analytics.summary.new_issues
|
||||
commits = @cycle_analytics.summary.commits
|
||||
deploys = @cycle_analytics.summary.deploys
|
||||
|
||||
summary = [
|
||||
{ title: "New Issue".pluralize(issues), value: issues },
|
||||
{ title: "Commit".pluralize(commits), value: commits },
|
||||
{ title: "Deploy".pluralize(deploys), value: deploys }
|
||||
]
|
||||
|
||||
cycle_analytics_hash = { summary: summary,
|
||||
stats: stats,
|
||||
def cycle_analytics_json
|
||||
{
|
||||
summary: @cycle_analytics.summary,
|
||||
stats: @cycle_analytics.stats,
|
||||
permissions: @cycle_analytics.permissions(user: current_user)
|
||||
}
|
||||
|
||||
[stats_values, cycle_analytics_hash]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -347,6 +347,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def merge_widget_refresh
|
||||
if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged'
|
||||
@status = :success
|
||||
elsif merge_request.merge_when_build_succeeds
|
||||
@status = :merge_when_build_succeeds
|
||||
end
|
||||
|
||||
render 'merge'
|
||||
end
|
||||
|
||||
def branch_from
|
||||
# This is always source
|
||||
@source_project = @merge_request.nil? ? @project : @merge_request.source_project
|
||||
|
|
|
@ -23,7 +23,8 @@ class Projects::NotesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def create
|
||||
@note = Notes::CreateService.new(project, current_user, note_params).execute
|
||||
create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha])
|
||||
@note = Notes::CreateService.new(project, current_user, create_params).execute
|
||||
|
||||
if @note.is_a?(Note)
|
||||
Banzai::NoteRenderer.render([@note], @project, current_user)
|
||||
|
|
|
@ -165,4 +165,10 @@ module DiffHelper
|
|||
|
||||
link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class]
|
||||
end
|
||||
|
||||
def render_overflow_warning?(diff_files)
|
||||
diffs = @merge_request_diff.presence || diff_files
|
||||
|
||||
diffs.overflow?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,6 +30,15 @@ module IssuablesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def serialize_issuable(issuable)
|
||||
case issuable
|
||||
when Issue
|
||||
IssueSerializer.new.represent(issuable).to_json
|
||||
when MergeRequest
|
||||
MergeRequestSerializer.new.represent(issuable).to_json
|
||||
end
|
||||
end
|
||||
|
||||
def template_dropdown_tag(issuable, &block)
|
||||
title = selected_template(issuable) || "Choose a template"
|
||||
options = {
|
||||
|
|
|
@ -19,6 +19,14 @@ module MergeRequestsHelper
|
|||
}
|
||||
end
|
||||
|
||||
def mr_widget_refresh_url(mr)
|
||||
if mr && mr.source_project
|
||||
merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr)
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
def mr_css_classes(mr)
|
||||
classes = "merge-request"
|
||||
classes << " closed" if mr.closed?
|
||||
|
|
|
@ -507,6 +507,10 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
def has_expiring_artifacts?
|
||||
artifacts_expire_at.present?
|
||||
end
|
||||
|
||||
def keep_artifacts!
|
||||
self.update(artifacts_expire_at: nil)
|
||||
end
|
||||
|
|
|
@ -318,6 +318,14 @@ class Commit
|
|||
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
|
||||
end
|
||||
|
||||
def persisted?
|
||||
true
|
||||
end
|
||||
|
||||
def touch
|
||||
# no-op but needs to be defined since #persisted? is defined
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def commit_reference(from_project, referable_commit_id, full: false)
|
||||
|
|
|
@ -13,6 +13,7 @@ module Issuable
|
|||
include StripAttribute
|
||||
include Awardable
|
||||
include Taskable
|
||||
include TimeTrackable
|
||||
|
||||
included do
|
||||
cache_markdown_field :title, pipeline: :single_line
|
||||
|
|
|
@ -7,11 +7,14 @@ module Milestoneish
|
|||
|
||||
def total_items_count(user)
|
||||
memoize_per_user(user, :total_items_count) do
|
||||
issues_count = count_issues_by_state(user).values.sum
|
||||
issues_count + merge_requests.size
|
||||
total_issues_count(user) + merge_requests.size
|
||||
end
|
||||
end
|
||||
|
||||
def total_issues_count(user)
|
||||
count_issues_by_state(user).values.sum
|
||||
end
|
||||
|
||||
def complete?(user)
|
||||
total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
# == TimeTrackable concern
|
||||
#
|
||||
# Contains functionality related to objects that support time tracking.
|
||||
#
|
||||
# Used by Issue and MergeRequest.
|
||||
#
|
||||
|
||||
module TimeTrackable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
attr_reader :time_spent, :time_spent_user
|
||||
|
||||
alias_method :time_spent?, :time_spent
|
||||
|
||||
default_value_for :time_estimate, value: 0, allows_nil: false
|
||||
|
||||
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
|
||||
validate :check_negative_time_spent
|
||||
|
||||
has_many :timelogs, as: :trackable, dependent: :destroy
|
||||
end
|
||||
|
||||
def spend_time(options)
|
||||
@time_spent = options[:duration]
|
||||
@time_spent_user = options[:user]
|
||||
@original_total_time_spent = nil
|
||||
|
||||
return if @time_spent == 0
|
||||
|
||||
if @time_spent == :reset
|
||||
reset_spent_time
|
||||
else
|
||||
add_or_subtract_spent_time
|
||||
end
|
||||
end
|
||||
alias_method :spend_time=, :spend_time
|
||||
|
||||
def total_time_spent
|
||||
timelogs.sum(:time_spent)
|
||||
end
|
||||
|
||||
def human_total_time_spent
|
||||
Gitlab::TimeTrackingFormatter.output(total_time_spent)
|
||||
end
|
||||
|
||||
def human_time_estimate
|
||||
Gitlab::TimeTrackingFormatter.output(time_estimate)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_spent_time
|
||||
timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user)
|
||||
end
|
||||
|
||||
def add_or_subtract_spent_time
|
||||
timelogs.new(time_spent: time_spent, user: @time_spent_user)
|
||||
end
|
||||
|
||||
def check_negative_time_spent
|
||||
return if time_spent.nil? || time_spent == :reset
|
||||
|
||||
# we need to cache the total time spent so multiple calls to #valid?
|
||||
# doesn't give a false error
|
||||
@original_total_time_spent ||= total_time_spent
|
||||
|
||||
if time_spent < 0 && (time_spent.abs > @original_total_time_spent)
|
||||
errors.add(:time_spent, 'Time to subtract exceeds the total time spent')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,62 +1,38 @@
|
|||
class CycleAnalytics
|
||||
STAGES = %i[issue plan code test review staging production].freeze
|
||||
|
||||
def initialize(project, current_user, from:)
|
||||
def initialize(project, options)
|
||||
@project = project
|
||||
@current_user = current_user
|
||||
@from = from
|
||||
@fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def summary
|
||||
@summary ||= Summary.new(@project, @current_user, from: @from)
|
||||
@summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
|
||||
from: @options[:from],
|
||||
current_user: @options[:current_user]).data
|
||||
end
|
||||
|
||||
def stats
|
||||
@stats ||= stats_per_stage
|
||||
end
|
||||
|
||||
def no_stats?
|
||||
stats.all? { |hash| hash[:value].nil? }
|
||||
end
|
||||
|
||||
def permissions(user:)
|
||||
Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
|
||||
end
|
||||
|
||||
def issue
|
||||
@fetcher.calculate_metric(:issue,
|
||||
Issue.arel_table[:created_at],
|
||||
[Issue::Metrics.arel_table[:first_associated_with_milestone_at],
|
||||
Issue::Metrics.arel_table[:first_added_to_board_at]])
|
||||
def [](stage_name)
|
||||
Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
|
||||
end
|
||||
|
||||
def plan
|
||||
@fetcher.calculate_metric(:plan,
|
||||
[Issue::Metrics.arel_table[:first_associated_with_milestone_at],
|
||||
Issue::Metrics.arel_table[:first_added_to_board_at]],
|
||||
Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
|
||||
end
|
||||
private
|
||||
|
||||
def code
|
||||
@fetcher.calculate_metric(:code,
|
||||
Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
|
||||
MergeRequest.arel_table[:created_at])
|
||||
end
|
||||
|
||||
def test
|
||||
@fetcher.calculate_metric(:test,
|
||||
MergeRequest::Metrics.arel_table[:latest_build_started_at],
|
||||
MergeRequest::Metrics.arel_table[:latest_build_finished_at])
|
||||
end
|
||||
|
||||
def review
|
||||
@fetcher.calculate_metric(:review,
|
||||
MergeRequest.arel_table[:created_at],
|
||||
MergeRequest::Metrics.arel_table[:merged_at])
|
||||
end
|
||||
|
||||
def staging
|
||||
@fetcher.calculate_metric(:staging,
|
||||
MergeRequest::Metrics.arel_table[:merged_at],
|
||||
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
|
||||
end
|
||||
|
||||
def production
|
||||
@fetcher.calculate_metric(:production,
|
||||
Issue.arel_table[:created_at],
|
||||
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
|
||||
def stats_per_stage
|
||||
STAGES.map do |stage_name|
|
||||
self[stage_name].as_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
class CycleAnalytics
|
||||
class Summary
|
||||
def initialize(project, current_user, from:)
|
||||
@project = project
|
||||
@current_user = current_user
|
||||
@from = from
|
||||
end
|
||||
|
||||
def new_issues
|
||||
IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count
|
||||
end
|
||||
|
||||
def commits
|
||||
ref = @project.default_branch.presence
|
||||
count_commits_for(ref)
|
||||
end
|
||||
|
||||
def deploys
|
||||
@project.deployments.where("created_at > ?", @from).count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Don't use the `Gitlab::Git::Repository#log` method, because it enforces
|
||||
# a limit. Since we need a commit count, we _can't_ enforce a limit, so
|
||||
# the easiest way forward is to replicate the relevant portions of the
|
||||
# `log` function here.
|
||||
def count_commits_for(ref)
|
||||
return unless ref
|
||||
|
||||
repository = @project.repository.raw_repository
|
||||
sha = @project.repository.commit(ref).sha
|
||||
|
||||
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{repository.path} log)
|
||||
cmd << '--format=%H'
|
||||
cmd << "--after=#{@from.iso8601}"
|
||||
cmd << sha
|
||||
|
||||
raw_output = IO.popen(cmd) { |io| io.read }
|
||||
raw_output.lines.count
|
||||
end
|
||||
end
|
||||
end
|
|
@ -898,10 +898,22 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def has_commits?
|
||||
commits_count > 0
|
||||
merge_request_diff && commits_count > 0
|
||||
end
|
||||
|
||||
def has_no_commits?
|
||||
!has_commits?
|
||||
end
|
||||
|
||||
def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
|
||||
return false unless can_be_merged_by?(current_user)
|
||||
|
||||
return true if autocomplete_precheck
|
||||
|
||||
return false unless mergeable?(skip_ci_check: true)
|
||||
return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?)
|
||||
return false if last_diff_sha != diff_head_sha
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -234,28 +234,28 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
# and save it as array of hashes in st_diffs db field
|
||||
def save_diffs
|
||||
new_attributes = {}
|
||||
new_diffs = []
|
||||
|
||||
if commits.size.zero?
|
||||
new_attributes[:state] = :empty
|
||||
else
|
||||
diff_collection = compare.diffs(Commit.max_diff_options)
|
||||
|
||||
if diff_collection.overflow?
|
||||
# Set our state to 'overflow' to make the #empty? and #collected?
|
||||
# methods (generated by StateMachine) return false.
|
||||
new_attributes[:state] = :overflow
|
||||
end
|
||||
|
||||
new_attributes[:real_size] = diff_collection.real_size
|
||||
new_attributes[:real_size] = compare.diffs.real_size
|
||||
|
||||
if diff_collection.any?
|
||||
new_diffs = dump_diffs(diff_collection)
|
||||
new_attributes[:state] = :collected
|
||||
end
|
||||
|
||||
new_attributes[:st_diffs] = new_diffs || []
|
||||
|
||||
# Set our state to 'overflow' to make the #empty? and #collected?
|
||||
# methods (generated by StateMachine) return false.
|
||||
#
|
||||
# This attribution has to come at the end of the method so 'overflow'
|
||||
# state does not get overridden by 'collected'.
|
||||
new_attributes[:state] = :overflow if diff_collection.overflow?
|
||||
end
|
||||
|
||||
new_attributes[:st_diffs] = new_diffs
|
||||
update_columns_serialized(new_attributes)
|
||||
end
|
||||
|
||||
|
|
|
@ -25,8 +25,9 @@ class ProjectStatistics < ActiveRecord::Base
|
|||
self.commit_count = project.repository.commit_count
|
||||
end
|
||||
|
||||
# Repository#size needs to be converted from MB to Byte.
|
||||
def update_repository_size
|
||||
self.repository_size = project.repository.size
|
||||
self.repository_size = project.repository.size * 1.megabyte
|
||||
end
|
||||
|
||||
def update_lfs_objects_size
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class Timelog < ActiveRecord::Base
|
||||
validates :time_spent, :user, presence: true
|
||||
|
||||
belongs_to :trackable, polymorphic: true
|
||||
belongs_to :user
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
class AnalyticsStageEntity < Grape::Entity
|
||||
include EntityDateHelper
|
||||
|
||||
expose :title
|
||||
expose :description
|
||||
|
||||
expose :median, as: :value do |stage|
|
||||
stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
class AnalyticsStageSerializer < BaseSerializer
|
||||
entity AnalyticsStageEntity
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
class AnalyticsSummaryEntity < Grape::Entity
|
||||
expose :value, safe: true
|
||||
|
||||
expose :title do |object|
|
||||
object.title.pluralize(object.value)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
class AnalyticsSummarySerializer < BaseSerializer
|
||||
entity AnalyticsSummaryEntity
|
||||
end
|
|
@ -13,4 +13,8 @@ class IssuableEntity < Grape::Entity
|
|||
expose :created_at
|
||||
expose :updated_at
|
||||
expose :deleted_at
|
||||
expose :time_estimate
|
||||
expose :total_time_spent
|
||||
expose :human_time_estimate
|
||||
expose :human_total_time_spent
|
||||
end
|
||||
|
|
|
@ -36,6 +36,14 @@ class IssuableBaseService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def create_time_estimate_note(issuable)
|
||||
SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
|
||||
end
|
||||
|
||||
def create_time_spent_note(issuable)
|
||||
SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
|
||||
end
|
||||
|
||||
def filter_params(issuable)
|
||||
ability_name = :"admin_#{issuable.to_ability_name}"
|
||||
|
||||
|
@ -272,6 +280,14 @@ class IssuableBaseService < BaseService
|
|||
create_task_status_note(issuable)
|
||||
end
|
||||
|
||||
if issuable.previous_changes.include?('time_estimate')
|
||||
create_time_estimate_note(issuable)
|
||||
end
|
||||
|
||||
if issuable.time_spent?
|
||||
create_time_spent_note(issuable)
|
||||
end
|
||||
|
||||
create_labels_note(issuable, old_labels) if issuable.labels != old_labels
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,8 @@ module MergeRequests
|
|||
params.except!(:target_project_id)
|
||||
params.except!(:source_branch)
|
||||
|
||||
merge_from_slash_command(merge_request) if params[:merge]
|
||||
|
||||
if merge_request.closed_without_fork?
|
||||
params.except!(:target_branch, :force_remove_source_branch)
|
||||
end
|
||||
|
@ -69,6 +71,19 @@ module MergeRequests
|
|||
end
|
||||
end
|
||||
|
||||
def merge_from_slash_command(merge_request)
|
||||
last_diff_sha = params.delete(:merge)
|
||||
return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha)
|
||||
|
||||
merge_request.update(merge_error: nil)
|
||||
|
||||
if merge_request.head_pipeline && merge_request.head_pipeline.active?
|
||||
MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request)
|
||||
else
|
||||
MergeWorker.perform_async(merge_request.id, current_user.id, {})
|
||||
end
|
||||
end
|
||||
|
||||
def reopen_service
|
||||
MergeRequests::ReopenService
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
module Notes
|
||||
class CreateService < BaseService
|
||||
def execute
|
||||
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
|
||||
|
||||
note = project.notes.new(params)
|
||||
note.author = current_user
|
||||
note.system = false
|
||||
|
@ -19,7 +21,8 @@ module Notes
|
|||
slash_commands_service = SlashCommandsService.new(project, current_user)
|
||||
|
||||
if slash_commands_service.supported?(note)
|
||||
content, command_params = slash_commands_service.extract_commands(note)
|
||||
options = { merge_request_diff_head_sha: merge_request_diff_head_sha }
|
||||
content, command_params = slash_commands_service.extract_commands(note, options)
|
||||
|
||||
only_commands = content.empty?
|
||||
|
||||
|
|
|
@ -19,10 +19,10 @@ module Notes
|
|||
self.class.supported?(note, current_user)
|
||||
end
|
||||
|
||||
def extract_commands(note)
|
||||
def extract_commands(note, options = {})
|
||||
return [note.note, {}] unless supported?(note)
|
||||
|
||||
SlashCommands::InterpretService.new(project, current_user).
|
||||
SlashCommands::InterpretService.new(project, current_user, options).
|
||||
execute(note.note, note.noteable)
|
||||
end
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ module SlashCommands
|
|||
class InterpretService < BaseService
|
||||
include Gitlab::SlashCommands::Dsl
|
||||
|
||||
attr_reader :issuable
|
||||
attr_reader :issuable, :options
|
||||
|
||||
# Takes a text and interprets the commands that are extracted from it.
|
||||
# Returns the content without commands, and hash of changes to be applied to a record.
|
||||
|
@ -13,7 +13,8 @@ module SlashCommands
|
|||
opts = {
|
||||
issuable: issuable,
|
||||
current_user: current_user,
|
||||
project: project
|
||||
project: project,
|
||||
params: params
|
||||
}
|
||||
|
||||
content, commands = extractor.extract_commands(content, opts)
|
||||
|
@ -58,6 +59,17 @@ module SlashCommands
|
|||
@updates[:state_event] = 'reopen'
|
||||
end
|
||||
|
||||
desc 'Merge (when build succeeds)'
|
||||
condition do
|
||||
last_diff_sha = params && params[:merge_request_diff_head_sha]
|
||||
issuable.is_a?(MergeRequest) &&
|
||||
issuable.persisted? &&
|
||||
issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha)
|
||||
end
|
||||
command :merge do
|
||||
@updates[:merge] = params[:merge_request_diff_head_sha]
|
||||
end
|
||||
|
||||
desc 'Change title'
|
||||
params '<New title>'
|
||||
condition do
|
||||
|
@ -243,6 +255,50 @@ module SlashCommands
|
|||
@updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
|
||||
end
|
||||
|
||||
desc 'Set time estimate'
|
||||
params '<1w 3d 2h 14m>'
|
||||
condition do
|
||||
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
|
||||
end
|
||||
command :estimate do |raw_duration|
|
||||
time_estimate = Gitlab::TimeTrackingFormatter.parse(raw_duration)
|
||||
|
||||
if time_estimate
|
||||
@updates[:time_estimate] = time_estimate
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Add or substract spent time'
|
||||
params '<1h 30m | -1h 30m>'
|
||||
condition do
|
||||
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
|
||||
end
|
||||
command :spend do |raw_duration|
|
||||
time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration)
|
||||
|
||||
if time_spent
|
||||
@updates[:spend_time] = { duration: time_spent, user: current_user }
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Remove time estimate'
|
||||
condition do
|
||||
issuable.persisted? &&
|
||||
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
|
||||
end
|
||||
command :remove_estimate do
|
||||
@updates[:time_estimate] = 0
|
||||
end
|
||||
|
||||
desc 'Remove spent time'
|
||||
condition do
|
||||
issuable.persisted? &&
|
||||
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
|
||||
end
|
||||
command :remove_time_spent do
|
||||
@updates[:spend_time] = { duration: :reset, user: current_user }
|
||||
end
|
||||
|
||||
# This is a dummy command, so that it appears in the autocomplete commands
|
||||
desc 'CC'
|
||||
params '@user'
|
||||
|
|
|
@ -109,6 +109,57 @@ module SystemNoteService
|
|||
create_note(noteable: noteable, project: project, author: author, note: body)
|
||||
end
|
||||
|
||||
# Called when the estimated time of a Noteable is changed
|
||||
#
|
||||
# noteable - Noteable object
|
||||
# project - Project owning noteable
|
||||
# author - User performing the change
|
||||
# time_estimate - Estimated time
|
||||
#
|
||||
# Example Note text:
|
||||
#
|
||||
# "Changed estimate of this issue to 3d 5h"
|
||||
#
|
||||
# Returns the created Note object
|
||||
|
||||
def change_time_estimate(noteable, project, author)
|
||||
parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
|
||||
body = if noteable.time_estimate == 0
|
||||
"Removed time estimate on this #{noteable.human_class_name}"
|
||||
else
|
||||
"Changed time estimate of this #{noteable.human_class_name} to #{parsed_time}"
|
||||
end
|
||||
|
||||
create_note(noteable: noteable, project: project, author: author, note: body)
|
||||
end
|
||||
|
||||
# Called when the spent time of a Noteable is changed
|
||||
#
|
||||
# noteable - Noteable object
|
||||
# project - Project owning noteable
|
||||
# author - User performing the change
|
||||
# time_spent - Spent time
|
||||
#
|
||||
# Example Note text:
|
||||
#
|
||||
# "Added 2h 30m of time spent on this issue"
|
||||
#
|
||||
# Returns the created Note object
|
||||
|
||||
def change_time_spent(noteable, project, author)
|
||||
time_spent = noteable.time_spent
|
||||
|
||||
if time_spent == :reset
|
||||
body = "Removed time spent on this #{noteable.human_class_name}"
|
||||
else
|
||||
parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
|
||||
action = time_spent > 0 ? 'Added' : 'Subtracted'
|
||||
body = "#{action} #{parsed_time} of time spent on this #{noteable.human_class_name}"
|
||||
end
|
||||
|
||||
create_note(noteable: noteable, project: project, author: author, note: body)
|
||||
end
|
||||
|
||||
# Called when the status of a Noteable is changed
|
||||
#
|
||||
# noteable - Noteable object
|
||||
|
|
|
@ -22,14 +22,14 @@
|
|||
%p.build-detail-row
|
||||
The artifacts were removed
|
||||
#{time_ago_with_tooltip(@build.artifacts_expire_at)}
|
||||
- elsif @build.artifacts_expire_at
|
||||
- elsif @build.has_expiring_artifacts?
|
||||
%p.build-detail-row
|
||||
The artifacts will be removed in
|
||||
%span.js-artifacts-remove= @build.artifacts_expire_at
|
||||
|
||||
- if @build.artifacts?
|
||||
.btn-group.btn-group-justified{ role: :group }
|
||||
- if @build.artifacts_expire_at
|
||||
- if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
|
||||
= link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
|
||||
Keep
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
%a.close{ href: "#", "data-dismiss" => "modal" } ×
|
||||
%h3.page-title== #{label} this #{commit.change_type_title(current_user)}
|
||||
.modal-body
|
||||
= form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
|
||||
= form_tag [type.underscore, @project.namespace, @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
|
||||
.form-group.branch
|
||||
= label_tag 'target_branch', target_label, class: 'control-label'
|
||||
.col-sm-10
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.page-content-header
|
||||
.header-main-content
|
||||
%strong
|
||||
= clipboard_button(clipboard_text: @commit.id)
|
||||
= clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard")
|
||||
= @commit.short_id
|
||||
%span.hidden-xs authored
|
||||
#{time_ago_with_tooltip(@commit.authored_date)}
|
||||
|
|
|
@ -36,6 +36,6 @@
|
|||
.table-list-cell.commit-actions.hidden-xs
|
||||
- if commit.status(ref)
|
||||
= render_commit_status(commit, ref: ref)
|
||||
= clipboard_button(clipboard_text: commit.id)
|
||||
= clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
|
||||
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
|
||||
= link_to_browse_code(project, commit)
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
= parallel_diff_btn
|
||||
= render 'projects/diffs/stats', diff_files: diff_files
|
||||
|
||||
- if diff_files.overflow?
|
||||
= render 'projects/diffs/warning', diff_files: diff_files
|
||||
- if render_overflow_warning?(diff_files)
|
||||
= render 'projects/diffs/warning', diff_files: diffs
|
||||
|
||||
.files{ data: { can_create_note: can_create_note } }
|
||||
- diff_files.each_with_index do |diff_file|
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
|
||||
- page_description @issue.description
|
||||
- page_card_attributes @issue.card_attributes
|
||||
- content_for :page_specific_javascripts do
|
||||
= page_specific_javascript_bundle_tag('lib_vue')
|
||||
|
||||
.clearfix.detail-page-header
|
||||
.issuable-header
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
- page_description @merge_request.description
|
||||
- page_card_attributes @merge_request.card_attributes
|
||||
- content_for :page_specific_javascripts do
|
||||
= page_specific_javascript_bundle_tag('lib_vue')
|
||||
= page_specific_javascript_bundle_tag('diff_notes')
|
||||
|
||||
.merge-request{ 'data-url' => merge_request_path(@merge_request) }
|
||||
|
@ -112,3 +113,5 @@
|
|||
merge_request = new MergeRequest({
|
||||
action: "#{controller.action_name}"
|
||||
});
|
||||
|
||||
var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}";
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
|
||||
- content_for :page_specific_javascripts do
|
||||
= page_specific_javascript_bundle_tag('lib_vue')
|
||||
= page_specific_javascript_bundle_tag('merge_conflicts')
|
||||
= page_specific_javascript_tag('lib/ace.js')
|
||||
= render "projects/merge_requests/show/mr_title"
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
- if @merge_request_diff.collected?
|
||||
- if @merge_request_diff.collected? || @merge_request_diff.overflow?
|
||||
= render 'projects/merge_requests/show/versions'
|
||||
= render "projects/diffs/diffs", diffs: @diffs
|
||||
- elsif @merge_request_diff.empty?
|
||||
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
|
||||
- else
|
||||
.alert.alert-warning
|
||||
%h4
|
||||
Changes view for this comparison is extremely large.
|
||||
%p
|
||||
You can
|
||||
= link_to "download it", merge_request_path(@merge_request, format: :diff), class: "vlink"
|
||||
instead.
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
|
||||
= hidden_field_tag :view, diff_view
|
||||
= hidden_field_tag :line_type
|
||||
= hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha)
|
||||
= note_target_fields(@note)
|
||||
= f.hidden_field :commit_id
|
||||
= f.hidden_field :line_code
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
.pull-left Last commit
|
||||
.last-commit.hidden-sm.pull-left
|
||||
%small.light
|
||||
= clipboard_button(clipboard_text: @commit.id)
|
||||
= clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard")
|
||||
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
|
||||
= time_ago_with_tooltip(@commit.committed_date)
|
||||
= @commit.full_title
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg>
|
After Width: | Height: | Size: 430 B |
|
@ -1,5 +1,8 @@
|
|||
- todo = issuable_todo(issuable)
|
||||
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
|
||||
- content_for :page_specific_javascripts do
|
||||
= page_specific_javascript_bundle_tag('issuable')
|
||||
|
||||
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
|
||||
.issuable-sidebar
|
||||
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
|
||||
.block.issuable-sidebar-header
|
||||
|
@ -72,7 +75,13 @@
|
|||
.selectbox.hide-collapsed
|
||||
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
|
||||
= dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
|
||||
|
||||
- if issuable.has_attribute?(:time_estimate)
|
||||
#issuable-time-tracker.block
|
||||
%issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') }
|
||||
// Fallback while content is loading
|
||||
.title.hide-collapsed
|
||||
Time tracking
|
||||
= icon('spinner spin')
|
||||
- if issuable.has_attribute?(:due_date)
|
||||
.block.due_date
|
||||
.sidebar-collapsed-icon
|
||||
|
@ -162,6 +171,8 @@
|
|||
= clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left")
|
||||
|
||||
:javascript
|
||||
gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}');
|
||||
new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}");
|
||||
new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
|
||||
new LabelsSelect();
|
||||
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
.pull-right.light #{milestone.percent_complete(current_user)}% complete
|
||||
.row
|
||||
.col-sm-6
|
||||
= link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path
|
||||
= link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
|
||||
·
|
||||
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
|
||||
.col-sm-6= milestone_progress_bar(milestone)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Support slash comand `/merge` for merging merge requests.
|
||||
merge_request: 7746
|
||||
author: Jarka Kadlecova
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fixes big pipeline and small pipeline width problems and tooltips text being outside the tooltip
|
||||
merge_request: 8593
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Adjust ProjectStatistic#repository_size with values saved as MB
|
||||
merge_request: 8616
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: "Correct User-agent placement in robots.txt"
|
||||
merge_request: 8623
|
||||
author: Eric Sabelhaus
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
title: 'Copy commit SHA to clipboard'
|
||||
merge_request: 8547
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Hide build artifacts keep button if operation is not allowed
|
||||
merge_request: 8501
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix Compare page throws 500 error when any branch/reference is not selected
|
||||
merge_request: 8492
|
||||
author: Martin Cabrera
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Show 'too many changes' message for created merge requests when they are too large
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fixed merge request tabs dont move when opening collapsed sidebar
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Use cached values to compute total issues count in milestone index pages
|
||||
merge_request: 8518
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add new endpoints for Time Tracking.
|
||||
merge_request: 8483
|
||||
author:
|
|
@ -94,6 +94,7 @@ constraints(ProjectUrlConstrainer.new) do
|
|||
get :pipelines
|
||||
get :merge_check
|
||||
post :merge
|
||||
get :merge_widget_refresh
|
||||
post :cancel_merge_when_build_succeeds
|
||||
get :ci_status
|
||||
get :ci_environments_status
|
||||
|
|
|
@ -24,6 +24,7 @@ var config = {
|
|||
environments: './environments/environments_bundle.js',
|
||||
filtered_search: './filtered_search/filtered_search_bundle.js',
|
||||
graphs: './graphs/graphs_bundle.js',
|
||||
issuable: './issuable/issuable_bundle.js',
|
||||
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
|
||||
merge_request_widget: './merge_request_widget/ci_bundle.js',
|
||||
network: './network/network_bundle.js',
|
||||
|
@ -34,6 +35,7 @@ var config = {
|
|||
users: './users/users_bundle.js',
|
||||
lib_chart: './lib/chart.js',
|
||||
lib_d3: './lib/d3.js',
|
||||
lib_vue: './lib/vue_resource.js',
|
||||
vue_pipelines: './vue_pipelines_index/index.js',
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class AddTimeEstimateToIssuables < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
# Set this constant to true if this migration requires downtime.
|
||||
DOWNTIME = false
|
||||
|
||||
# When a migration requires downtime you **must** uncomment the following
|
||||
# constant and define a short and easy to understand explanation as to why the
|
||||
# migration requires downtime.
|
||||
# DOWNTIME_REASON = ''
|
||||
|
||||
# When using the methods "add_concurrent_index" or "add_column_with_default"
|
||||
# you must disable the use of transactions as these methods can not run in an
|
||||
# existing transaction. When using "add_concurrent_index" make sure that this
|
||||
# method is the _only_ method called in the migration, any other changes
|
||||
# should go in a separate migration. This ensures that upon failure _only_ the
|
||||
# index creation fails and can be retried or reverted easily.
|
||||
#
|
||||
# To disable transactions uncomment the following line and remove these
|
||||
# comments:
|
||||
# disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
add_column :issues, :time_estimate, :integer
|
||||
add_column :merge_requests, :time_estimate, :integer
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class CreateTimelogs < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
# Set this constant to true if this migration requires downtime.
|
||||
DOWNTIME = false
|
||||
|
||||
# When a migration requires downtime you **must** uncomment the following
|
||||
# constant and define a short and easy to understand explanation as to why the
|
||||
# migration requires downtime.
|
||||
# DOWNTIME_REASON = ''
|
||||
|
||||
# When using the methods "add_concurrent_index" or "add_column_with_default"
|
||||
# you must disable the use of transactions as these methods can not run in an
|
||||
# existing transaction. When using "add_concurrent_index" make sure that this
|
||||
# method is the _only_ method called in the migration, any other changes
|
||||
# should go in a separate migration. This ensures that upon failure _only_ the
|
||||
# index creation fails and can be retried or reverted easily.
|
||||
#
|
||||
# To disable transactions uncomment the following line and remove these
|
||||
# comments:
|
||||
# disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
create_table :timelogs do |t|
|
||||
t.integer :time_spent, null: false
|
||||
t.references :trackable, polymorphic: true
|
||||
t.references :user
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
add_index :timelogs, [:trackable_type, :trackable_id]
|
||||
add_index :timelogs, :user_id
|
||||
end
|
||||
end
|
14
db/schema.rb
14
db/schema.rb
|
@ -506,6 +506,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do
|
|||
t.integer "lock_version"
|
||||
t.text "title_html"
|
||||
t.text "description_html"
|
||||
t.integer "time_estimate"
|
||||
end
|
||||
|
||||
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
|
||||
|
@ -685,6 +686,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do
|
|||
t.integer "lock_version"
|
||||
t.text "title_html"
|
||||
t.text "description_html"
|
||||
t.integer "time_estimate"
|
||||
end
|
||||
|
||||
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
|
||||
|
@ -1128,6 +1130,18 @@ ActiveRecord::Schema.define(version: 20170106172224) do
|
|||
|
||||
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
|
||||
|
||||
create_table "timelogs", force: :cascade do |t|
|
||||
t.integer "time_spent", null: false
|
||||
t.integer "trackable_id"
|
||||
t.string "trackable_type"
|
||||
t.integer "user_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree
|
||||
add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree
|
||||
|
||||
create_table "todos", force: :cascade do |t|
|
||||
t.integer "user_id", null: false
|
||||
t.integer "project_id", null: false
|
||||
|
|
|
@ -712,6 +712,146 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## Set a time estimate for an issue
|
||||
|
||||
Sets an estimated time of work for this issue.
|
||||
|
||||
```
|
||||
POST /projects/:id/issues/:issue_id/time_estimate
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `issue_id` | integer | yes | The ID of a project's issue |
|
||||
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
|
||||
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_estimate?duration=3h30m
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"human_time_estimate": "3h 30m",
|
||||
"human_total_time_spent": null,
|
||||
"time_estimate": 12600,
|
||||
"total_time_spent": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Reset the time estimate for an issue
|
||||
|
||||
Resets the estimated time for this issue to 0 seconds.
|
||||
|
||||
```
|
||||
POST /projects/:id/issues/:issue_id/reset_time_estimate
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `issue_id` | integer | yes | The ID of a project's issue |
|
||||
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_time_estimate
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"human_time_estimate": null,
|
||||
"human_total_time_spent": null,
|
||||
"time_estimate": 0,
|
||||
"total_time_spent": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Add spent time for an issue
|
||||
|
||||
Adds spent time for this issue
|
||||
|
||||
```
|
||||
POST /projects/:id/issues/:issue_id/add_spent_time
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `issue_id` | integer | yes | The ID of a project's issue |
|
||||
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
|
||||
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/add_spent_time?duration=1h
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"human_time_estimate": null,
|
||||
"human_total_time_spent": "1h",
|
||||
"time_estimate": 0,
|
||||
"total_time_spent": 3600
|
||||
}
|
||||
```
|
||||
|
||||
## Reset spent time for an issue
|
||||
|
||||
Resets the total spent time for this issue to 0 seconds.
|
||||
|
||||
```
|
||||
POST /projects/:id/issues/:issue_id/reset_spent_time
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `issue_id` | integer | yes | The ID of a project's issue |
|
||||
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_spent_time
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"human_time_estimate": null,
|
||||
"human_total_time_spent": null,
|
||||
"time_estimate": 0,
|
||||
"total_time_spent": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Get time tracking stats
|
||||
|
||||
```
|
||||
GET /projects/:id/issues/:issue_id/time_stats
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `issue_id` | integer | yes | The ID of a project's issue |
|
||||
|
||||
```bash
|
||||
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_stats
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"human_time_estimate": "2h",
|
||||
"human_total_time_spent": "1h",
|
||||
"time_estimate": 7200,
|
||||
"total_time_spent": 3600
|
||||
}
|
||||
```
|
||||
|
||||
## Comments on issues
|
||||
|
||||
Comments are done via the [notes](notes.md) resource.
|
||||
|
|
|
@ -1018,3 +1018,142 @@ Example response:
|
|||
}]
|
||||
}
|
||||
```
|
||||
## Set a time estimate for a merge request
|
||||
|
||||
Sets an estimated time of work for this merge request.
|
||||
|
||||
```
|
||||
POST /projects/:id/merge_requests/:merge_request_id/time_estimate
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `merge_request_id` | integer | yes | The ID of a project's merge request |
|
||||
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
|
||||
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_estimate?duration=3h30m
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"human_time_estimate": "3h 30m",
|
||||
"human_total_time_spent": null,
|
||||
"time_estimate": 12600,
|
||||
"total_time_spent": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Reset the time estimate for a merge request
|
||||
|
||||
Resets the estimated time for this merge request to 0 seconds.
|
||||
|
||||
```
|
||||
POST /projects/:id/merge_requests/:merge_request_id/reset_time_estimate
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `merge_request_id` | integer | yes | The ID of a project's merge_request |
|
||||
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_time_estimate
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"human_time_estimate": null,
|
||||
"human_total_time_spent": null,
|
||||
"time_estimate": 0,
|
||||
"total_time_spent": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Add spent time for a merge request
|
||||
|
||||
Adds spent time for this merge request
|
||||
|
||||
```
|
||||
POST /projects/:id/merge_requests/:merge_request_id/add_spent_time
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `merge_request_id` | integer | yes | The ID of a project's merge request |
|
||||
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
|
||||
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/add_spent_time?duration=1h
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"human_time_estimate": null,
|
||||
"human_total_time_spent": "1h",
|
||||
"time_estimate": 0,
|
||||
"total_time_spent": 3600
|
||||
}
|
||||
```
|
||||
|
||||
## Reset spent time for a merge request
|
||||
|
||||
Resets the total spent time for this merge request to 0 seconds.
|
||||
|
||||
```
|
||||
POST /projects/:id/merge_requests/:merge_request_id/reset_spent_time
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `merge_request_id` | integer | yes | The ID of a project's merge_request |
|
||||
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_spent_time
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"human_time_estimate": null,
|
||||
"human_total_time_spent": null,
|
||||
"time_estimate": 0,
|
||||
"total_time_spent": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Get time tracking stats
|
||||
|
||||
```
|
||||
GET /projects/:id/merge_requests/:merge_request_id/time_stats
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `merge_request_id` | integer | yes | The ID of a project's merge request |
|
||||
|
||||
```bash
|
||||
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_stats
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"human_time_estimate": "2h",
|
||||
"human_total_time_spent": "1h",
|
||||
"time_estimate": 7200,
|
||||
"total_time_spent": 3600
|
||||
}
|
||||
```
|
||||
|
|
|
@ -14,6 +14,7 @@ do.
|
|||
|:---------------------------|:-------------|
|
||||
| `/close` | Close the issue or merge request |
|
||||
| `/reopen` | Reopen the issue or merge request |
|
||||
| `/merge` | Merge (when build succeeds) |
|
||||
| `/title <New title>` | Change title |
|
||||
| `/assign @username` | Assign |
|
||||
| `/unassign` | Remove assignee |
|
||||
|
@ -29,3 +30,7 @@ do.
|
|||
| <code>/due <in 2 days | this Friday | December 31st></code> | Set due date |
|
||||
| `/remove_due_date` | Remove due date |
|
||||
| `/wip` | Toggle the Work In Progress status |
|
||||
| <code>/estimate <1w 3d 2h 14m></code> | Set time estimate |
|
||||
| `/remove_estimate` | Remove estimated time |
|
||||
| <code>/spend <1h 30m | -1h 5m></code> | Add or substract spent time |
|
||||
| `/remove_time_spent` | Remove time spent |
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
- [Slash commands](../user/project/slash_commands.md)
|
||||
- [Sharing a project with a group](share_with_group.md)
|
||||
- [Share projects with other groups](share_projects_with_other_groups.md)
|
||||
- [Time tracking](time_tracking.md)
|
||||
- [Web Editor](../user/project/repository/web_editor.md)
|
||||
- [Releases](releases.md)
|
||||
- [Milestones](milestones.md)
|
||||
|
|
|
@ -79,7 +79,7 @@ Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the
|
|||
initial translation of existing SVN revisions into the Git repository:
|
||||
|
||||
```
|
||||
subgit install $GIT_REPOS_PATH
|
||||
subgit install $GIT_REPO_PATH
|
||||
```
|
||||
|
||||
After the initial translation is completed, the Git repository and the SVN
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
|
@ -0,0 +1,73 @@
|
|||
# Time Tracking
|
||||
|
||||
> Introduced in GitLab 8.14.
|
||||
|
||||
Time Tracking allows you to track estimates and time spent on issues and merge
|
||||
requests within GitLab.
|
||||
|
||||
## Overview
|
||||
|
||||
Time Tracking lets you:
|
||||
* record the time spent working on an issue or a merge request,
|
||||
* add an estimate of the amount of time needed to complete an issue or a merge
|
||||
request.
|
||||
|
||||
You don't have to indicate an estimate to enter the time spent, and vice versa.
|
||||
|
||||
Data about time tracking is shown on the issue/merge request sidebar, as shown
|
||||
below.
|
||||
|
||||
![Time tracking in the sidebar](time-tracking/time-tracking-sidebar.png)
|
||||
|
||||
## How to enter data
|
||||
|
||||
Time Tracking uses two [slash commands] that GitLab introduced with this new
|
||||
feature: `/spend` and `/estimate`.
|
||||
|
||||
Slash commands can be used in the body of an issue or a merge request, but also
|
||||
in a comment in both an issue or a merge request.
|
||||
|
||||
Below is an example of how you can use those new slash commands inside a comment.
|
||||
|
||||
![Time tracking example in a comment](time-tracking/time-tracking-example.png)
|
||||
|
||||
Adding time entries (time spent or estimates) is limited to project members.
|
||||
|
||||
### Estimates
|
||||
|
||||
To enter an estimate, write `/estimate`, followed by the time. For example, if
|
||||
you need to enter an estimate of 3 days, 5 hours and 10 minutes, you would write
|
||||
`/estimate 3d 5h 10m`.
|
||||
|
||||
Every time you enter a new time estimate, any previous time estimates will be
|
||||
overridden by this new value. There should only be one valid estimate in an
|
||||
issue or a merge request.
|
||||
|
||||
To remove an estimation entirely, use `/remove_estimation`.
|
||||
|
||||
### Time spent
|
||||
|
||||
To enter a time spent, use `/spend 3d 5h 10m`.
|
||||
|
||||
Every new time spent entry will be added to the current total time spent for the
|
||||
issue or the merge request.
|
||||
|
||||
You can remove time by entering a negative amount: `/spend -3d` will remove 3
|
||||
days from the total time spent. You can't go below 0 minutes of time spent,
|
||||
so GitLab will automatically reset the time spent if you remove a larger amount
|
||||
of time compared to the time that was entered already.
|
||||
|
||||
To remove all the time spent at once, use `/remove_time_spent`.
|
||||
|
||||
## Configuration
|
||||
|
||||
The following time units are available:
|
||||
* weeks (w)
|
||||
* days (d)
|
||||
* hours (h)
|
||||
* minutes (m)
|
||||
|
||||
Default conversion rates are 1w = 5d and 1d = 8h.
|
||||
|
||||
[landing]: https://about.gitlab.com/features/time-tracking
|
||||
[slash-commands]: ../user/project/slash_commands.md
|
|
@ -35,7 +35,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
|
|||
|
||||
step 'I have group with projects' do
|
||||
@group = create(:group)
|
||||
@project = create(:project, namespace: @group)
|
||||
@project = create(:empty_project, namespace: @group)
|
||||
@event = create(:closed_issue_event, project: @project)
|
||||
|
||||
@project.team << [current_user, :master]
|
||||
|
@ -54,8 +54,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'group has a projects that does not belongs to me' do
|
||||
@forbidden_project1 = create(:project, group: @group)
|
||||
@forbidden_project2 = create(:project, group: @group)
|
||||
@forbidden_project1 = create(:empty_project, group: @group)
|
||||
@forbidden_project2 = create(:empty_project, group: @group)
|
||||
end
|
||||
|
||||
step 'I should see 1 project at group list' do
|
||||
|
|
|
@ -79,13 +79,13 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
|
|||
|
||||
def project
|
||||
@project ||= begin
|
||||
project = create :project
|
||||
project = create(:empty_project)
|
||||
project.team << [current_user, :master]
|
||||
project
|
||||
end
|
||||
end
|
||||
|
||||
def public_project
|
||||
@public_project ||= create :project, :public
|
||||
@public_project ||= create(:empty_project, :public)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -105,14 +105,14 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
|
|||
|
||||
def project
|
||||
@project ||= begin
|
||||
project = create :project
|
||||
project = create(:project, :repository)
|
||||
project.team << [current_user, :master]
|
||||
project
|
||||
end
|
||||
end
|
||||
|
||||
def public_project
|
||||
@public_project ||= create :project, :public
|
||||
@public_project ||= create(:project, :public, :repository)
|
||||
end
|
||||
|
||||
def forked_project
|
||||
|
|
|
@ -104,7 +104,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
|
|||
group = owned_group
|
||||
|
||||
%w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
|
||||
project = create :project, path: path, group: group
|
||||
project = create(:empty_project, path: path, group: group)
|
||||
milestone = create :milestone, title: "Version 7.2", project: project
|
||||
|
||||
create(:label, project: project, title: 'bug')
|
||||
|
|
|
@ -109,7 +109,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
|
|||
|
||||
step 'Group "Owned" has archived project' do
|
||||
group = Group.find_by(name: 'Owned')
|
||||
@archived_project = create(:project, namespace: group, archived: true, path: "archived-project")
|
||||
@archived_project = create(:empty_project, :archived, namespace: group, path: "archived-project")
|
||||
end
|
||||
|
||||
step 'I should see "archived" label' do
|
||||
|
|
|
@ -162,7 +162,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
|
|||
step 'I have group with projects' do
|
||||
@group = create(:group)
|
||||
@group.add_owner(current_user)
|
||||
@project = create(:project, namespace: @group)
|
||||
@project = create(:project, :repository, namespace: @group)
|
||||
@event = create(:closed_issue_event, project: @project)
|
||||
|
||||
@project.team << [current_user, :master]
|
||||
|
|
|
@ -46,11 +46,11 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'other projects have deploy keys' do
|
||||
@second_project = create(:project, namespace: create(:group))
|
||||
@second_project = create(:empty_project, namespace: create(:group))
|
||||
@second_project.team << [current_user, :master]
|
||||
create(:deploy_keys_project, project: @second_project)
|
||||
|
||||
@third_project = create(:project, namespace: create(:group))
|
||||
@third_project = create(:empty_project, namespace: create(:group))
|
||||
@third_project.team << [current_user, :master]
|
||||
create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first)
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I am a member of project "Shop"' do
|
||||
@project = create(:project, name: "Shop")
|
||||
@project = create(:project, :repository, name: "Shop")
|
||||
@project.team << [@user, :reporter]
|
||||
end
|
||||
|
||||
|
@ -18,7 +18,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I already have a project named "Shop" in my namespace' do
|
||||
@my_project = create(:project, name: "Shop", namespace: current_user.namespace)
|
||||
@my_project = create(:project, :repository, name: "Shop", namespace: current_user.namespace)
|
||||
end
|
||||
|
||||
step 'I should see a "Name has already been taken" warning' do
|
||||
|
|
|
@ -7,7 +7,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
|
|||
|
||||
step 'I am a member of project "Shop"' do
|
||||
@project = Project.find_by(name: "Shop")
|
||||
@project ||= create(:project, name: "Shop")
|
||||
@project ||= create(:project, :repository, name: "Shop")
|
||||
@project.team << [@user, :reporter]
|
||||
end
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
|
|||
|
||||
step 'There is an open Merge Request' do
|
||||
@user = create(:user)
|
||||
@project = create(:project, :public)
|
||||
@project = create(:project, :public, :repository)
|
||||
@project_member = create(:project_member, :developer, user: @user, project: @project)
|
||||
@merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
|
||||
end
|
||||
|
|
|
@ -35,7 +35,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
|
|||
|
||||
step 'There is an open Merge Request' do
|
||||
@user = create(:user)
|
||||
@project = create(:project, :public)
|
||||
@project = create(:project, :public, :repository)
|
||||
@project_member = create(:project_member, :developer, user: @user, project: @project)
|
||||
@merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue