Commit 9ebb4c51 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'ce-to-ee-2017-08-30' into 'master'

CE upstream: Wednesday

Closes gitlab-ce#37147, gitlab-ce#36804, gitlab-ce#36633, gitlab-ce#34323, and gitlab-ci-multi-runner#2696

See merge request !2787
parents 1cb77f73 1e059e51
......@@ -74,19 +74,6 @@ stages:
- docker.elastic.co/elasticsearch/elasticsearch:5.5.2
.only-if-want-mysql: &only-if-want-mysql
only:
- /mysql/
- /-stable/
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
- tags@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ee
- tags@gitlab/gitlabhq
- tags@gitlab/gitlab-ee
# Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes.
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
......@@ -130,7 +117,6 @@ stages:
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
<<: *only-if-want-mysql
<<: *except-docs
.spinach-metadata: &spinach-metadata
......@@ -162,7 +148,6 @@ stages:
.spinach-metadata-mysql: &spinach-metadata-mysql
<<: *spinach-metadata
<<: *use-mysql
<<: *only-if-want-mysql
<<: *except-docs
.only-canonical-masters: &only-canonical-masters
......
......@@ -608,13 +608,25 @@ Style/YodaCondition:
Style/Proc:
Enabled: true
# Use `spam?` instead of `is_spam?`
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
# NameWhitelist: is_a?
Style/PredicateName:
Enabled: true
NamePrefixBlacklist: is_
Exclude:
- 'spec/**/*'
- 'features/**/*'
# Metrics #####################################################################
# A calculated magnitude based on number of assignments,
# branches, and conditions.
Metrics/AbcSize:
Enabled: true
Max: 56.96
Max: 55.25
# This cop checks if the length of a block exceeds some maximum value.
Metrics/BlockLength:
......@@ -633,7 +645,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method.
Metrics/CyclomaticComplexity:
Enabled: true
Max: 16
Max: 15
# Limit lines to 80 characters.
Metrics/LineLength:
......@@ -655,7 +667,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity:
Enabled: true
Max: 18
Max: 17
# Lint ########################################################################
......@@ -1186,6 +1198,10 @@ GitlabSecurity/DeepMunge:
- 'lib/**/*.rake'
- 'spec/**/*'
# To be enabled by https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13610
GitlabSecurity/JsonSerialization:
Enabled: false
GitlabSecurity/PublicSend:
Enabled: true
Exclude:
......
......@@ -237,14 +237,6 @@ Style/PercentLiteralDelimiters:
Style/PerlBackrefs:
Enabled: false
# Offense count: 105
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
# NameWhitelist: is_a?
Style/PredicateName:
Enabled: false
# Offense count: 58
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
......
......@@ -165,7 +165,7 @@ Assigning a team label makes sure issues get the attention of the appropriate
people.
The current team labels are ~Build, ~CI, ~Discussion, ~Documentation, ~Edge,
~Gitaly, ~Platform, ~Prometheus, ~Release, and ~"UX".
~Geo, ~Gitaly, ~Platform, ~Prometheus, ~Release, and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
......
......@@ -27,7 +27,7 @@ gem 'doorkeeper-openid_connect', '~> 1.1.0'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.2'
......@@ -136,12 +136,9 @@ gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
gem 'nokogiri', '~> 1.6.7', '>= 1.6.7.2'
gem 'nokogiri', '~> 1.8.0'
# Diffs
gem 'diffy', '~> 3.1.0'
......@@ -255,7 +252,7 @@ gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.0'
gem 'gemojione', '~> 3.3'
gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
......@@ -296,7 +293,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta12'
gem 'prometheus-client-mmap', '~>0.7.0.beta14'
gem 'raindrops', '~> 0.18'
end
......@@ -349,7 +346,7 @@ group :development, :test do
gem 'rubocop', '~> 0.49.1', require: false
gem 'rubocop-rspec', '~> 1.15.1', require: false
gem 'rubocop-gitlab-security', '~> 0.0.6', require: false
gem 'rubocop-gitlab-security', '~> 0.1.0', require: false
gem 'scss_lint', '~> 0.54.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false
......
......@@ -285,7 +285,7 @@ GEM
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
gemojione (3.0.1)
gemojione (3.3.0)
json
get_process_mem (0.2.0)
gettext (3.2.2)
......@@ -307,7 +307,7 @@ GEM
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
rugged (>= 0.23.0b)
github-markup (1.4.0)
github-markup (1.6.1)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
......@@ -328,13 +328,14 @@ GEM
activesupport (>= 4.1.0)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
gollum-lib (4.2.1)
github-markup (~> 1.4.0)
gollum-lib (4.2.7)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (~> 1.6.4)
rouge (~> 2.0)
sanitize (~> 2.1.0)
stringex (~> 2.5.1)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15)
rugged (~> 0.25)
......@@ -354,7 +355,7 @@ GEM
multi_json (~> 1.10)
retriable (~> 1.4)
signet (~> 0.6)
google-protobuf (3.3.0)
google-protobuf (3.4.0.2)
googleauth (0.5.1)
faraday (~> 0.9)
jwt (~> 1.4)
......@@ -496,7 +497,7 @@ GEM
railties (>= 4, < 5.2)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.5)
mail (2.6.6)
mime-types (>= 1.16, < 4)
mail_room (0.9.1)
memoist (0.15.0)
......@@ -505,7 +506,7 @@ GEM
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
mini_portile2 (2.1.0)
mini_portile2 (2.2.0)
minitest (5.7.0)
mmap2 (2.2.7)
mousetrap-rails (1.4.6)
......@@ -520,8 +521,8 @@ GEM
net-ntp (2.1.3)
net-ssh (4.1.0)
netrc (0.11.0)
nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
numerizer (0.1.1)
oauth (0.5.1)
oauth2 (1.4.0)
......@@ -544,9 +545,9 @@ GEM
jwt (~> 1.0)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-cas3 (1.1.3)
omniauth-cas3 (1.1.4)
addressable (~> 2.3)
nokogiri (~> 1.6.6)
nokogiri (~> 1.7, >= 1.7.1)
omniauth (~> 1.2)
omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2)
......@@ -634,7 +635,7 @@ GEM
cliver (~> 0.3.1)
multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
posix-spawn (0.3.11)
posix-spawn (0.3.13)
powerpack (0.1.1)
premailer (1.10.4)
addressable
......@@ -648,7 +649,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta12)
prometheus-client-mmap (0.7.0.beta14)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
coderay (~> 1.1.0)
......@@ -798,7 +799,7 @@ GEM
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.0.6)
rubocop-gitlab-security (0.1.0)
rubocop (>= 0.47.1)
rubocop-rspec (1.15.1)
rubocop (>= 0.42.0)
......@@ -901,7 +902,7 @@ GEM
state_machines-activerecord (0.4.0)
activerecord (>= 4.1, < 5.1)
state_machines-activemodel (>= 0.3.0)
stringex (2.5.2)
stringex (2.7.1)
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
......@@ -920,9 +921,9 @@ GEM
timfel-krb5-auth (0.8.3)
toml-rb (0.3.15)
citrus (~> 3.0, > 3.0)
truncato (0.7.8)
truncato (0.7.10)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
nokogiri (~> 1.8.0, >= 1.7.0)
tzinfo (1.2.3)
thread_safe (~> 0.1)
u2f (0.2.1)
......@@ -1049,7 +1050,7 @@ DEPENDENCIES
foreman (~> 0.78.0)
fuubar (~> 2.2.0)
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0)
gemojione (~> 3.3)
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
......@@ -1098,7 +1099,7 @@ DEPENDENCIES
net-ldap
net-ntp
net-ssh (~> 4.1.0)
nokogiri (~> 1.6.7, >= 1.6.7.2)
nokogiri (~> 1.8.0)
oauth2 (~> 1.4)
octokit (~> 4.6.2)
oj (~> 2.17.4)
......@@ -1106,7 +1107,7 @@ DEPENDENCIES
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.1)
omniauth-azure-oauth2 (~> 0.0.6)
omniauth-cas3 (~> 1.1.2)
omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.2)
......@@ -1131,7 +1132,7 @@ DEPENDENCIES
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta12)
prometheus-client-mmap (~> 0.7.0.beta14)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
......@@ -1163,7 +1164,7 @@ DEPENDENCIES
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
rubocop (~> 0.49.1)
rubocop-gitlab-security (~> 0.0.6)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.15.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
......@@ -1198,7 +1199,7 @@ DEPENDENCIES
thin (~> 1.7.0)
timecop (~> 0.8.0)
toml-rb (~> 0.3.15)
truncato (~> 0.7.8)
truncato (~> 0.7.9)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
unf (~> 0.1.4)
......@@ -1213,4 +1214,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.15.3
1.15.4
......@@ -213,13 +213,13 @@ import initGroupAnalytics from './init_group_analytics';
break;
case 'dashboard:issues':
case 'dashboard:merge_requests':
case 'groups:merge_requests':
new ProjectSelect();
initLegacyFilters();
break;
case 'groups:issues':
case 'groups:merge_requests':
if (filteredSearchEnabled) {
const filteredSearchManager = new gl.FilteredSearchManager('issues');
const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
new ProjectSelect();
......
import Cookies from 'js-cookie';
import bp from './breakpoints';
const HIDE_INTERVAL_TIMEOUT = 300;
......@@ -8,9 +7,11 @@ const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out';
let currentOpenMenu = null;
let menuCornerLocs;
let timeoutId;
let sidebar;
export const mousePos = [];
export const setSidebar = (el) => { sidebar = el; };
export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; };
export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
......@@ -20,10 +21,8 @@ let headerHeight = 50;
export const getHeaderHeight = () => headerHeight;
export const canShowActiveSubItems = (el) => {
const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md';
if (el.classList.contains('active') && !isHiddenByMedia) {
return Cookies.get('sidebar_collapsed') === 'true';
if (el.classList.contains('active') && (sidebar && !sidebar.classList.contains('sidebar-icons-only'))) {
return false;
}
return true;
......@@ -143,13 +142,13 @@ export const documentMouseMove = (e) => {
};
export default () => {
const sidebar = document.querySelector('.sidebar-top-level-items');
sidebar = document.querySelector('.nav-sidebar');
if (!sidebar) return;
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
sidebar.addEventListener('mouseleave', () => {
sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
......
<script>
import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub';
import groupIdenticon from './group_identicon.vue';
export default {
components: {
groupIdenticon,
identicon,
},
props: {
group: {
......@@ -205,7 +205,7 @@ export default {
class="avatar s40"
:src="group.avatarUrl"
/>
<group-identicon
<identicon
v-else
:entity-id=group.id
:entity-name="group.name"
......
......@@ -37,7 +37,7 @@ export default {
Edited
<time-ago-tooltip
v-if="updatedAt"
placement="bottom"
tooltip-placement="bottom"
:time="updatedAt"
/>
<span
......
export const isSticky = (el, scrollY, stickyTop) => {
const top = el.offsetTop - scrollY;
const top = Math.floor(el.offsetTop - scrollY);
if (top <= stickyTop) {
el.classList.add('is-stuck');
......
......@@ -253,6 +253,7 @@ import bp from './breakpoints';
loadDiff(source) {
if (this.diffsLoaded) {
document.dispatchEvent(new CustomEvent('scroll'));
return;
}
......
......@@ -3,8 +3,9 @@
import _ from 'underscore';
import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service';
import monitoringRow from './monitoring_row.vue';
import monitoringState from './monitoring_state.vue';
import GraphGroup from './graph_group.vue';
import GraphRow from './graph_row.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub';
......@@ -31,8 +32,9 @@
},
components: {
monitoringRow,
monitoringState,
GraphGroup,
GraphRow,
EmptyState,
},
methods: {
......@@ -94,7 +96,6 @@
this.updatedAspectRatios = 0;
}
},
},
created() {
......@@ -118,40 +119,27 @@
},
};
</script>
<template>
<div
class="prometheus-graphs"
v-if="!showEmptyState">
<div
class="row"
<div v-if="!showEmptyState" class="prometheus-graphs">
<graph-group
v-for="(groupData, index) in store.groups"
:key="index">
<div
class="col-md-12">
<div
class="panel panel-default prometheus-panel">
<div
class="panel-heading">
<h4>{{groupData.group}}</h4>
</div>
<div
class="panel-body">
<monitoring-row
v-for="(row, index) in groupData.metrics"
:key="index"
:row-data="row"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
/>
</div>
</div>
</div>
</div>
:key="index"
:name="groupData.group"
>
<graph-row
v-for="(row, index) in groupData.metrics"
:key="index"
:row-data="row"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
/>
</graph-group>
</div>
<monitoring-state
<empty-state
v-else
:selected-state="state"
:documentation-path="documentationPath"
:settings-path="settingsPath"
v-else
/>
</template>
......@@ -62,49 +62,33 @@
},
};
</script>
<template>
<div
class="prometheus-state">
<div
class="row">
<div
class="col-md-4 col-md-offset-4 state-svg"
v-html="currentState.svg">
</div>
<div class="prometheus-state">
<div class="row">
<div class="col-md-4 col-md-offset-4 state-svg" v-html="currentState.svg"></div>
</div>
<div
class="row">
<div
class="col-md-6 col-md-offset-3">
<h4
class="text-center state-title">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h4 class="text-center state-title">
{{currentState.title}}
</h4>
</div>
</div>
<div
class="row">
<div
class="col-md-6 col-md-offset-3">
<div
class="description-text text-center state-description">
{{currentState.description}}
<a
:href="settingsPath"
v-if="showButtonDescription">
Prometheus server
</a>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="description-text text-center state-description">
{{currentState.description}}
<a v-if="showButtonDescription" :href="settingsPath">
Prometheus server
</a>
</div>
</div>
</div>
<div
class="row state-button-section">
<div
class="col-md-4 col-md-offset-4 text-center state-button">
<a
class="btn btn-success"
:href="buttonPath">
{{currentState.buttonText}}
<div class="row state-button-section">
<div class="col-md-4 col-md-offset-4 text-center state-button">
<a class="btn btn-success" :href="buttonPath">
{{currentState.buttonText}}
</a>
</div>
</div>
......
<script>
import d3 from 'd3';
import monitoringLegends from './monitoring_legends.vue';
import monitoringFlag from './monitoring_flag.vue';
import monitoringDeployment from './monitoring_deployment.vue';
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
import { formatRelevantDigits } from '../../lib/utils/number_utils';
import { timeScaleFormat } from '../utils/date_time_formatters';
import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left;
export default {
props: {
columnData: {
graphData: {
type: Object,
required: true,
},
......@@ -65,9 +66,9 @@
},
components: {
monitoringLegends,
monitoringFlag,
monitoringDeployment,
GraphLegend,
GraphFlag,
GraphDeployment,
},
computed: {
......@@ -96,7 +97,7 @@
methods: {
draw() {
const breakpointSize = bp.getBreakpointSize();
const query = this.columnData.queries[0];
const query = this.graphData.queries[0];
this.margin = measurements.large.margin;
if (breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300;
......@@ -105,7 +106,7 @@
}
this.data = query.result[0].values;
this.unitOfDisplay = query.unit || '';
this.yAxisLabel = this.columnData.y_label || 'Values';
this.yAxisLabel = this.graphData.y_label || 'Values';
this.legendTitle = query.label || 'Average';
this.graphWidth = this.$refs.baseSvg.clientWidth -
this.margin.left - this.margin.right;
......@@ -159,6 +160,7 @@
const xAxis = d3.svg.axis()
.scale(axisXScale)
.ticks(measurements.xTicks)
.tickFormat(timeScaleFormat)
.orient('bottom');
const yAxis = d3.svg.axis()
......@@ -222,7 +224,7 @@
:class="classType">
<h5
class="text-center graph-title">
{{columnData.title}}
{{graphData.title}}
</h5>
<div
class="prometheus-svg-container"
......@@ -238,7 +240,7 @@
class="y-axis"
transform="translate(70, 20)">
</g>
<monitoring-legends
<graph-legend
:graph-width="graphWidth"
:graph-height="graphHeight"
:margin="margin"
......@@ -266,21 +268,13 @@
stroke-width="2"
transform="translate(-5, 20)">
</path>
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
transform="translate(-5, 20)"
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
<monitoring-deployment
<graph-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<monitoring-flag
<graph-flag
v-if="showFlag"
:current-x-coordinate="currentXCoordinate"
:current-y-coordinate="currentYCoordinate"
......@@ -289,6 +283,14 @@
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
transform="translate(-5, 20)"
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
</svg>
</svg>
</div>
......
<script>
import {
dateFormat,
timeFormat,
} from '../constants';
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
export default {
props: {
......@@ -58,7 +55,7 @@
class="deploy-info"
v-if="showDeployInfo">
<g
v-for="(deployment, index) in deploymentData"
v-for="(deployment, index) in deploymentData"
:key="index"
:class="nameDeploymentClass(deployment)"
:transform="transformDeploymentGroup(deployment)">
......@@ -92,7 +89,7 @@
width="90"
height="58">
</rect>
<g
<g
transform="translate(5, 2)">
<text
class="deploy-info-text text-metric-bold">
......
<script>
import {
dateFormat,
timeFormat,
} from '../constants';
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
export default {
props: {
......@@ -72,7 +69,7 @@
r="5"
transform="translate(-5, 20)">
</circle>
<svg
<svg
class="rect-text-metric"
:x="currentFlagPosition"
y="0">
......
......@@ -74,7 +74,7 @@
};
</script>
<template>
<g
<g
class="axis-label-container">
<line
class="label-x-axis-line"
......@@ -100,7 +100,7 @@
:width="yLabelWidth"
:height="yLabelHeight">
</rect>
<text
<text
class="label-axis-text y-label-text"
text-anchor="middle"
:transform="textTransform"
......
<script>
export default {
props: {
name: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="panel panel-default prometheus-panel">
<div class="panel-heading">
<h4>{{name}}</h4>
</div>
<div class="panel-body">
<slot />
</div>
</div>
</template>
<script>
import monitoringColumn from './monitoring_column.vue';
import Graph from './graph.vue';
export default {
props: {
......@@ -17,7 +17,7 @@
},
},
components: {
monitoringColumn,
Graph,
},
computed: {
bootstrapClass() {
......@@ -26,12 +26,12 @@
},
};
</script>
<template>
<div
class="prometheus-row row">
<monitoring-column
v-for="(column, index) in rowData"
:column-data="column"
<div class="prometheus-row row">
<graph
v-for="(graphData, index) in rowData"
:graph-data="graphData"
:class-type="bootstrapClass"
:key="index"
:update-aspect-ratio="updateAspectRatio"
......
import d3 from 'd3';
export const dateFormat = d3.time.format('%b %d, %Y');
export const timeFormat = d3.time.format('%H:%M%p');
import Vue from 'vue';
import Monitoring from './components/monitoring.vue';
import Dashboard from './components/dashboard.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#prometheus-graphs',
components: {
'monitoring-dashboard': Monitoring,
Dashboard,
},
render: createElement => createElement('monitoring-dashboard'),
render: createElement => createElement('dashboard'),
}));
import d3 from 'd3';
export const dateFormat = d3.time.format('%b %-d, %Y');
export const timeFormat = d3.time.format('%-I:%M%p');
export const timeScaleFormat = d3.time.format.multi([
['.%L', d => d.getMilliseconds()],
[':%S', d => d.getSeconds()],
['%-I:%M', d => d.getMinutes()],
['%-I %p', d => d.getHours()],
['%a %-d', d => d.getDay() && d.getDate() !== 1],
['%b %-d', d => d.getDate() !== 1],
['%B', d => d.getMonth()],
['%Y', () => true],
]);
......@@ -48,7 +48,6 @@ export default class NewNavSidebar {
if (this.$sidebar.length) {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
this.$page.toggleClass('page-with-new-sidebar', !collapsed);
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
}
NewNavSidebar.setCollapsedCookie(collapsed);
......
<script>
export default {
name: 'PipelineNavigationTabs',
props: {
scope: {
type: String,
required: true,
export default {
name: 'PipelineNavigationTabs',
props: {
scope: {
type: String,
required: true,
},
count: {
type: Object,
required: true,
},
paths: {
type: Object,
required: true,
},
},
count: {
type: Object,
required: true,
mounted() {
$(document).trigger('init.scrolling-tabs');
},
paths: {
type: Object,
required: true,
methods: {
shouldRenderBadge(count) {
// 0 is valid in a badge, but evaluates to false, we need to check for undefined
return count !== undefined;
},
},
},
mounted() {
$(document).trigger('init.scrolling-tabs');
},
};
</script>
<template>
......@@ -27,7 +33,9 @@ export default {
:class="{ active: scope === 'all'}">
<a :href="paths.allPath">
All
<span class="badge js-totalbuilds-count">
<span
v-if="shouldRenderBadge(count.all)"
class="badge js-totalbuilds-count">
{{count.all}}
</span>
</a>
......@@ -37,7 +45,9 @@ export default {
:class="{ active: scope === 'pending'}">
<a :href="paths.pendingPath">
Pending
<span class="badge">
<span
v-if="shouldRenderBadge(count.pending)"
class="badge">
{{count.pending}}
</span>
</a>
......@@ -47,7 +57,9 @@ export default {
:class="{ active: scope === 'running'}">
<a :href="paths.runningPath">
Running
<span class="badge">
<span
v-if="shouldRenderBadge(count.running)"
class="badge">
{{count.running}}
</span>
</a>
......@@ -57,7 +69,9 @@ export default {
:class="{ active: scope === 'finished'}">
<a :href="paths.finishedPath">
Finished
<span class="badge">
<span
v-if="shouldRenderBadge(count.finished)"
class="badge">
{{count.finished}}
</span>
</a>
......
......@@ -139,7 +139,9 @@
};
</script>
<template>
<div :class="cssClass">
<div
class="pipelines-container"
:class="cssClass">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
......
......@@ -14,7 +14,14 @@ export default class ProjectSelectComboButton {
bindEvents() {
this.projectSelectInput.siblings('.new-project-item-select-button')
.on('click', this.openDropdown);
.on('click', e => this.openDropdown(e));
this.newItemBtn.on('click', (e) => {
if (!this.getProjectFromLocalStorage()) {
e.preventDefault();
this.openDropdown(e);
}
});
this.projectSelectInput.on('change', () => this.selectProject());
}
......@@ -28,8 +35,9 @@ export default class ProjectSelectComboButton {
}
}
openDropdown() {
$(this).siblings('.project-item-select').select2('open');
// eslint-disable-next-line class-methods-use-this
openDropdown(event) {
$(event.currentTarget).siblings('.project-item-select').select2('open');
}
selectProject() {
......@@ -56,10 +64,8 @@ export default class ProjectSelectComboButton {
if (project) {
this.newItemBtn.attr('href', project.url);
this.newItemBtn.text(`${this.formattedText.defaultTextPrefix} in ${project.name}`);
this.newItemBtn.enable();
} else {
this.newItemBtn.text(`Select project to create ${this.formattedText.presetTextSuffix}`);
this.newItemBtn.disable();
}
}
......
<script>
import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
import tooltip from '../directives/tooltip';
export default {
props: {
......@@ -100,17 +101,22 @@
this.author.username ? `${this.author.username}'s avatar` : null;
},
},
data() {
return { commitIconSvg };
directives: {
tooltip,
},
components: {
userAvatarLink,
},
created() {
this.commitIconSvg = commitIconSvg;
},
};
</script>
<template>
<div class="branch-commit">
<div v-if="hasCommitRef" class="icon-container hidden-xs">
<div
v-if="hasCommitRef"
class="icon-container hidden-xs">
<i
v-if="tag"
class="fa fa-tag"
......@@ -126,7 +132,10 @@
<a
v-if="hasCommitRef"
class="ref-name hidden-xs"
:href="commitRef.ref_url">
:href="commitRef.ref_url"
v-tooltip
data-container="body"
:title="commitRef.name">
{{commitRef.name}}
</a>
......@@ -153,7 +162,8 @@
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
/>
<a class="commit-row-message"
<a
class="commit-row-message"
:href="commitUrl">
{{title}}
</a>
......
......@@ -189,7 +189,7 @@
width: auto;
top: 100%;
left: 0;
z-index: 9;
z-index: 200;
min-width: 240px;
max-width: 500px;
margin-top: 2px;
......@@ -771,6 +771,11 @@
&::before {
top: 16px;
}
&.dropdown-menu-user-link::before {
top: 50%;
transform: translateY(-50%);
}
}
}
}
......@@ -792,3 +797,5 @@
margin-top: 2px;
}
}
@include new-style-dropdown('.js-namespace-select + ');
......@@ -490,3 +490,7 @@
padding: 8px 16px;
text-align: center;
}
.issues-details-filters {
@include new-style-dropdown;
}
......@@ -279,7 +279,10 @@
// TODO: change global style
.ajax-project-dropdown,
body[data-page="projects:new"] #select2-drop {
body[data-page="projects:new"] #select2-drop,
body[data-page="projects:blob:new"] #select2-drop,
body[data-page="profiles:show"] #select2-drop,
body[data-page="projects:blob:edit"] #select2-drop {
&.select2-drop {
color: $gl-text-color;
}
......
......@@ -10,8 +10,7 @@
color: $md-link-color;
}
img {
/*max-width: 100%;*/
img:not(.emoji) {
margin: 0 0 8px;
}
......@@ -26,6 +25,7 @@
min-width: inherit;
min-height: inherit;
background-color: inherit;
max-width: 100%;
}
p a:not(.no-attachment-icon) img {
......
......@@ -121,6 +121,7 @@ $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1.0);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-green: $green-600;
$gl-text-green-hover: $green-700;
$gl-text-red: $red-500;
$gl-text-orange: $orange-600;
$gl-link-color: $blue-600;
......
......@@ -204,6 +204,8 @@
.gitlab-ci-yml-selector,
.dockerfile-selector,
.template-type-selector {
@include new-style-dropdown;
display: inline-block;
vertical-align: top;
font-family: $regular_font;
......
......@@ -102,6 +102,8 @@
}
.member-search-form {
@include new-style-dropdown;
position: relative;
@media (min-width: $screen-sm-min) {
......
......@@ -475,6 +475,8 @@
}
.mr-source-target {
@include new-style-dropdown;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
......@@ -596,6 +598,8 @@
}
.mr-version-controls {
@include new-style-dropdown;
position: relative;
background: $gray-light;
color: $gl-text-color;
......
......@@ -766,17 +766,25 @@ ul.notes {
background-color: transparent;
border: none;
outline: 0;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
&.is-disabled {
cursor: default;
}
&:not(.is-disabled):hover,
&:not(.is-disabled) {
&:hover,
&:focus {
color: $gl-text-green;
}
}
&.is-active {
color: $gl-text-green;
svg {
fill: $gl-text-green;
&:hover,
&:focus {
color: $gl-text-green-hover;
}
}
......
......@@ -14,3 +14,7 @@
font-size: 18px;
}
}
.notification-form {
@include new-style-dropdown;
}
......@@ -1159,3 +1159,7 @@ a.linked-pipeline-mini-item {
}
}
}
.pipelines-container .top-area .nav-controls > .btn:last-child {
float: none;
}
......@@ -99,6 +99,30 @@
.blob-viewer-container {
flex: 1;
overflow: auto;
> div,
.file-content {
display: flex;
}
> div,
.file-content,
.blob-viewer,
.line-number,
.blob-content,
.code {
min-height: 100%;
width: 100%;
}
.line-numbers {
min-width: 44px;
}
.blob-content {
flex: 1;
overflow-x: auto;
}
}
#tabs {
......
......@@ -265,3 +265,7 @@
font-weight: $gl-font-weight-bold;
}
}
.todos-filters {
@include new-style-dropdown;
}
......@@ -59,7 +59,7 @@ class AutocompleteController < ApplicationController
.limit(AWARD_EMOJI_MAX)
.where(user: current_user)
.group(:name)
.order(count: :desc, name: :asc)
.order('count_all DESC, name ASC')
.count
# Transform from hash to array to guarantee json order
......
......@@ -15,7 +15,17 @@ module IssuableCollections
end
def merge_requests_collection
merge_requests_finder.execute.preload(:source_project, :target_project, :author, :assignee, :labels, :milestone, :head_pipeline, target_project: :namespace, merge_request_diff: :merge_request_diff_commits)
merge_requests_finder.execute.preload(
:source_project,
:target_project,
:author,
:assignee,
:labels,
:milestone,
head_pipeline: :project,
target_project: :namespace,
merge_request_diff: :merge_request_diff_commits
)
end
def issues_finder
......
......@@ -35,13 +35,13 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def edit
render_404 if @milestone.is_legacy_group_milestone?
render_404 if @milestone.legacy_group_milestone?
end
def update
# Keep this compatible with legacy group milestones where we have to update
# all projects milestones states at once.
if @milestone.is_legacy_group_milestone?
if @milestone.legacy_group_milestone?
update_params = milestone_params.select { |key| key == "state_event" }
milestones = @milestone.milestones
else
......@@ -67,7 +67,7 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def milestone_path
if @milestone.is_legacy_group_milestone?
if @milestone.legacy_group_milestone?
group_milestone_path(group, @milestone.safe_title, title: @milestone.title)
else
group_milestone_path(group, @milestone.iid)
......
......@@ -4,7 +4,7 @@ class Oauth::GeoAuthController < ActionController::Base
def auth
oauth = Gitlab::Geo::OauthSession.new(state: params[:state])
unless oauth.is_oauth_state_valid?
unless oauth.oauth_state_valid?
redirect_to root_url
return
end
......@@ -14,7 +14,7 @@ class Oauth::GeoAuthController < ActionController::Base
def callback
oauth = Gitlab::Geo::OauthSession.new(state: params[:state])
unless oauth.is_oauth_state_valid?
unless oauth.oauth_state_valid?
redirect_to new_user_session_path
return
end
......
......@@ -205,7 +205,7 @@ class Projects::IssuesController < Projects::ApplicationController
task_status: @issue.task_status
}
if @issue.is_edited?
if @issue.edited?
response[:updated_at] = @issue.updated_at
response[:updated_by_name] = @issue.last_edited_by.name
response[:updated_by_path] = user_path(@issue.last_edited_by)
......
......@@ -327,14 +327,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
elsif @merge_request.head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@merge_request.merge_async(current_user.id, params)
:success
else
:failed
end
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@merge_request.merge_async(current_user.id, params)
:success
end
......
......@@ -181,7 +181,7 @@ module ApplicationHelper
end
def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', exclude_author: false)
return unless object.is_edited?
return unless object.edited?
content_tag :small, class: 'edited-text' do
output = content_tag(:span, 'Edited ')
......
......@@ -237,7 +237,7 @@ module IssuablesHelper
end
def updated_at_by(issuable)
return {} unless issuable.is_edited?
return {} unless issuable.edited?
{
updatedAt: issuable.updated_at.to_time.iso8601,
......
......@@ -196,7 +196,7 @@ module MilestonesHelper
def group_milestone_route(milestone, params = {})
params = nil if params.empty?
if milestone.is_legacy_group_milestone?
if milestone.legacy_group_milestone?
group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params)
else
group_milestone_path(@group, milestone.iid, milestone: params)
......
module MilestonesRoutingHelper
def milestone_path(milestone, *args)
if milestone.is_group_milestone?
if milestone.group_milestone?
group_milestone_path(milestone.group, milestone, *args)
elsif milestone.is_project_milestone?
elsif milestone.project_milestone?
project_milestone_path(milestone.project, milestone, *args)
end
end
def milestone_url(milestone, *args)
if milestone.is_group_milestone?
if milestone.group_milestone?
group_milestone_url(milestone.group, milestone, *args)
elsif milestone.is_project_milestone?
elsif milestone.project_milestone?
project_milestone_url(milestone.project, milestone, *args)
end
end
......
......@@ -397,7 +397,9 @@ module Ci
[
{ key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
{ key: 'GITLAB_USER_EMAIL', value: user.email, public: true }
{ key: 'GITLAB_USER_EMAIL', value: user.email, public: true },
{ key: 'GITLAB_USER_LOGIN', value: user.username, public: true },
{ key: 'GITLAB_USER_NAME', value: user.name, public: true }
]
end
......
......@@ -143,7 +143,7 @@ module Ci
expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: false)
end
def is_runner_queue_value_latest?(value)
def runner_queue_value_latest?(value)
ensure_runner_queue_value == value if value.present?
end
......
module Editable
extend ActiveSupport::Concern
def is_edited?
def edited?
last_edited_at.present? && last_edited_at != created_at
end
......
......@@ -70,19 +70,19 @@ module Milestoneish
due_date && due_date.past?
end
def is_group_milestone?
def group_milestone?
false
end
def is_project_milestone?
def project_milestone?
false
end
def is_legacy_group_milestone?
def legacy_group_milestone?
false
end
def is_dashboard_milestone?
def dashboard_milestone?
false
end
......
......@@ -23,7 +23,7 @@ module ProtectedRef
# If we don't `protected_branch` or `protected_tag` would be empty and
# `project` cannot be delegated to it, which in turn would cause validations
# to fail.
has_many :"#{type}_access_levels", dependent: :destroy, inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
has_many :"#{type}_access_levels", inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }
......
......@@ -3,7 +3,7 @@ class DashboardMilestone < GlobalMilestone
{ authorized_only: true }
end
def is_dashboard_milestone?
def dashboard_milestone?
true
end
end
......@@ -49,7 +49,7 @@ class Deployment < ActiveRecord::Base
# created before then could have a `sha` referring to a commit that no
# longer exists in the repository, so just ignore those.
begin
project.repository.is_ancestor?(commit.id, sha)
project.repository.ancestor?(commit.id, sha)
rescue Rugged::OdbError
false
end
......
......@@ -263,9 +263,9 @@ class Group < Namespace
projects.detect { |project| !project.empty_repo? }
end
def refresh_members_authorized_projects
def refresh_members_authorized_projects(blocking: true)
UserProjectAccessChangedService.new(user_ids_for_project_authorizations)
.execute
.execute(blocking: blocking)
end
def user_ids_for_project_authorizations
......
......@@ -19,7 +19,7 @@ class GroupMilestone < GlobalMilestone
{ group_id: group.id }
end
def is_legacy_group_milestone?
def legacy_group_milestone?
true
end
end
......@@ -249,6 +249,14 @@ class MergeRequest < ActiveRecord::Base
end
end
# Calls `MergeWorker` to proceed with the merge process and
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
def merge_async(user_id, params)
jid = MergeWorker.perform_async(id, user_id, params)
update_column(:merge_jid, jid)
end
def first_commit
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
......@@ -392,9 +400,7 @@ class MergeRequest < ActiveRecord::Base
end
def merge_ongoing?
return false unless merge_jid
Gitlab::SidekiqStatus.num_running([merge_jid]) > 0
!!merge_jid && !merged?
end
def closed_without_fork?
......@@ -844,7 +850,7 @@ class MergeRequest < ActiveRecord::Base
lock_mr
yield
ensure
unlock_mr if locked?
unlock_mr
end
end
......
......@@ -167,7 +167,7 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
def to_reference(from_project = nil, format: :iid, full: false)
return if is_group_milestone? && format != :name
return if group_milestone? && format != :name
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
......@@ -211,11 +211,11 @@ class Milestone < ActiveRecord::Base
group || project
end
def is_group_milestone?
def group_milestone?
group_id.present?
end
def is_project_milestone?
def project_milestone?
project_id.present?
end
......
......@@ -152,14 +152,14 @@ module Network
end
def find_free_parent_space(range, space_base, space_step, space_default)
if is_overlap?(range, space_default)
if overlap?(range, space_default)
find_free_space(range, space_step, space_base, space_default)
else
space_default
end
end
def is_overlap?(range, overlap_space)
def overlap?(range, overlap_space)
range.each do |i|
if i != range.first &&
i != range.last &&
......
......@@ -27,46 +27,45 @@ class NotificationRecipient
@notification_setting ||= find_notification_setting
end
def raw_notification_level
notification_setting&.level&.to_sym
end
def notification_level
# custom is treated the same as watch if it's enabled - otherwise it's
# set to :custom, meaning to send exactly when our type is :participating
# or :mention.
@notification_level ||=
case raw_notification_level
when :custom
if @custom_action && notification_setting&.event_enabled?(@custom_action)
:watch
else
:custom
end
else
raw_notification_level
end
@notification_level ||= notification_setting&.level&.to_sym
end
def notifiable?
return false unless has_access?
return false if own_activity?
return true if @type == :subscription
return false if notification_level.nil? || notification_level == :disabled
return %i[participating mention].include?(@type) if notification_level == :custom
# even users with :disabled notifications receive manual subscriptions
return !unsubscribed? if @type == :subscription
return false if %i[watch participating].include?(notification_level) && excluded_watcher_action?
return false unless NotificationSetting.levels[notification_level] <= NotificationSetting.levels[@type]
return false unless suitable_notification_level?
# check this last because it's expensive
# nobody should receive notifications if they've specifically unsubscribed
return false if unsubscribed?
true
end
def suitable_notification_level?
case notification_level
when :disabled, nil
false
when :custom
custom_enabled? || %i[participating mention].include?(@type)
when :watch, :participating
!excluded_watcher_action?
when :mention
@type == :mention
else
false
end
end
def custom_enabled?
@custom_action && notification_setting&.event_enabled?(@custom_action)
end
def unsubscribed?
return false unless @target
return false unless @target.respond_to?(:subscriptions)
......@@ -98,7 +97,7 @@ class NotificationRecipient
def excluded_watcher_action?
return false unless @custom_action
return false if raw_notification_level == :custom
return false if notification_level == :custom
NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
end
......
......@@ -101,9 +101,9 @@ class ChatNotificationService < Service
when "push", "tag_push"
ChatMessage::PushMessage.new(data)
when "issue"
ChatMessage::IssueMessage.new(data) unless is_update?(data)
ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
ChatMessage::MergeMessage.new(data) unless is_update?(data)
ChatMessage::MergeMessage.new(data) unless update?(data)
when "note"
ChatMessage::NoteMessage.new(data)
when "pipeline"
......@@ -136,7 +136,7 @@ class ChatNotificationService < Service
project.web_url
end
def is_update?(data)
def update?(data)
data[:object_attributes][:action] == 'update'
end
......
......@@ -85,9 +85,9 @@ class HipchatService < Service
when "push", "tag_push"
create_push_message(data)
when "issue"
create_issue_message(data) unless is_update?(data)
create_issue_message(data) unless update?(data)
when "merge_request"
create_merge_request_message(data) unless is_update?(data)
create_merge_request_message(data) unless update?(data)
when "note"
create_note_message(data)
when "pipeline"
......@@ -283,7 +283,7 @@ class HipchatService < Service
"<a href=\"#{project_url}\">#{project_name}</a>"
end
def is_update?(data)
def update?(data)
data[:object_attributes][:action] == 'update'
end
......
......@@ -79,6 +79,10 @@ class Repository
@project = project
end
def ==(other)
@disk_path == other.disk_path
end
def raw_repository
return nil unless full_path
......@@ -94,6 +98,10 @@ class Repository
)
end
def inspect
"#<#{self.class.name}:#{@disk_path}>"
end
#
# Git repository can contains some hidden refs like:
# /refs/notes/*
......@@ -982,7 +990,7 @@ class Repository
if branch_commit
same_head = branch_commit.id == root_ref_commit.id
!same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
!same_head && ancestor?(branch_commit.id, root_ref_commit.id)
else
nil
end
......@@ -1030,7 +1038,7 @@ class Repository
upstream_commit = commit("refs/remotes/#{MIRROR_REMOTE}/#{branch_name}")
if upstream_commit
is_ancestor?(branch_commit.id, upstream_commit.id)
ancestor?(branch_commit.id, upstream_commit.id)
else
false
end
......@@ -1044,12 +1052,12 @@ class Repository
nil
end
def is_ancestor?(ancestor_id, descendant_id)
def ancestor?(ancestor_id, descendant_id)
return false if ancestor_id.nil? || descendant_id.nil?
Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
if is_enabled
raw_repository.is_ancestor?(ancestor_id, descendant_id)
raw_repository.ancestor?(ancestor_id, descendant_id)
else
rugged_is_ancestor?(ancestor_id, descendant_id)
end
......@@ -1078,25 +1086,22 @@ class Repository
end
def with_repo_branch_commit(start_repository, start_branch_name)
return yield(nil) if start_repository.empty_repo?
return yield nil if start_repository.empty_repo?
branch_name_or_sha =
if start_repository == self
start_branch_name
else
tmp_ref = fetch_ref(
start_repository.path_to_repo,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
"refs/tmp/#{SecureRandom.hex}/head"
)
if start_repository == self
yield commit(start_branch_name)
else
sha = start_repository.commit(start_branch_name).sha
start_repository.commit(start_branch_name).sha
if branch_commit = commit(sha)
yield branch_commit
else
with_repo_tmp_commit(
start_repository, start_branch_name, sha) do |tmp_commit|
yield tmp_commit
end
end
yield(commit(branch_name_or_sha))
ensure
rugged.references.delete(tmp_ref) if tmp_ref
end
end
def add_remote(name, url)
......@@ -1113,7 +1118,7 @@ class Repository
end
def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false)
gitlab_shell.fetch_remote(repository_storage_path, disk_path, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
end
def fetch_ref(source_path, source_ref, target_ref)
......
......@@ -5,6 +5,7 @@ class User < ActiveRecord::Base
include Gitlab::ConfigHelper
include Gitlab::CurrentSettings
include Gitlab::SQL::Pattern
include Avatarable
include Referable
include Sortable
......@@ -317,7 +318,7 @@ class User < ActiveRecord::Base
# Returns an ActiveRecord::Relation.
def search(query)
table = arel_table
pattern = "%#{query}%"
pattern = User.to_pattern(query)
order = <<~SQL
CASE
......
......@@ -19,13 +19,13 @@ class ProjectPolicy < BasePolicy
desc "Project has public builds enabled"
condition(:public_builds, scope: :subject) { project.public_builds? }
# For guest access we use #is_team_member? so we can use
# For guest access we use #team_member? so we can use
# project.members, which gets cached in subject scope.
# This is safe because team_access_level is guaranteed
# by ProjectAuthorization's validation to be at minimum
# GUEST
desc "User has guest access"
condition(:guest) { is_team_member? }
condition(:guest) { team_member? }
desc "User has reporter access"
condition(:reporter) { team_access_level >= Gitlab::Access::REPORTER }
......@@ -296,7 +296,7 @@ class ProjectPolicy < BasePolicy
private
def is_team_member?
def team_member?
return false if @user.nil?
greedy_load_subject = false
......
......@@ -7,7 +7,7 @@ class AkismetService
@options = options
end
def is_spam?
def spam?
return false unless akismet_enabled?
params = {
......
......@@ -15,7 +15,7 @@ module Ci
pipeline_schedule: schedule
)
result = validate(current_user || trigger_request.trigger.owner,
result = validate(current_user,
ignore_skip_ci: ignore_skip_ci,
save_on_errors: save_on_errors,
mirror_update: mirror_update)
......
......@@ -30,7 +30,7 @@ class GitPushService < BaseService
@project.repository.after_create_branch
# Re-find the pushed commits.
if is_default_branch?
if default_branch?
# Initial push to the default branch. Take the full history of that branch as "newly pushed".
process_default_branch
else
......@@ -50,10 +50,10 @@ class GitPushService < BaseService
# Update the bare repositories info/attributes file using the contents of the default branches
# .gitattributes file
update_gitattributes if is_default_branch?
update_gitattributes if default_branch?
end
if current_application_settings.elasticsearch_indexing? && is_default_branch?
if current_application_settings.elasticsearch_indexing? && default_branch?
ElasticCommitIndexerWorker.perform_async(@project.id, params[:oldrev], params[:newrev])
end
......@@ -71,7 +71,7 @@ class GitPushService < BaseService
end
def update_caches
if is_default_branch?
if default_branch?
if push_to_new_branch?
# If this is the initial push into the default branch, the file type caches
# will already be reset as a result of `Project#change_head`.
......@@ -113,7 +113,7 @@ class GitPushService < BaseService
# Schedules processing of commit messages.
def process_commit_messages
default = is_default_branch?
default = default_branch?
@push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit|
if commit.matches_cross_reference_regex?
......@@ -216,7 +216,7 @@ class GitPushService < BaseService
Gitlab::Git.branch_ref?(params[:ref])
end
def is_default_branch?
def default_branch?
Gitlab::Git.branch_ref?(params[:ref]) &&
(Gitlab::Git.ref_name(params[:ref]) == project.default_branch || project.default_branch.nil?)
end
......
......@@ -33,10 +33,12 @@ module MergeRequests
merge_request.in_locked_state do
if commit
after_merge
clean_merge_jid
success
end
end
rescue MergeError => e
clean_merge_jid
log_merge_error(e.message, save_message_on_model: true)
end
......@@ -99,6 +101,10 @@ module MergeRequests
end
end
def clean_merge_jid
merge_request.update_column(:merge_jid, nil)
end
def branch_deletion_user
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
......
......@@ -30,7 +30,7 @@ module MergeRequests
next
end
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params)
end
end
......
......@@ -95,7 +95,7 @@ module MergeRequests
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, {})
merge_request.merge_async(current_user.id, {})
end
end
......
module Milestones
class CloseService < Milestones::BaseService
def execute(milestone)
if milestone.close && milestone.is_project_milestone?
if milestone.close && milestone.project_milestone?
event_service.close_milestone(milestone, current_user)
end
......
......@@ -3,7 +3,7 @@ module Milestones
def execute
milestone = parent.milestones.new(params)
if milestone.save && milestone.is_project_milestone?
if milestone.save && milestone.project_milestone?
event_service.open_milestone(milestone, current_user)
end
......
module Milestones
class DestroyService < Milestones::BaseService
def execute(milestone)
return unless milestone.is_project_milestone?
return unless milestone.project_milestone?
Milestone.transaction do
update_params = { milestone: nil }
......
module Milestones
class ReopenService < Milestones::BaseService
def execute(milestone)
if milestone.activate && milestone.is_project_milestone?
if milestone.activate && milestone.project_milestone?
event_service.reopen_milestone(milestone, current_user)
end
......
......@@ -95,7 +95,7 @@ module NotificationRecipientService
def add_participants(user)
return unless target.respond_to?(:participants)
self << [target.participants(user), :watch]
self << [target.participants(user), :participating]
end
# Get project/group users with CUSTOM notification level
......
......@@ -106,10 +106,7 @@ module Projects
event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create)
unless @project.group || @project.gitlab_project_import?
owners = [current_user, @project.namespace.owner].compact.uniq
@project.add_master(owners, current_user: current_user)
end
setup_authorizations
# EE-only
create_predefined_push_rule
......@@ -117,6 +114,18 @@ module Projects
@project.group&.refresh_members_authorized_projects
end
# Refresh the current user's authorizations inline (so they can access the
# project immediately after this request completes), and any other affected
# users in the background
def setup_authorizations
if @project.group
@project.group.refresh_members_authorized_projects(blocking: false)
current_user.refresh_authorized_projects
else
@project.add_master(@project.namespace.owner, current_user: current_user)
end
end
def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
......
......@@ -45,7 +45,7 @@ class SpamService
def check(api)
return false unless request && check_for_spam?
return false unless akismet.is_spam?
return false unless akismet.spam?
create_spam_log(api)
true
......
......@@ -142,7 +142,7 @@ module SystemNoteService
#
# Returns the created Note object
def change_milestone(noteable, project, author, milestone)
format = milestone&.is_group_milestone? ? :name : :iid
format = milestone&.group_milestone? ? :name : :iid
body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
......
......@@ -2,47 +2,16 @@ module TestHooks
class SystemService < TestHooks::BaseService
private
def project
@project ||= begin
project = Project.first
throw(:validation_error, 'Ensure that at least one project exists.') unless project
project
end
end
def push_events_data
if project.empty_repo?
throw(:validation_error, "Ensure project \"#{project.human_name}\" has commits.")
end
Gitlab::DataBuilder::Push.build_sample(project, current_user)
Gitlab::DataBuilder::Push.sample_data
end
def tag_push_events_data
if project.repository.tags.empty?
throw(:validation_error, "Ensure project \"#{project.human_name}\" has tags.")
end
Gitlab::DataBuilder::Push.build_sample(project, current_user)
Gitlab::DataBuilder::Push.sample_data
end
def repository_update_events_data
commit = project.commit
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
unless commit
throw(:validation_error, "Ensure project \"#{project.human_name}\" has commits.")
end
change = Gitlab::DataBuilder::Repository.single_change(
commit.parent_id || Gitlab::Git::BLANK_SHA,
commit.id,
ref
)
Gitlab::DataBuilder::Repository.update(project, current_user, [change], [ref])
Gitlab::DataBuilder::Repository.sample_data
end
end
end
......@@ -5,7 +5,13 @@ class UserProjectAccessChangedService
@user_ids = Array.wrap(user_ids)
end
def execute
AuthorizedProjectsWorker.bulk_perform_and_wait(@user_ids.map { |id| [id] })
def execute(blocking: true)
bulk_args = @user_ids.map { |id| [id] }
if blocking
AuthorizedProjectsWorker.bulk_perform_and_wait(bulk_args)
else
AuthorizedProjectsWorker.bulk_perform_async(bulk_args)
end
end
end
- page_title "Merge Requests"
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
- if show_new_nav? && current_user
- content_for :breadcrumbs_extra do
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
......@@ -13,7 +17,7 @@
.nav-controls{ class: ("visible-xs" if show_new_nav?) }
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
= render 'shared/issuable/filter', type: :merge_requests
= render 'shared/issuable/search_bar', type: :merge_requests
.row-content-block.second-block
Only merge requests from
......
= render "header_title"
= render 'shared/milestones/top', milestone: @milestone, group: @group
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true if @milestone.is_legacy_group_milestone?
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true if @milestone.legacy_group_milestone?
= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102
- breadcrumb_link = breadcrumb_title_link
- container = @no_breadcrumb_container ? 'container-fluid' : container_class
- hide_top_links = @hide_top_links || false
%nav.breadcrumbs{ role: "navigation" }
.breadcrumbs-container{ class: [container_class, @content_class] }
.breadcrumbs-container{ class: [container, @content_class] }
- if defined?(@new_sidebar)
= button_tag class: 'toggle-mobile-nav', type: 'button' do
%span.sr-only Open sidebar
......
......@@ -12,7 +12,7 @@
Add a GPG key
%p.profile-settings-content
Before you can add a GPG key you need to
= link_to 'generate it.', help_page_path('user/project/gpg_signed_commits/index.md')
= link_to 'generate it.', help_page_path('user/project/repository/gpg_signed_commits/index.md')
= render 'form'
%hr
%h5
......
- board = local_assigns.fetch(:board, nil)
- @no_breadcrumb_container = true
- @no_container = true
- @content_class = "issue-boards-content js-focus-mode-board"
- page_title "Boards"
......
......@@ -12,7 +12,7 @@
%span.monospace= signature.gpg_key_primary_keyid
= link_to('Learn more about signing commits', help_page_path('user/project/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
= link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
= label
......@@ -68,9 +68,10 @@
- if git_import_enabled?
%button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL')
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
.row
.col-lg-12
......
......@@ -26,8 +26,12 @@
":title" => "buttonText",
":ref" => "'button'" }
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
%div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
= icon('spin spinner', 'v-if' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
%div{ 'v-else' => '' }
%template{ 'v-if' => 'isResolved' }
= render 'shared/icons/icon_status_success_solid.svg'
%template{ 'v-else' => '' }
= render 'shared/icons/icon_status_success.svg'
- if current_user
- if note.emoji_awardable?
......
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg>
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg>
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></svg>
......@@ -5,7 +5,7 @@
.row
.col-sm-6
%strong= link_to truncate(milestone.title, length: 100), milestone_path
- if milestone.is_group_milestone?
- if milestone.group_milestone?
%span - Group Milestone
- else
%span - Project Milestone
......@@ -18,10 +18,10 @@
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone)
- if milestone.is_a?(GlobalMilestone) || milestone.is_group_milestone?
- if milestone.is_a?(GlobalMilestone) || milestone.group_milestone?
.row
.col-sm-6
- if milestone.is_legacy_group_milestone?
- if milestone.legacy_group_milestone?
.expiration= render('shared/milestone_expired', milestone: milestone)
.projects
- milestone.milestones.each do |milestone|
......@@ -31,7 +31,7 @@
- if @group
.col-sm-6.milestone-actions
- if can?(current_user, :admin_milestones, @group)
- if milestone.is_group_milestone?
- if milestone.group_milestone?
= link_to edit_group_milestone_path(@group, milestone), class: "btn btn-xs btn-grouped" do
Edit
\
......
......@@ -22,7 +22,7 @@
- if group
.pull-right
- if can?(current_user, :admin_milestones, group)
- if milestone.is_group_milestone?
- if milestone.group_milestone?
= link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
Edit
- if milestone.active?
......@@ -33,7 +33,7 @@
.detail-page-description.milestone-detail
%h2.title
= markdown_field(milestone, :title)
- if @milestone.is_group_milestone? && @milestone.description.present?
- if @milestone.group_milestone? && @milestone.description.present?
%div
.description
.wiki
......@@ -44,7 +44,7 @@
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
- if @milestone.is_legacy_group_milestone? || @milestone.is_dashboard_milestone?
- if @milestone.legacy_group_milestone? || @milestone.dashboard_milestone?
.table-holder
%table.table
%thead
......@@ -67,7 +67,7 @@
Open
%td
= ms.expires_at
- elsif @milestone.is_group_milestone?
- elsif @milestone.group_milestone?
%br
View
= link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title)
......
......@@ -4,20 +4,40 @@ class AuthorizedProjectsWorker
# Schedules multiple jobs and waits for them to be completed.
def self.bulk_perform_and_wait(args_list)
# Short-circuit: it's more efficient to do small numbers of jobs inline
return bulk_perform_inline(args_list) if args_list.size <= 3
waiter = Gitlab::JobWaiter.new(args_list.size)
# Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]]
# into [[1, "key"], [2, "key"], [3, "key"]]
waiting_args_list = args_list.map { |args| args << waiter.key }
waiting_args_list = args_list.map { |args| [*args, waiter.key] }
bulk_perform_async(waiting_args_list)
waiter.wait
end
# Schedules multiple jobs to run in sidekiq without waiting for completion
def self.bulk_perform_async(args_list)
Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
end
# Performs multiple jobs directly. Failed jobs will be put into sidekiq so
# they can benefit from retries
def self.bulk_perform_inline(args_list)
failed = []
args_list.each do |args|
begin
new.perform(*args)
rescue
failed << args
end
end
bulk_perform_async(failed) if failed.present?
end
def perform(user_id, notify_key = nil)
user = User.find_by(id: user_id)
......
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.
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