Commit 17b7952e authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2017-09-13

# Conflicts:
#	Gemfile.lock
#	app/controllers/projects/issues_controller.rb
#	app/models/ci/runner.rb
#	app/views/admin/dashboard/index.html.haml
#	app/views/layouts/nav/sidebar/_admin.html.haml
#	doc/README.md
#	lib/ci/model.rb
#	lib/gitlab/workhorse.rb
#	spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
[ci skip]
parents 20ec88c7 33010da2
......@@ -435,6 +435,7 @@ db:migrate:reset-mysql:
.migration-paths: &migration-paths
<<: *dedicated-runner
<<: *pull-cache
<<: *except-docs
stage: test
variables:
SETUP_DB: "false"
......
......@@ -645,7 +645,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method.
Metrics/CyclomaticComplexity:
Enabled: true
Max: 14
Max: 13
# Limit lines to 80 characters.
Metrics/LineLength:
......@@ -667,7 +667,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity:
Enabled: true
Max: 17
Max: 15
# Lint ########################################################################
......
......@@ -422,7 +422,7 @@ request is as follows:
1. Fork the project into your personal space on GitLab.com
1. Create a feature branch, branch away from `master`
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
1. Write [tests](https://docs.gitlab.com/ee/development/rake_tasks.html#run-tests) and code
1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide]
......
......@@ -423,4 +423,4 @@ gem 'flipper-active_record', '~> 0.10.2'
# Structured logging
gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.6'
gem 'grape_logging', '~> 1.7'
......@@ -380,7 +380,7 @@ GEM
activesupport
grape (>= 0.16.0)
rake
grape_logging (1.6.0)
grape_logging (1.7.0)
grape
grpc (1.4.5)
google-protobuf (~> 3.1)
......@@ -1073,8 +1073,12 @@ DEPENDENCIES
grape (~> 1.0)
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0)
<<<<<<< HEAD
gssapi
grape_logging (~> 1.6)
=======
grape_logging (~> 1.7)
>>>>>>> upstream/master
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
......
......@@ -12,4 +12,5 @@ import 'core-js/fn/symbol';
// Browser polyfills
import './polyfills/custom_event';
import './polyfills/element';
import './polyfills/event';
import './polyfills/nodelist';
if (typeof window.CustomEvent !== 'function') {
window.CustomEvent = function CustomEvent(event, params) {
const evt = document.createEvent('CustomEvent');
const evtParams = params || { bubbles: false, cancelable: false, detail: undefined };
const evtParams = {
bubbles: false,
cancelable: false,
detail: undefined,
...params,
};
evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail);
return evt;
};
......
/**
* Polyfill for IE11 support.
* new Event() is not supported by IE11.
* Although `initEvent` is deprecated for modern browsers it is the one supported by IE
*/
if (typeof window.Event !== 'function') {
window.Event = function Event(event, params) {
const evt = document.createEvent('Event');
const evtParams = {
bubbles: false,
cancelable: false,
...params,
};
evt.initEvent(event, evtParams.bubbles, evtParams.cancelable);
return evt;
};
window.Event.prototype = Event;
}
......@@ -148,7 +148,7 @@ export const documentMouseMove = (e) => {
export const subItemsMouseLeave = (relatedTarget) => {
clearTimeout(timeoutId);
if (!relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
hideMenu(currentOpenMenu);
}
};
......
......@@ -72,10 +72,6 @@ export default {
required: false,
default: () => [],
},
isConfidential: {
type: Boolean,
required: true,
},
markdownPreviewPath: {
type: String,
required: true,
......@@ -131,7 +127,6 @@ export default {
this.showForm = true;
this.store.setFormState({
title: this.state.titleText,
confidential: this.isConfidential,
description: this.state.descriptionText,
lockedWarningVisible: false,
updateLoading: false,
......@@ -147,8 +142,6 @@ export default {
.then((data) => {
if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url);
} else if (data.confidential !== this.isConfidential) {
gl.utils.visitUrl(location.pathname);
}
return this.service.getData();
......
<script>
export default {
props: {
formState: {
type: Object,
required: true,
},
},
};
</script>
<template>
<fieldset class="checkbox">
<label for="issue-confidential">
<input
type="checkbox"
value="1"
id="issue-confidential"
v-model="formState.confidential" />
This issue is confidential and should only be visible to team members with at least Reporter access.
</label>
</fieldset>
</template>
......@@ -4,7 +4,6 @@
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue';
import confidentialCheckbox from './fields/confidential_checkbox.vue';
export default {
props: {
......@@ -44,7 +43,6 @@
descriptionField,
descriptionTemplate,
editActions,
confidentialCheckbox,
},
computed: {
hasIssuableTemplates() {
......@@ -81,8 +79,6 @@
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" />
<confidential-checkbox
:form-state="formState" />
<edit-actions
:form-state="formState"
:can-destroy="canDestroy" />
......
......@@ -35,7 +35,6 @@ document.addEventListener('DOMContentLoaded', () => {
initialDescriptionHtml: this.initialDescriptionHtml,
initialDescriptionText: this.initialDescriptionText,
issuableTemplates: this.issuableTemplates,
isConfidential: this.isConfidential,
markdownPreviewPath: this.markdownPreviewPath,
markdownDocsPath: this.markdownDocsPath,
projectPath: this.projectPath,
......
......@@ -3,7 +3,6 @@ export default class Store {
this.state = initialState;
this.formState = {
title: '',
confidential: false,
description: '',
lockedWarningVisible: false,
updateLoading: false,
......
......@@ -13,7 +13,7 @@ export function formatRelevantDigits(number) {
let relevantDigits = 0;
let formattedNumber = '';
if (!isNaN(Number(number))) {
digitsLeft = number.split('.')[0];
digitsLeft = number.toString().split('.')[0];
switch (digitsLeft.length) {
case 1:
relevantDigits = 3;
......
<script>
/* global Flash */
import _ from 'underscore';
import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
import Graph from './graph.vue';
......@@ -21,10 +20,9 @@
hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath,
endpoint: metricsData.additionalMetrics,
metricsEndpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint,
showEmptyState: true,
backOffRequestCounter: 0,
updateAspectRatio: false,
updatedAspectRatios: 0,
resizeThrottled: {},
......@@ -39,50 +37,16 @@
methods: {
getGraphsData() {
const maxNumberOfRequests = 3;
this.state = 'loading';
gl.utils.backOff((next, stop) => {
this.service.get().then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < maxNumberOfRequests) {
next();
} else {
stop(new Error('Failed to connect to the prometheus server'));
}
} else {
stop(resp);
}
}).catch(stop);
})
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.state = 'unableToConnect';
return false;
}
return resp.json();
})
.then((metricGroupsData) => {
if (!metricGroupsData) return false;
this.store.storeMetrics(metricGroupsData.data);
return this.getDeploymentData();
})
.then((deploymentData) => {
if (deploymentData !== false) {
this.store.storeDeploymentData(deploymentData.deployments);
this.showEmptyState = false;
}
return {};
})
.catch(() => {
this.state = 'unableToConnect';
});
},
getDeploymentData() {
return this.service.getDeploymentData(this.deploymentEndpoint)
.then(resp => resp.json())
.catch(() => new Flash('Error getting deployment information.'));
Promise.all([
this.service.getGraphsData()
.then(data => this.store.storeMetrics(data)),
this.service.getDeploymentData()
.then(data => this.store.storeDeploymentData(data))
.catch(() => new Flash('Error getting deployment information.')),
])
.then(() => { this.showEmptyState = false; })
.catch(() => { this.state = 'unableToConnect'; });
},
resize() {
......@@ -99,7 +63,10 @@
},
created() {
this.service = new MonitoringService(this.endpoint);
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
},
......
......@@ -3,7 +3,7 @@
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
import monitoringPaths from './monitoring_paths.vue';
import GraphPath from './graph_path.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
......@@ -40,8 +40,6 @@
graphHeightOffset: 120,
margin: {},
unitOfDisplay: '',
areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1',
yAxisLabel: '',
legendTitle: '',
reducedDeploymentData: [],
......@@ -63,7 +61,7 @@
GraphLegend,
GraphFlag,
GraphDeployment,
monitoringPaths,
GraphPath,
},
computed: {
......@@ -143,7 +141,7 @@
},
renderAxesPaths() {
this.timeSeries = createTimeSeries(this.graphData.queries[0].result,
this.timeSeries = createTimeSeries(this.graphData.queries[0],
this.graphWidth,
this.graphHeight,
this.graphHeightOffset);
......@@ -162,7 +160,7 @@
const xAxis = d3.svg.axis()
.scale(axisXScale)
.ticks(measurements.xTicks)
.ticks(d3.time.minute, 60)
.tickFormat(timeScaleFormat)
.orient('bottom');
......@@ -238,7 +236,7 @@
class="graph-data"
:viewBox="innerViewBox"
ref="graphData">
<monitoring-paths
<graph-path
v-for="(path, index) in timeSeries"
:key="index"
:generated-line-path="path.linePath"
......@@ -246,7 +244,7 @@
:line-color="path.lineColor"
:area-color="path.areaColor"
/>
<monitoring-deployment
<graph-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
:graph-height="graphHeight"
......
......@@ -81,6 +81,13 @@
formatMetricUsage(series) {
return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`;
},
createSeriesString(index, series) {
if (series.metricTag) {
return `${series.metricTag} ${this.formatMetricUsage(series)}`;
}
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
},
},
mounted() {
this.$nextTick(() => {
......@@ -164,7 +171,7 @@
ref="legendTitleSvg"
x="38"
:y="graphHeight - 30">
{{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}}
{{createSeriesString(index, series)}}
</text>
<text
v-else
......
import Vue from 'vue';
import VueResource from 'vue-resource';
import statusCodes from '../../lib/utils/http_status';
Vue.use(VueResource);
const MAX_REQUESTS = 3;
function backOffRequest(makeRequestCallback) {
let requestCounter = 0;
return gl.utils.backOff((next, stop) => {
makeRequestCallback().then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
requestCounter += 1;
if (requestCounter < MAX_REQUESTS) {
next();
} else {
stop(new Error('Failed to connect to the prometheus server'));
}
} else {
stop(resp);
}
}).catch(stop);
});
}
export default class MonitoringService {
constructor(endpoint) {
this.graphs = Vue.resource(endpoint);
constructor({ metricsEndpoint, deploymentEndpoint }) {
this.metricsEndpoint = metricsEndpoint;
this.deploymentEndpoint = deploymentEndpoint;
}
get() {
return this.graphs.get();
getGraphsData() {
return backOffRequest(() => Vue.http.get(this.metricsEndpoint))
.then(resp => resp.json())
.then((response) => {
if (!response || !response.data) {
throw new Error('Unexpected metrics data response from prometheus endpoint');
}
return response.data;
});
}
// eslint-disable-next-line class-methods-use-this
getDeploymentData(endpoint) {
return Vue.http.get(endpoint);
getDeploymentData() {
return backOffRequest(() => Vue.http.get(this.deploymentEndpoint))
.then(resp => resp.json())
.then((response) => {
if (!response || !response.deployments) {
throw new Error('Unexpected deployment data response from prometheus endpoint');
}
return response.deployments;
});
}
}
import d3 from 'd3';
import _ from 'underscore';
export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) {
const maxValues = seriesData.map((timeSeries, index) => {
const defaultColorPalette = {
blue: ['#1f78d1', '#8fbce8'],
orange: ['#fc9403', '#feca81'],
red: ['#db3b21', '#ed9d90'],
green: ['#1aaa55', '#8dd5aa'],
purple: ['#6666c4', '#d1d1f0'],
};
const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) {
let usedColors = [];
function pickColor(name) {
let pick;
if (name && defaultColorPalette[name]) {
pick = name;
} else {
const unusedColors = _.difference(defaultColorOrder, usedColors);
if (unusedColors.length > 0) {
pick = unusedColors[0];
} else {
usedColors = [];
pick = defaultColorOrder[0];
}
}
usedColors.push(pick);
return defaultColorPalette[pick];
}
const maxValues = queryData.result.map((timeSeries, index) => {
const maxValue = d3.max(timeSeries.values.map(d => d.value));
return {
maxValue,
......@@ -12,10 +41,11 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
let timeSeriesNumber = 1;
let lineColor = '#1f78d1';
let areaColor = '#8fbce8';
return seriesData.map((timeSeries) => {
return queryData.result.map((timeSeries, timeSeriesNumber) => {
let metricTag = '';
let lineColor = '';
let areaColor = '';
const timeSeriesScaleX = d3.time.scale()
.range([0, graphWidth - 70]);
......@@ -23,49 +53,30 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
.range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
timeSeriesScaleX.ticks(d3.time.minute, 60);
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
const lineFunction = d3.svg.line()
.interpolate('linear')
.x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area()
.interpolate('linear')
.x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset)
.y1(d => timeSeriesScaleY(d.value))
.interpolate('linear');
switch (timeSeriesNumber) {
case 1:
lineColor = '#1f78d1';
areaColor = '#8fbce8';
break;
case 2:
lineColor = '#fc9403';
areaColor = '#feca81';
break;
case 3:
lineColor = '#db3b21';
areaColor = '#ed9d90';
break;
case 4:
lineColor = '#1aaa55';
areaColor = '#8dd5aa';
break;
case 5:
lineColor = '#6666c4';
areaColor = '#d1d1f0';
break;
default:
lineColor = '#1f78d1';
areaColor = '#8fbce8';
break;
}
.y1(d => timeSeriesScaleY(d.value));
if (timeSeriesNumber <= 5) {
timeSeriesNumber = timeSeriesNumber += 1;
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
const seriesCustomizationData = queryData.series != null &&
_.findWhere(queryData.series[0].when,
{ value: timeSeriesMetricLabel });
if (seriesCustomizationData != null) {
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
} else {
timeSeriesNumber = 1;
metricTag = timeSeriesMetricLabel || `series ${timeSeriesNumber + 1}`;
[lineColor, areaColor] = pickColor();
}
return {
......@@ -75,6 +86,7 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
values: timeSeries.values,
lineColor,
areaColor,
metricTag,
};
});
}
......@@ -19,6 +19,11 @@ export default class NewNavSidebar {
}
bindEvents() {
document.addEventListener('click', (e) => {
if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) {
this.toggleCollapsedSidebar(true);
}
});
this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false));
......@@ -63,7 +68,7 @@ export default class NewNavSidebar {
if (breakpoint === 'sm' || breakpoint === 'md') {
this.toggleCollapsedSidebar(true);
} else if (breakpoint === 'lg') {
const collapse = this.$sidebar.hasClass('sidebar-icons-only');
const collapse = Cookies.get('sidebar_collapsed') === 'true';
this.toggleCollapsedSidebar(collapse);
}
}
......
gl-emoji {
font-style: normal;
display: inline-flex;
vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
......
......@@ -17,8 +17,11 @@
max-width: $limited-layout-width-sm;
margin-left: auto;
margin-right: auto;
padding-top: 64px;
padding-bottom: 64px;
@media (min-width: $screen-md-min) {
padding-top: 64px;
padding-bottom: 64px;
}
}
}
......
......@@ -431,6 +431,7 @@ header.navbar-gitlab-new {
.breadcrumb-item-text {
@include str-truncated(128px);
text-decoration: inherit;
}
.breadcrumbs-list-angle {
......
......@@ -99,6 +99,13 @@ $new-sidebar-collapsed-width: 50px;
box-shadow: inset -2px 0 0 $border-color;
transform: translate3d(0, 0, 0);
&:not(.sidebar-icons-only) {
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
box-shadow: inset -2px 0 0 $border-color,
2px 1px 3px $dropdown-shadow-color;
}
}
&.sidebar-icons-only {
width: $new-sidebar-collapsed-width;
......
......@@ -608,7 +608,7 @@
+ .files,
+ .alert {
margin-top: 30px;
margin-top: 32px;
}
}
}
......
......@@ -7,11 +7,11 @@ module Ci
def create
@content = params[:content]
@error = Ci::GitlabCiYamlProcessor.validation_message(@content)
@error = Gitlab::Ci::YamlProcessor.validation_message(@content)
@status = @error.blank?
if @error.blank?
@config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@config_processor = Gitlab::Ci::YamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
@jobs = @config_processor.jobs
......
......@@ -48,7 +48,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
ProjectsFinder
.new(params: finder_params, current_user: current_user)
.execute
.includes(:route, :creator, namespace: :route)
.includes(:route, :creator, namespace: [:route, :owner])
end
def load_events
......
......@@ -9,7 +9,11 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action :authenticate_user!, only: [:new, :export_csv]
before_action :check_issues_available!
<<<<<<< HEAD
before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv]
=======
before_action :issue, except: [:index, :new, :create, :bulk_update]
>>>>>>> upstream/master
before_action :set_issues_index, only: [:index]
# Allow write(create) issue
......
......@@ -132,10 +132,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def charts
@charts = {}
@charts[:week] = Ci::Charts::WeekChart.new(project)
@charts[:month] = Ci::Charts::MonthChart.new(project)
@charts[:year] = Ci::Charts::YearChart.new(project)
@charts[:pipeline_times] = Ci::Charts::PipelineTime.new(project)
@charts[:week] = Gitlab::Ci::Charts::WeekChart.new(project)
@charts[:month] = Gitlab::Ci::Charts::MonthChart.new(project)
@charts[:year] = Gitlab::Ci::Charts::YearChart.new(project)
@charts[:pipeline_times] = Gitlab::Ci::Charts::PipelineTime.new(project)
@counts = {}
@counts[:total] = @project.pipelines.count(:all)
......
module AutoDevopsHelper
def show_auto_devops_callout?(project)
show_callout?('auto_devops_settings_dismissed') &&
Feature.get(:auto_devops_banner_disabled).off? &&
show_callout?('auto_devops_settings_dismissed') &&
can?(current_user, :admin_pipeline, project) &&
project.has_auto_devops_implicitly_disabled?
project.has_auto_devops_implicitly_disabled? &&
!project.repository.gitlab_ci_yml &&
project.ci_services.active.none?
end
end
......@@ -30,7 +30,7 @@ module BuildsHelper
def build_failed_issue_options
{
title: "Build Failed ##{@build.id}",
title: "Job Failed ##{@build.id}",
description: project_job_url(@project, @build)
}
end
......
......@@ -7,7 +7,8 @@ module GraphHelper
refs << commit_refs.join(' ')
# append note count
refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0
notes_count = @graph.notes[commit.id]
refs << "[#{notes_count} #{pluralize(notes_count, 'note')}]" if notes_count > 0
refs
end
......
......@@ -214,7 +214,6 @@ module IssuablesHelper
canUpdate: can?(current_user, :update_issue, issuable),
canDestroy: can?(current_user, :destroy_issue, issuable),
issuableRef: issuable.to_reference,
isConfidential: issuable.confidential,
markdownPreviewPath: preview_markdown_path(@project),
markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable),
......
......@@ -137,15 +137,7 @@ module ProjectsHelper
end
def last_push_event
return unless current_user
return current_user.recent_push unless @project
project_ids = [@project.id]
if fork = current_user.fork_of(@project)
project_ids << fork.id
end
current_user.recent_push(project_ids)
current_user&.recent_push(@project)
end
def project_feature_access_select(field)
......@@ -338,7 +330,7 @@ module ProjectsHelper
def git_user_name
if current_user
current_user.name
current_user.name.gsub('"', '\"')
else
_("Your name")
end
......
......@@ -104,7 +104,7 @@ module TreeHelper
subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
if subtree.count == 1 && subtree.first.dir?
return tree_join(tree.name, flatten_tree(subtree.first))
return tree_join(tree.name, flatten_tree(root_path, subtree.first))
else
return tree.name
end
......
......@@ -150,11 +150,11 @@ class ApplicationSetting < ActiveRecord::Base
validates :housekeeping_full_repack_period,
presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_incremental_repack_period }
numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_incremental_repack_period }
validates :housekeeping_gc_period,
presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period }
numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_full_repack_period }
validates :terminal_max_session_time,
presence: true,
......@@ -260,7 +260,7 @@ class ApplicationSetting < ActiveRecord::Base
housekeeping_full_repack_period: 50,
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
import_sources: Gitlab::ImportSources.values,
import_sources: Settings.gitlab['import_sources'],
koding_enabled: false,
koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'],
......
......@@ -13,7 +13,7 @@ module BlobViewer
prepare!
@validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data)
@validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data)
end
def valid?
......
......@@ -457,8 +457,8 @@ module Ci
return unless trace
trace = trace.dup
Ci::MaskSecret.mask!(trace, project.runners_token) if project
Ci::MaskSecret.mask!(trace, token)
Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
Gitlab::Ci::MaskSecret.mask!(trace, token)
trace
end
......
module Ci
class GroupVariable < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include HasVariable
include Presentable
......
module Ci
class Pipeline < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include HasStatus
include Importable
include AfterCommitQueue
......@@ -349,8 +349,8 @@ module Ci
return @config_processor if defined?(@config_processor)
@config_processor ||= begin
Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path)
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
Gitlab::Ci::YamlProcessor.new(ci_yaml_file, project.full_path)
rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e
self.yaml_errors = e.message
nil
rescue
......
module Ci
class PipelineSchedule < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include Importable
acts_as_paranoid
......
module Ci
class PipelineScheduleVariable < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include HasVariable
belongs_to :pipeline_schedule
......
module Ci
class PipelineVariable < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include HasVariable
belongs_to :pipeline
......
module Ci
class Runner < ActiveRecord::Base
<<<<<<< HEAD
extend Ci::Model
prepend EE::Ci::Runner
=======
extend Gitlab::Ci::Model
>>>>>>> upstream/master
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
......
module Ci
class RunnerProject < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
belongs_to :runner
belongs_to :project
......
module Ci
class Stage < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include Importable
include HasStatus
include Gitlab::OptimisticLocking
......
module Ci
class Trigger < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
acts_as_paranoid
......
module Ci
class TriggerRequest < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
belongs_to :trigger
belongs_to :pipeline, foreign_key: :commit_id
......
module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include HasVariable
include Presentable
prepend EE::Ci::Variable
......
......@@ -49,7 +49,7 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :push_event_payload, foreign_key: :event_id
has_one :push_event_payload
# Callbacks
after_create :reset_project_activity
......@@ -247,13 +247,7 @@ class Event < ActiveRecord::Base
def action_name
if push?
if new_ref?
"pushed new"
elsif rm_ref?
"deleted"
else
"pushed to"
end
push_action_name
elsif closed?
"closed"
elsif merged?
......@@ -269,11 +263,7 @@ class Event < ActiveRecord::Base
elsif commented?
"commented on"
elsif created_project?
if project.external_import?
"imported"
else
"created"
end
created_project_action_name
else
"opened"
end
......@@ -366,6 +356,24 @@ class Event < ActiveRecord::Base
private
def push_action_name
if new_ref?
"pushed new"
elsif rm_ref?
"deleted"
else
"pushed to"
end
end
def created_project_action_name
if project.external_import?
"imported"
else
"created"
end
end
def recent_update?
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
end
......
class GpgSignature < ActiveRecord::Base
include ShaAttribute
include IgnorableColumn
ignore_column :valid_signature
sha_attribute :commit_sha
sha_attribute :gpg_key_primary_keyid
......
......@@ -39,9 +39,6 @@ class Issue < ActiveRecord::Base
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
validates :project, presence: true
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
......
......@@ -28,7 +28,7 @@ class PersonalAccessToken < ActiveRecord::Base
protected
def validate_scopes
unless scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) }
unless revoked || scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) }
errors.add :scopes, "can only contain available scopes"
end
end
......
class ProjectAutoDevops < ActiveRecord::Base
belongs_to :project
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
def variables
......
......@@ -30,6 +30,44 @@ class PushEvent < Event
delegate :commit_count, to: :push_event_payload
alias_method :commits_count, :commit_count
# Returns events of pushes that either pushed to an existing ref or created a
# new one.
def self.created_or_pushed
actions = [
PushEventPayload.actions[:pushed],
PushEventPayload.actions[:created]
]
joins(:push_event_payload)
.where(push_event_payloads: { action: actions })
end
# Returns events of pushes to a branch.
def self.branch_events
ref_type = PushEventPayload.ref_types[:branch]
joins(:push_event_payload)
.where(push_event_payloads: { ref_type: ref_type })
end
# Returns PushEvent instances for which no merge requests have been created.
def self.without_existing_merge_requests
existing_mrs = MergeRequest.except(:order)
.select(1)
.where('merge_requests.source_project_id = events.project_id')
.where('merge_requests.source_branch = push_event_payloads.ref')
# For reasons unknown the use of #eager_load will result in the
# "push_event_payload" association not being set. Because of this we're
# using "joins" here, which does mean an additional query needs to be
# executed in order to retrieve the "push_event_association" when the
# returned PushEvent is used.
joins(:push_event_payload)
.where('NOT EXISTS (?)', existing_mrs)
.created_or_pushed
.branch_events
end
def self.sti_name
PUSHED
end
......
......@@ -670,20 +670,13 @@ class User < ActiveRecord::Base
@personal_projects_count ||= personal_projects.count
end
def recent_push(project_ids = nil)
# Get push events not earlier than 2 hours ago
events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
events = events.where(project_id: project_ids) if project_ids
def recent_push(project = nil)
service = Users::LastPushEventService.new(self)
# Use the latest event that has not been pushed or merged recently
events.includes(:project).recent.find do |event|
next unless event.project.repository.branch_exists?(event.branch_name)
merge_requests = MergeRequest.where("created_at >= ?", event.created_at)
.where(source_project_id: event.project.id,
source_branch: event.branch_name)
merge_requests.empty?
if project
service.last_event_for_project(project)
else
service.last_event_for_user
end
end
......
......@@ -32,8 +32,8 @@ class BuildDetailsEntity < JobEntity
private
def build_failed_issue_options
{ title: "Build Failed ##{build.id}",
description: project_job_path(project, build) }
{ title: "Job Failed ##{build.id}",
description: "Job [##{build.id}](#{project_job_path(project, build)}) failed for #{build.sha}:\n" }
end
def current_user
......
......@@ -16,7 +16,7 @@ module Ci
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
.execute(:trigger, ignore_skip_ci: true) do |pipeline|
trigger.trigger_requests.create!(pipeline: pipeline)
pipeline.trigger_requests.create!(trigger: trigger)
create_pipeline_variables!(pipeline)
end
......
......@@ -74,12 +74,19 @@ class EventCreateService
# We're using an explicit transaction here so that any errors that may occur
# when creating push payload data will result in the event creation being
# rolled back as well.
Event.transaction do
event = create_event(project, current_user, Event::PUSHED)
event = Event.transaction do
new_event = create_event(project, current_user, Event::PUSHED)
PushEventPayloadService.new(event, push_data).execute
PushEventPayloadService
.new(new_event, push_data)
.execute
new_event
end
Users::LastPushEventService.new(current_user)
.cache_last_push_event(event)
Users::ActivityService.new(current_user, 'push').execute
end
......
module Users
# Service class for caching and retrieving the last push event of a user.
class LastPushEventService
EXPIRATION = 2.hours
def initialize(user)
@user = user
end
# Caches the given push event for the current user in the Rails cache.
#
# event - An instance of PushEvent to cache.
def cache_last_push_event(event)
keys = [
project_cache_key(event.project),
user_cache_key
]
if event.project.forked?
keys << project_cache_key(event.project.forked_from_project)
end
keys.each { |key| set_key(key, event.id) }
end
# Returns the last PushEvent for the current user.
#
# This method will return nil if no event was found.
def last_event_for_user
find_cached_event(user_cache_key)
end
# Returns the last PushEvent for the current user and the given project.
#
# project - An instance of Project for which to retrieve the PushEvent.
#
# This method will return nil if no event was found.
def last_event_for_project(project)
find_cached_event(project_cache_key(project))
end
def find_cached_event(cache_key)
event_id = get_key(cache_key)
return unless event_id
unless (event = find_event_in_database(event_id))
# We don't want to keep querying the same data over and over when a
# merge request has been created, thus we remove the key if no event
# (meaning an MR was created) is returned.
Rails.cache.delete(cache_key)
end
event
end
private
def find_event_in_database(id)
PushEvent
.without_existing_merge_requests
.find_by(id: id)
end
def user_cache_key
"last-push-event/#{@user.id}"
end
def project_cache_key(project)
"last-push-event/#{@user.id}/#{project.id}"
end
def get_key(key)
Rails.cache.read(key, raw: true)
end
def set_key(key, value)
# We're using raw values here since this takes up less space and we don't
# store complex objects.
Rails.cache.write(key, value, raw: true, expires_in: EXPIRATION)
end
end
end
......@@ -126,6 +126,7 @@
GitLab API
%span.pull-right
= API::API::version
<<<<<<< HEAD
- if Gitlab::Geo.enabled?
%p
......@@ -136,6 +137,13 @@
- else
Undefined
=======
- if Gitlab.config.pages.enabled
%p
GitLab Pages
%span.pull-right
= Gitlab::Pages::VERSION
>>>>>>> upstream/master
%p
Git
%span.pull-right
......
-# haml-lint:disable InlineJavaScript
:javascript
window.onload = function() {
var s=document.createElement("script");s.onload=function(){bootlint.showLintReportForCurrentDocument([], {hasProblems: false, problemFree: false});};s.src="https://maxcdn.bootstrapcdn.com/bootlint/latest/bootlint.min.js";document.body.appendChild(s);
}
......@@ -76,4 +76,3 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
= render 'layouts/bootlint' if Rails.env.development?
......@@ -6,7 +6,7 @@
= icon('wrench')
.sidebar-context-title Admin Area
%ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do
= sidebar_link admin_root_path, title: _('Overview'), css: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('overview')
......@@ -14,7 +14,7 @@
Overview
%ul.sidebar-sub-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: { class: "fly-out-top-item" } ) do
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_root_path do
%strong.fly-out-top-item-name
#{ _('Overview') }
......@@ -52,16 +52,21 @@
%span
ConvDev Index
<<<<<<< HEAD
= nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles audit_logs)) do
= sidebar_link admin_conversational_development_index_path, title: _('Monitoring') do
=======
= nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do
= sidebar_link admin_system_info_path, title: _('Monitoring') do
>>>>>>> upstream/master
.nav-icon-container
= custom_icon('monitoring')
%span.nav-item-name
Monitoring
%ul.sidebar-sub-level-items
= nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_conversational_development_index_path do
= nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_system_info_path do
%strong.fly-out-top-item-name
#{ _('Monitoring') }
%li.divider.fly-out-top-item
......
......@@ -118,7 +118,7 @@
%span.badge.count.issue_counter.fly-out-badge
= number_with_delimiter(@project.open_issues_count)
%li.divider.fly-out-top-item
= nav_link(controller: :issues) do
= nav_link(controller: :issues, action: :index) do
= link_to project_issues_path(@project), title: 'Issues' do
%span
List
......
......@@ -36,10 +36,10 @@
.preview= image_tag "#{scheme.css_class}-scheme-preview.png"
= f.radio_button :color_scheme_id, scheme.id
= scheme.name
.col-sm-12
%hr
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Behavior
......
......@@ -33,7 +33,7 @@
.commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
- commit_timeago = time_ago_with_tooltip(commit.committed_date)
- commit_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom')
- commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
#{ commit_text.html_safe }
- if show_project_name
......
......@@ -41,10 +41,10 @@
.swipe.view.hide
.swipe-frame
.frame.deleted
= image_tag(old_blob_raw_path, alt: diff_file.old_path)
= image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
.swipe-wrap
.frame.added
= image_tag(blob_raw_path, alt: diff_file.new_path)
= image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false)
%span.swipe-bar
%span.top-handle
%span.bottom-handle
......@@ -52,9 +52,9 @@
.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
= image_tag(old_blob_raw_path, alt: diff_file.old_path)
= image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
.frame.added
= image_tag(blob_raw_path, alt: diff_file.new_path)
= image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false)
.controls
.transparent
.drag-track
......
......@@ -39,6 +39,6 @@
Tags
.col-sm-10
= f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control'
.help-block You can setup jobs to only use Runners with specific tags
.help-block You can setup jobs to only use Runners with specific tags. Separate tags with commas.
.form-actions
= f.submit 'Save changes', class: 'btn btn-save'
......@@ -8,7 +8,7 @@
"aria-hidden": "true" }
%span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"",
":title" => '(list.label ? list.label.description : "")' }
":title" => '(list.label ? list.label.description : "")', data: { container: "body" } }
{{ list.title }}
%span.has-tooltip{ "v-if": "list.type === \"label\"",
......
......@@ -6,15 +6,15 @@
%script#js-register-u2f-setup{ type: "text/template" }
- if current_user.two_factor_otp_enabled?
.row.append-bottom-10
.col-md-3
%button#js-setup-u2f-device.btn.btn-info Setup new U2F device
.col-md-9
.col-md-4
%button#js-setup-u2f-device.btn.btn-info.btn-block Setup new U2F device
.col-md-8
%p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.
- else
.row.append-bottom-10
.col-md-3
%button#js-setup-u2f-device.btn.btn-info{ disabled: true } Setup new U2F device
.col-md-9
.col-md-4
%button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true } Setup new U2F device
.col-md-8
%p.text-warning You need to register a two-factor authentication app before you can set up a U2F device.
%script#js-register-u2f-in-progress{ type: "text/template" }
......
---
title: Allow to use same periods for different housekeeping tasks (effectively
skipping the lesser task)
merge_request: 13711
author: @cernvcs
type: added
---
title: Escape quotes in git username
merge_request: 14020
author: Brandon Everett
type: fixed
---
title: Decrease Perceived Complexity threshold to 15
merge_request: 14160
author: Maxim Rydkin
type: other
---
title: Decrease Cyclomatic Complexity threshold to 13
merge_request: 14152
author: Maxim Rydkin
type: other
---
title: Add GitLab-Pages version to Admin Dashboard
merge_request: 14040
author: @travismiller
type: added
---
title: Fixed non-UTF-8 valid branch names from causing an error.
merge_request: 14090
type: fixed
---
title: 'Add help text to runner edit: tags should be separated by commas.'
merge_request:
author: Brendan O'Leary
type: added
---
title: Image attachments are properly displayed in notification emails again
merge_request: 14161
author:
type: fixed
---
title: Resolve Image onion skin + swipe does not work anymore
merge_request:
author:
type: fixed
---
title: Move `lib/ci` to `lib/gitlab/ci`
merge_request: 14078
author: Maxim Rydkin
type: other
---
title: Show notes number more user-friendly in the graph
merge_request: 13949
author: Vladislav Kaverin
type: changed
---
title: Fixed merge request changes bar jumping
merge_request:
author:
type: fixed
---
title: Tooltips in the commit info box now all face the same direction
merge_request:
author: Jedidiah Broadbent
type: fixed
---
title: Fix ConvDev Index nav item and Monitoring submenu regression
merge_request: !14124
author:
type: fixed
---
title: Eager load namespace owners for project dashboards
merge_request:
author:
type: other
---
title: Scripts to detect orphaned repositories
merge_request: 14204
author:
type: added
---
title: Fixes the 500 errors caused by a race condition in GPG's tmp directory handling
merge_request: 14194
author: Alexis Reigel
type: fixed
---
title: Fix Pipeline Triggers to show triggered label and predefined variables (e.g.
CI_PIPELINE_TRIGGERED)
merge_request: 14244
author:
type: fixed
---
title: Issue board tooltips are now the correct width when the column is collapsed
merge_request:
author: Jedidiah Broadbent
type: fixed
---
title: Hide read_registry scope when registry is disabled on instance
merge_request: 13314
author: Robin Bobbitt
---
title: Adds Event polyfill for IE11
merge_request:
author:
type: fixed
---
title: Read import sources from setting at first initialization
merge_request: 14141
author: Visay Keo
type: fixed
---
title: Update native unicode emojis to always render as normal text (previously could render italicized)
merge_request:
author: Branka Martinovic
type: fixed
---
title: Perform prometheus data endpoint requests in parallel
merge_request: 14003
author:
type: fixed
---
title: Replace the profile/emails.feature spinach test with an rspec analog
merge_request: 14172
author: Vitaliy @blackst0ne Klachkov
type: other
---
title: Replace project/group_links.feature spinach test with an rspec analog
merge_request: 14169
author: Vitaliy @blackst0ne Klachkov
type: other
---
title: Replace the project/milestone.feature spinach test with an rspec analog
merge_request: 14171
author: Vitaliy @blackst0ne Klachkov
type: other
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment