Commit 06fb09da authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into 3119-improve-error-recovery

parents 046ad504 49a2f7e0
......@@ -479,7 +479,7 @@ db:migrate:reset-mysql:
variables:
SETUP_DB: "false"
script:
- git fetch origin v9.3.0-ee
- git fetch https://gitlab.com/gitlab-org/gitlab-ee.git v9.3.0-ee
- git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS
- cp config/gitlab.yml.example config/gitlab.yml
......
......@@ -112,7 +112,7 @@ linters:
# Reports when you define the same selector twice in a single sheet.
MergeableSelector:
enabled: false
enabled: true
# Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores.
......
......@@ -4,7 +4,6 @@ entry.
## 10.1.1 (2017-10-31)
- No changes.
- [FIXED] Auto Devops kubernetes default namespace is now correctly built out of gitlab project group-name. !14642 (Mircea Danila Dumitrescu)
- [FIXED] Forbid the usage of `Redis#keys`. !14889
- [FIXED] Make the circuitbreaker more robust by adding higher thresholds, and multiple access attempts. !14933
......
......@@ -7,7 +7,7 @@ const Api = {
groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json',
projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels',
groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
......
/* globals Flash */
import Visibility from 'visibilityjs';
import axios from 'axios';
import setAxiosCsrfToken from './lib/utils/axios_utils';
import Poll from './lib/utils/poll';
import { s__ } from './locale';
import initSettingsPanels from './settings_panels';
......@@ -17,6 +18,7 @@ import Flash from './flash';
class ClusterService {
constructor(options = {}) {
this.options = options;
setAxiosCsrfToken();
}
fetchData() {
return axios.get(this.options.endpoint);
......
......@@ -8,6 +8,7 @@ const unknownClass = 'geo-node-unknown';
const healthyIcon = 'fa-check';
const unhealthyIcon = 'fa-times';
const unknownIcon = 'fa-times';
const notAvailable = 'Not Available';
class GeoNodeStatus {
constructor(el) {
......@@ -49,7 +50,19 @@ class GeoNodeStatus {
}
static formatCountAndPercentage(count, total, percentage) {
return `${gl.text.addDelimiter(count)}/${gl.text.addDelimiter(total)} (${percentage})`;
if (count !== null || total != null) {
return `${gl.text.addDelimiter(count)}/${gl.text.addDelimiter(total)} (${percentage})`;
}
return notAvailable;
}
static formatCount(count) {
if (count !== null) {
gl.text.addDelimiter(count);
}
return notAvailable;
}
getStatus() {
......@@ -73,21 +86,21 @@ class GeoNodeStatus {
status.repositories_count,
status.repositories_synced_in_percentage);
const repoFailedText = gl.text.addDelimiter(status.repositories_failed_count);
const repoFailedText = GeoNodeStatus.formatCount(status.repositories_failed_count);
const lfsText = GeoNodeStatus.formatCountAndPercentage(
status.lfs_objects_synced_count,
status.lfs_objects_count,
status.lfs_objects_synced_in_percentage);
const lfsFailedText = gl.text.addDelimiter(status.lfs_objects_failed_count);
const lfsFailedText = GeoNodeStatus.formatCount(status.lfs_objects_failed_count);
const attachmentText = GeoNodeStatus.formatCountAndPercentage(
status.attachments_synced_count,
status.attachments_count,
status.attachments_synced_in_percentage);
const attachmentFailedText = gl.text.addDelimiter(status.attachments_failed_count);
const attachmentFailedText = GeoNodeStatus.formatCount(status.attachments_failed_count);
this.$repositoriesSynced.text(repoText);
this.$repositoriesFailed.text(repoFailedText);
......@@ -96,14 +109,14 @@ class GeoNodeStatus {
this.$attachmentsSynced.text(attachmentText);
this.$attachmentsFailed.text(attachmentFailedText);
let eventDate = 'N/A';
let cursorDate = 'N/A';
let eventDate = notAvailable;
let cursorDate = notAvailable;
if (status.last_event_timestamp !== null) {
if (status.last_event_timestamp !== null && status.last_event_timestamp > 0) {
eventDate = gl.utils.formatDate(new Date(status.last_event_timestamp * 1000));
}
if (status.cursor_last_event_timestamp !== null) {
if (status.cursor_last_event_timestamp !== null && status.cursor_last_event_timestamp > 0) {
cursorDate = gl.utils.formatDate(new Date(status.cursor_last_event_timestamp * 1000));
}
......
import axios from 'axios';
import csrf from './csrf';
export default function setAxiosCsrfToken() {
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
}
import PipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
import linkedPipelinesMiniList from '../../vue_shared/components/linked_pipelines_mini_list.vue';
export default {
name: 'MRWidgetPipeline',
props: {
mr: { type: Object, required: true },
},
components: {
'pipeline-stage': PipelineStage,
ciIcon,
icon,
linkedPipelinesMiniList,
},
computed: {
hasPipeline() {
return this.mr.pipeline && Object.keys(this.mr.pipeline).length > 0;
},
hasCIError() {
const { hasCI, ciStatus } = this.mr;
return hasCI && !ciStatus;
},
stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
},
status() {
return this.mr.pipeline.details.status || {};
},
/* We typically set defaults ([]) in the store or prop declarations, but because triggered
* and triggeredBy are appended to `pipeline`, we can't set defaults in the store, and we
* need to check their length here to prevent initializing linked-pipeline-mini-lists
* unneccessarily. */
triggered() {
return this.mr.pipeline.triggered || [];
},
triggeredBy() {
const response = this.mr.pipeline.triggered_by;
return response ? [response] : [];
},
},
template: `
<div
v-if="hasPipeline || hasCIError"
class="mr-widget-heading">
<div class="ci-widget media">
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
<span
aria-hidden="true">
<icon
name="status_failed"/>
</span>
</div>
<div class="media-body">
Could not connect to the CI server. Please check your settings and try again
</div>
</template>
<template v-else-if="hasPipeline">
<div class="ci-status-icon append-right-10">
<a
class="icon-link"
:href="this.status.details_path">
<ci-icon :status="status" />
</a>
</div>
<div class="media-body">
<span>
Pipeline
<a
:href="mr.pipeline.path"
class="pipeline-id">#{{mr.pipeline.id}}</a>
</span>
<span class="mr-widget-pipeline-graph">
<span class="stage-cell">
<linked-pipelines-mini-list
v-if="triggeredBy.length"
:triggered-by="triggeredBy"
/>
<div
v-if="mr.pipeline.details.stages.length > 0"
v-for="(stage, index) in mr.pipeline.details.stages"
class="stage-container dropdown js-mini-pipeline-graph"
:class="{
'has-downstream': index === mr.pipeline.details.stages.length - 1 && triggered.length
}">
<pipeline-stage :stage="stage" />
</div>
<linked-pipelines-mini-list
v-if="triggered.length"
:triggered="triggered"
/>
</span>
</span>
<span>
{{mr.pipeline.details.status.label}} for
<a
:href="mr.pipeline.commit.commit_path"
class="commit-sha js-commit-link">
{{mr.pipeline.commit.short_id}}</a>.
</span>
<span
v-if="mr.pipeline.coverage"
class="js-mr-coverage">
Coverage {{mr.pipeline.coverage}}%
</span>
</div>
</template>
</div>
</div>
`,
};
<script>
import pipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
import linkedPipelinesMiniList from '../../vue_shared/components/linked_pipelines_mini_list.vue';
export default {
name: 'MRWidgetPipeline',
props: {
pipeline: {
type: Object,
required: true,
},
// This prop needs to be camelCase, html attributes are case insensive
// https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
hasCi: {
type: Boolean,
required: false,
},
ciStatus: {
type: String,
required: false,
},
},
components: {
pipelineStage,
ciIcon,
icon,
linkedPipelinesMiniList,
},
computed: {
hasPipeline() {
return this.pipeline && Object.keys(this.pipeline).length > 0;
},
hasCIError() {
return this.hasCi && !this.ciStatus;
},
status() {
return this.pipeline.details &&
this.pipeline.details.status ? this.pipeline.details.status : {};
},
hasStages() {
return this.pipeline.details &&
this.pipeline.details.stages &&
this.pipeline.details.stages.length;
},
/* We typically set defaults ([]) in the store or prop declarations, but because triggered
* and triggeredBy are appended to `pipeline`, we can't set defaults in the store, and we
* need to check their length here to prevent initializing linked-pipeline-mini-lists
* unneccessarily. */
triggered() {
return this.pipeline.triggered || [];
},
triggeredBy() {
const response = this.pipeline.triggered_by;
return response ? [response] : [];
},
},
};
</script>
<template>
<div
v-if="hasPipeline || hasCIError"
class="mr-widget-heading">
<div class="ci-widget media">
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
<icon
name="status_failed"/>
</div>
<div class="media-body">
Could not connect to the CI server. Please check your settings and try again
</div>
</template>
<template v-else-if="hasPipeline">
<a
class="append-right-10"
:href="this.status.details_path">
<ci-icon :status="status" />
</a>
<div class="media-body">
Pipeline
<a
:href="pipeline.path"
class="pipeline-id">
#{{pipeline.id}}
</a>
{{pipeline.details.status.label}} for
<a
:href="pipeline.commit.commit_path"
class="commit-sha js-commit-link">
{{pipeline.commit.short_id}}</a>.
<span class="mr-widget-pipeline-graph">
<span class="stage-cell">
<linked-pipelines-mini-list
v-if="triggeredBy.length"
:triggered-by="triggeredBy"
/>
<div
v-if="hasStages"
v-for="(stage, i) in pipeline.details.stages"
:key="i"
class="stage-container dropdown js-mini-pipeline-graph"
:class="{
'has-downstream': i === pipeline.details.stages.length - 1 && triggered.length
}">
<pipeline-stage :stage="stage" />
</div>
<linked-pipelines-mini-list
v-if="triggered.length"
:triggered="triggered"
/>
</span>
</span>
<template v-if="pipeline.coverage">
Coverage {{pipeline.coverage}}%
</template>
</div>
</template>
</div>
</div>
</template>
......@@ -13,7 +13,7 @@ export { default as Vue } from 'vue';
export { default as SmartInterval } from '~/smart_interval';
export { default as WidgetHeader } from './components/mr_widget_header';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
export { default as WidgetPipeline } from './components/mr_widget_pipeline';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links';
export { default as MergedState } from './components/states/mr_widget_merged';
......
......@@ -233,7 +233,10 @@ export default {
<mr-widget-header :mr="mr" />
<mr-widget-pipeline
v-if="shouldRenderPipelines"
:mr="mr" />
:pipeline="mr.pipeline"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
/>
<mr-widget-deployment
v-if="shouldRenderDeployments"
:mr="mr"
......
......@@ -40,6 +40,10 @@
&.top-block {
border-top: 0;
.container-fluid {
background-color: inherit;
}
}
&.middle-block {
......@@ -98,10 +102,6 @@
background-color: $white-light;
border-top: 0;
}
&.top-block .container-fluid {
background-color: inherit;
}
}
.sub-header-block {
......
......@@ -12,15 +12,15 @@
border-left: 3px solid $border-color;
color: $text-color;
background: $gray-light;
}
.bs-callout h4 {
margin-top: 0;
margin-bottom: 5px;
}
h4 {
margin-top: 0;
margin-bottom: 5px;
}
.bs-callout p:last-child {
margin-bottom: 0;
p:last-child {
margin-bottom: 0;
}
}
/* Variations */
......
......@@ -56,6 +56,14 @@ hr {
.str-truncated {
@include str-truncated;
&-60 {
@include str-truncated(60%);
}
&-100 {
@include str-truncated(100%);
}
}
.block-truncated {
......@@ -85,10 +93,17 @@ hr {
font-size: 14px;
}
table a code {
position: relative;
top: -2px;
margin-right: 3px;
table {
a code {
position: relative;
top: -2px;
margin-right: 3px;
}
td.permission-x {
background: $table-permission-x-bg !important;
text-align: center;
}
}
.loading {
......@@ -277,13 +292,6 @@ img.emoji {
margin: 5px 0;
}
table {
td.permission-x {
background: $table-permission-x-bg !important;
text-align: center;
}
}
.btn-sign-in {
text-shadow: none;
......@@ -349,10 +357,11 @@ table {
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
}
.dropzone .dz-preview .dz-progress .dz-upload {
background: $gl-success !important;
.dz-upload {
background: $gl-success !important;
}
}
.dz-message {
......@@ -413,16 +422,6 @@ table {
border-radius: $border-radius-default;
}
.str-truncated {
&-60 {
@include str-truncated(60%);
}
&-100 {
@include str-truncated(100%);
}
}
.tooltip {
.tooltip-inner {
word-wrap: break-word;
......
......@@ -141,15 +141,15 @@
svg {
fill: $gl-text-color-secondary;
}
}
.nav-item-name {
flex: 1;
}
.nav-item-name {
flex: 1;
}
li.active {
> a {
font-weight: $gl-font-weight-bold;
&.active {
> a {
font-weight: $gl-font-weight-bold;
}
}
}
......
......@@ -728,11 +728,11 @@
.pika-single.animate-picker.is-bound {
@include set-visible;
}
.pika-single.animate-picker.is-bound.is-hidden {
@include set-invisible;
overflow: hidden;
&.is-hidden {
@include set-invisible;
overflow: hidden;
}
}
@mixin dropdown-item-hover {
......@@ -939,9 +939,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
border-right: 0;
}
}
}
.projects-dropdown-container {
.projects-list-frequent-container,
.projects-list-search-container, {
padding: 8px 0;
......@@ -952,11 +950,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
.projects-list-frequent-container li.section-empty,
.projects-list-search-container li.section-empty {
padding: 0 15px;
}
.section-header,
.projects-list-frequent-container li.section-empty,
.projects-list-search-container li.section-empty {
color: $gl-text-color-secondary;
font-size: $gl-font-size;
}
......
......@@ -165,22 +165,36 @@
&:last-child {
border-right: 0;
}
}
td.blame-commit {
padding: 5px 10px;
min-width: 400px;
max-width: 400px;
background: $gray-light;
border-left: 3px solid;
&.blame-commit {
padding: 5px 10px;
min-width: 400px;
max-width: 400px;
background: $gray-light;
border-left: 3px solid;
.commit-row-title {
display: flex;
}
.item-title {
flex: 1;
margin-right: 0.5em;
}
}
&.line-numbers {
float: none;
border-left: 1px solid $blame-line-numbers-border;
.commit-row-title {
display: flex;
i {
float: none;
margin-right: 0;
}
}
.item-title {
flex: 1;
margin-right: 0.5em;
&.lines {
padding: 0;
}
}
......@@ -195,20 +209,6 @@
border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%);
}
}
td.line-numbers {
float: none;
border-left: 1px solid $blame-line-numbers-border;
i {
float: none;
margin-right: 0;
}
}
td.lines {
padding: 0;
}
}
&.logs {
......
......@@ -463,10 +463,10 @@
word-break: break-all;
}
}
}
.filter-dropdown-item.droplab-item-active .btn {
@extend %filter-dropdown-item-btn-hover;
&.droplab-item-active .btn {
@extend %filter-dropdown-item-btn-hover;
}
}
.filter-dropdown-loading {
......
......@@ -352,7 +352,77 @@
.header-user .dropdown-menu-nav,
.header-new .dropdown-menu-nav {
margin-top: $dropdown-vertical-offset;
margin-top: 4px;
}
.search {
margin: 4px 8px 0;
form {
height: 32px;
border: 0;
border-radius: $border-radius-default;
transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
box-shadow: none;
}
}
.search-input {
color: $white-light;
background: none;
transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
height: 32px;
transition: border-color ease-in-out 0.15s;
}
&.search-active {
form {
background-color: rgba($indigo-200, .3);
box-shadow: none;
.search-input {
color: $gl-text-color;
transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
color: $gl-text-color-tertiary;
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: $gl-text-color-tertiary;
transition: color ease-in-out 0.15s;
}
}
}
.location-badge {
background-color: $nav-badge-bg;
border-color: $border-color;
}
.search-input-wrap {
.clear-icon {
color: $white-light;
}
}
}
}
.breadcrumbs {
......
......@@ -30,10 +30,10 @@ body {
.container {
padding-top: 0;
z-index: 5;
}
.container .content {
margin: 0;
.content {
margin: 0;
}
}
.navless-container {
......@@ -82,26 +82,26 @@ body {
transition: background-color 0.15s, border-color 0.15s;
background-color: $orange-500;
border-color: $orange-500;
}
.alert-warning + .alert-warning {
background-color: $orange-600;
border-color: $orange-600;
}
&:only-of-type {
background-color: $orange-500;
border-color: $orange-500;
}
.alert-warning + .alert-warning + .alert-warning {
background-color: $orange-700;
border-color: $orange-700;
}
+ .alert-warning {
background-color: $orange-600;
border-color: $orange-600;
.alert-warning + .alert-warning + .alert-warning + .alert-warning {
background-color: $orange-800;
border-color: $orange-800;
}
+ .alert-warning {
background-color: $orange-700;
border-color: $orange-700;
.alert-warning:only-of-type {
background-color: $orange-500;
border-color: $orange-500;
+ .alert-warning {
background-color: $orange-800;
border-color: $orange-800;
}
}
}
}
}
......
......@@ -305,40 +305,40 @@ ul.indent-list {
}
}
.group-list-tree .avatar-container.content-loading {
position: relative;
.group-list-tree {
.avatar-container.content-loading {
position: relative;
> a,
> a .avatar {
height: 100%;
border-radius: 50%;
}
> a,
> a .avatar {
height: 100%;
border-radius: 50%;
}
> a {
padding: 2px;
}
> a {
padding: 2px;
> a .avatar {
border: 2px solid $white-normal;
.avatar {
border: 2px solid $white-normal;
&.identicon {
line-height: 30px;
&.identicon {
line-height: 30px;
}
}
}
}
&::after {
content: "";
position: absolute;
height: 100%;
width: 100%;
background-color: transparent;
border: 2px outset $kdb-border;
border-radius: 50%;
animation: spin-avatar 3s infinite linear;
&::after {
content: "";
position: absolute;
height: 100%;
width: 100%;
background-color: transparent;
border: 2px outset $kdb-border;
border-radius: 50%;
animation: spin-avatar 3s infinite linear;
}
}
}
.group-list-tree {
.folder-toggle-wrap {
float: left;
line-height: $list-text-height;
......
......@@ -173,21 +173,8 @@
ul > li {
white-space: nowrap;
}
}
@media(max-width: $screen-xs-max) {
.atwho-view-ul {
width: 350px;
}
.atwho-view ul li {
overflow: hidden;
text-overflow: ellipsis;
}
}
// TODO: fallback to global style
.atwho-view {
// TODO: fallback to global style
.atwho-view-ul {
padding: 8px 1px;
......@@ -220,3 +207,14 @@
}
}
}
@media(max-width: $screen-xs-max) {
.atwho-view-ul {
width: 350px;
}
.atwho-view ul li {
overflow: hidden;
text-overflow: ellipsis;
}
}
......@@ -9,6 +9,7 @@
.modal-body {
position: relative;
padding: #{3 * $grid-size} #{2 * $grid-size};
background-color: $modal-body-bg;
.form-actions {
margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size};
......@@ -46,7 +47,3 @@ body.modal-open {
.modal.popup-dialog {
display: block;
}
.modal-body {
background-color: $modal-body-bg;
}
......@@ -367,11 +367,64 @@
}
}
.project-item-select-holder.btn-group {
display: flex;
max-width: 350px;
overflow: hidden;
float: right;
.page-with-layout-nav {
.right-sidebar {
top: ($header-height + 1) * 2;
}
&.page-with-sub-nav {
.right-sidebar {
top: ($header-height + 1) * 3;
&.affix {
top: $header-height;
}
}
}
}
.with-performance-bar .page-with-layout-nav {
.right-sidebar {
top: ($header-height + 1) * 2 + $performance-bar-height;
}
&.page-with-sub-nav {
.right-sidebar {
top: ($header-height + 1) * 3 + $performance-bar-height;
&.affix {
top: $header-height + $performance-bar-height;
}
}
}
}
@media (max-width: $screen-xs-max) {
.top-area {
flex-flow: row wrap;
.nav-controls {
$controls-margin: $btn-xs-side-margin - 2px;
flex: 0 0 100%;
&.controls-flex {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: center;
padding: 0 0 $gl-padding-top;
}
.controls-item,
.controls-item-full,
.controls-item:last-child {
flex: 1 1 35%;
display: block;
width: 100%;
margin: $controls-margin;
}
}
}
.new-project-item-link {
white-space: nowrap;
......
......@@ -60,22 +60,12 @@
border-radius: $border-radius-base;
border: 1px solid $dropdown-border-color;
min-width: 175px;
color: $gl-text-color;
z-index: 999;
color: $gl-grayish-blue;
}
.select2-drop-mask {
z-index: 998;
}
.select2-drop.select2-drop-above.select2-drop-active {
border-top: 1px solid $dropdown-border-color;
margin-top: -6px;
}
.select2-results li.select2-result-with-children > .select2-result-label {
font-weight: $gl-font-weight-bold;
color: $gl-text-color;
.select2-results .select2-result-label,
.select2-more-results {
padding: 10px 15px;
}
.select2-container-active {
......@@ -144,58 +134,46 @@
.select2-drop-auto-width & {
padding: 15px 15px 5px;
}
}
.select2-search input {
padding: 2px 25px 2px 5px;
background: $white-light image-url('select2.png');
background-repeat: no-repeat;
background-position: right 0 bottom 6px;
border: 1px solid $input-border;
border-radius: $border-radius-default;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
border-color: $input-border-focus;
input {
padding: 2px 25px 2px 5px;
background: $white-light image-url('select2.png');
background-repeat: no-repeat;
background-position: right 0 bottom 6px;
border: 1px solid $input-border;
border-radius: $border-radius-default;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
border-color: $input-border-focus;
}
&.select2-active {
background-color: $white-light;
background-image: image-url('select2-spinner.gif') !important;
background-repeat: no-repeat;
background-position: right 5px center !important;
background-size: 16px 16px !important;
}
}
}
.select2-search input.select2-active {
background-color: $white-light;
background-image: image-url('select2-spinner.gif') !important;
background-repeat: no-repeat;
background-position: right 5px center !important;
background-size: 16px 16px !important;
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
.select2-results .select2-selection-limit {
background: $gray-light;
display: list-item;
padding: 10px 15px;
}
.select2-results {
margin: 0;
padding: #{$gl-padding / 2} 0;
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-result-label,
.select2-more-results {
padding: #{$gl-padding / 2} $gl-padding;
}
padding: 10px 0;
.select2-highlighted {
background: transparent;
li.select2-result-with-children > .select2-result-label {
font-weight: $gl-font-weight-bold;
color: $gl-text-color;
.select2-result-label {
background: $dropdown-item-hover-bg;
}
}
.select2-result {
padding: 0 1px;
}
}
......@@ -212,6 +190,8 @@
}
.select2-highlighted {
background: $gl-link-color !important;
.group-result {
.group-path {
color: $white-light;
......
......@@ -217,13 +217,31 @@ $white-gc-bg: #eaf2f5;
.cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $white-c1; font-style: italic; }
.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
.gd { color: $white-gd; background-color: $white-gd-bg; }
.gd .x { color: $white-gd-x; background-color: $white-gd-x-bg; }
.gd {
color: $white-gd;
background-color: $white-gd-bg;
.x {
color: $white-gd-x;
background-color: $white-gd-x-bg;
}
}
.ge { font-style: italic; }
.gr { color: $white-gr; }
.gh { color: $white-gh; }
.gi { color: $white-gi; background-color: $white-gi-bg; }
.gi .x { color: $white-gi-x; background-color: $white-gi-x-bg; }
.gi {
color: $white-gi;
background-color: $white-gi-bg;
.x {
color: $white-gi-x;
background-color: $white-gi-x-bg;
}
}
.go { color: $white-go; }
.gp { color: $white-gp; }
.gs { font-weight: $gl-font-weight-bold; }
......
......@@ -158,13 +158,31 @@ span.highlight_word {
.cp { color: $highlighted-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $highlighted-c1; font-style: italic; }
.cs { color: $highlighted-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
.gd { color: $highlighted-gd; background-color: $highlighted-gd-bg; }
.gd .x { color: $highlighted-gd; background-color: $highlighted-gd-x-bg; }
.gd {
color: $highlighted-gd;
background-color: $highlighted-gd-bg;
.x {
color: $highlighted-gd;
background-color: $highlighted-gd-x-bg;
}
}
.ge { font-style: italic; }
.gr { color: $highlighted-gr; }
.gh { color: $highlighted-gh; }
.gi { color: $highlighted-gi; background-color: $highlighted-gi-bg; }
.gi .x { color: $highlighted-gi; background-color: $highlighted-gi-x-bg; }
.gi {
color: $highlighted-gi;
background-color: $highlighted-gi-bg;
.x {
color: $highlighted-gi;
background-color: $highlighted-gi-x-bg;
}
}
.go { color: $highlighted-go; }
.gp { color: $highlighted-gp; }
.gs { font-weight: $gl-font-weight-bold; }
......
......@@ -107,7 +107,7 @@
}
.boards-list {
height: calc(100vh - 152px);
height: calc(100vh - 105px);
width: 100%;
padding-top: 25px;
padding-bottom: 25px;
......@@ -116,8 +116,12 @@
overflow-x: scroll;
white-space: nowrap;
@media (min-width: $screen-sm-min) {
height: calc(100vh - 222px);
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
height: calc(100vh - 90px);
}
@media (min-width: $screen-md-min) {
height: calc(100vh - 160px);
min-height: 475px;
}
}
......
......@@ -68,18 +68,18 @@
&.affix {
top: $header-height;
}
// with sidebar
&.affix.sidebar-expanded {
right: 306px;
left: 16px;
}
// with sidebar
&.sidebar-expanded {
right: 306px;
left: 16px;
}
// without sidebar
&.affix.sidebar-collapsed {
right: 16px;
left: 16px;
// without sidebar
&.sidebar-collapsed {
right: 16px;
left: 16px;
}
}
&.affix-top {
......
......@@ -22,6 +22,11 @@
}
}
}
svg {
width: 136px;
height: 136px;
}
}
.col-headers {
......@@ -155,11 +160,6 @@
}
}
.landing svg {
width: 136px;
height: 136px;
}
.fa-spinner {
font-size: 28px;
position: relative;
......
......@@ -380,6 +380,10 @@
}
}
}
.line_content {
white-space: pre-wrap;
}
}
.file-content .diff-file {
......@@ -387,10 +391,6 @@
border: 0;
}
.diff-file .line_content {
white-space: pre-wrap;
}
.diff-wrap-lines .line_content {
white-space: pre-wrap;
}
......
......@@ -415,23 +415,6 @@
width: 100%;
padding: 0;
padding-bottom: 100%;
}
.prometheus-svg-container > svg {
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
text {
fill: $gl-text-color;
stroke-width: 0;
}
.text-metric-bold {
font-weight: $gl-font-weight-bold;
}
.label-axis-text {
fill: $black;
......@@ -446,42 +429,51 @@
font-size: 12px;
}
.legend-axis-text {
fill: $black;
}
> svg {
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
.tick {
> line {
stroke: $gray-darker;
.label-axis-text,
.text-metric-usage {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 12px;
}
> text {
font-size: 12px;
.legend-axis-text {
fill: $black;
}
}
.text-metric-title {
font-size: 12px;
}
.tick > text {
font-size: 12px;
}
.y-label-text,
.x-label-text {
fill: $gray-darkest;
}
.text-metric-title {
font-size: 12px;
}
.axis-tick {
stroke: $gray-darker;
}
.y-label-text,
.x-label-text {
fill: $gray-darkest;
}
@media (max-width: $screen-sm-max) {
.label-axis-text,
.text-metric-usage,
.legend-axis-text {
font-size: 8px;
.axis-tick {
stroke: $gray-darker;
}
.tick > text {
font-size: 8px;
@media (max-width: $screen-sm-max) {
.label-axis-text,
.text-metric-usage,
.legend-axis-text {
font-size: 8px;
}
.tick > text {
font-size: 8px;
}
}
}
}
......@@ -127,7 +127,16 @@
}
.right-sidebar {
a:not(.btn-retry),
position: absolute;
top: $header-height;
bottom: 0;
right: 0;
transition: width .3s;
background: $gray-light;
z-index: 200;
overflow: hidden;
a,
.btn-link {
color: inherit;
}
......@@ -228,17 +237,6 @@
.btn-clipboard:hover {
color: $gl-text-color;
}
}
.right-sidebar {
position: absolute;
top: $header-height;
bottom: 0;
right: 0;
transition: width $right-sidebar-transition-duration;
background: $gray-light;
z-index: 200;
overflow: hidden;
.issuable-sidebar {
width: calc(100% + 100px);
......
......@@ -109,6 +109,30 @@
border-top-right-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
// Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
// These styles prevent this from breaking the layout, and only applied when providers are configured.
&.custom-provider-tabs {
flex-wrap: wrap;
li {
min-width: 85px;
flex-basis: auto;
// This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
// We are making somewhat of an assumption about the configuration here: that users do not have more than
// 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
// of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
// above one of the bottom row elements. If you know a better way, please implement it!
&:nth-child(n+5) {
border-top: 1px solid $border-color;
}
}
a {
font-size: 16px;
}
}
li {
flex: 1;
text-align: center;
......@@ -154,32 +178,6 @@
}
}
// Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
// These styles prevent this from breaking the layout, and only applied when providers are configured.
.new-session-tabs.custom-provider-tabs {
flex-wrap: wrap;
li {
min-width: 85px;
flex-basis: auto;
// This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
// We are making somewhat of an assumption about the configuration here: that users do not have more than
// 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
// of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
// above one of the bottom row elements. If you know a better way, please implement it!
&:nth-child(n+5) {
border-top: 1px solid $border-color;
}
}
a {
font-size: 16px;
}
}
.form-control {
&:active,
&:focus {
......@@ -231,35 +229,35 @@
margin: 0;
padding: 0;
height: 100%;
}
// Fixes footer container to bottom of viewport
.devise-layout-html body {
// offset height of fixed header + 1 to avoid scroll
height: calc(100% - 51px);
margin: 0;
padding: 0;
// Fixes footer container to bottom of viewport
body {
// offset height of fixed header + 1 to avoid scroll
height: calc(100% - 51px);
margin: 0;
padding: 0;
.page-wrap {
min-height: 100%;
position: relative;
}
.page-wrap {
min-height: 100%;
position: relative;
}
.footer-container,
hr.footer-fixed {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: $white-light;
}
.footer-container,
hr.footer-fixed {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: $white-light;
}
.navless-container {
padding: 65px 15px; // height of footer + bottom padding of email confirmation link
.navless-container {
padding: 65px 15px; // height of footer + bottom padding of email confirmation link
@media (max-width: $screen-xs-max) {
padding: 0 15px 65px;
@media (max-width: $screen-xs-max) {
padding: 0 15px 65px;
}
}
}
}
......@@ -55,9 +55,17 @@
width: auto;
}
}
&.existing-title {
@media (min-width: $screen-sm-min) {
float: left;
}
}
}
.member-form-control {
@include new-style-dropdown;
@media (max-width: $screen-xs-max) {
padding-bottom: 5px;
margin-left: 0;
......@@ -70,12 +78,6 @@
line-height: 43px;
}
.member.existing-title {
@media (min-width: $screen-sm-min) {
float: left;
}
}
.member-search-form {
@include new-style-dropdown;
......@@ -331,7 +333,3 @@
}
}
}
.member-form-control {
@include new-style-dropdown;
}
......@@ -155,6 +155,10 @@
&.media > *:first-child {
margin-right: 10px;
}
.approve-btn {
margin-right: 5px;
}
}
.mr-widget-pipeline-graph {
......@@ -190,6 +194,10 @@
overflow: hidden;
word-break: break-all;
&.media > *:first-child {
margin-right: 10px;
}
&.label-truncated {
position: relative;
display: inline-block;
......@@ -207,14 +215,7 @@
background-color: $gray-light;
}
}
}
.mr-widget-help {
padding: 10px 16px 10px 48px;
font-style: italic;
}
.mr-widget-body {
h4 {
float: left;
font-weight: $gl-font-weight-bold;
......@@ -237,6 +238,10 @@
margin-right: 7px;
}
.approve-btn {
margin-right: 5px;
}
label {
font-weight: $gl-font-weight-normal;
}
......@@ -329,6 +334,22 @@
}
}
.mini-pipeline-graph-dropdown-menu .mini-pipeline-graph-dropdown-item {
display: flex;
align-items: center;
.ci-status-text,
.ci-status-icon {
top: 0;
margin-right: 10px;
}
}
.mr-widget-help {
padding: 10px 16px 10px 48px;
font-style: italic;
}
.ci-coverage {
float: right;
}
......@@ -347,10 +368,6 @@
flex-wrap: wrap;
}
.mr-widget-body-controls {
flex-wrap: wrap;
}
.mr_source_commit,
.mr_target_commit {
margin-bottom: 0;
......@@ -460,16 +477,16 @@
padding-bottom: 0;
}
}
}
.mr-info-list.mr-memory-usage {
p {
float: left;
}
&.mr-memory-usage {
p {
float: left;
}
.memory-graph-container {
float: left;
margin-left: 5px;
.memory-graph-container {
float: left;
margin-left: 5px;
}
}
}
......
......@@ -66,6 +66,15 @@
height: 6px;
margin: 0;
}
.sidebar-collapsed-icon {
clear: both;
padding: 15px 5px 5px;
.progress {
margin: 5px 0;
}
}
}
.collapsed-milestone-date {
......@@ -93,17 +102,6 @@
margin-right: 0;
}
.milestone-progress {
.sidebar-collapsed-icon {
clear: both;
padding: 15px 5px 5px;
.progress {
margin: 5px 0;
}
}
}
.right-sidebar-collapsed & {
.reference {
border-top: 1px solid $border-gray-normal;
......@@ -156,18 +154,16 @@
.status-box {
margin-top: 0;
}
.milestone-buttons {
margin-left: auto;
}
.status-box {
order: 1;
}
.milestone-buttons {
margin-left: auto;
order: 2;
.verbose {
display: none;
}
}
.header-text-content {
......@@ -175,10 +171,6 @@
width: 100%;
}
.milestone-buttons .verbose {
display: none;
}
@media (min-width: $screen-xs-min) {
.milestone-buttons .verbose {
display: inline;
......
......@@ -111,24 +111,9 @@
margin: auto;
align-items: center;
.icon {
margin-right: $issuable-warning-icon-margin;
}
}
.disabled-comment .issuable-note-warning {
border: 0;
border-radius: $label-border-radius;
padding-top: $gl-vert-padding;
padding-bottom: $gl-vert-padding;
.icon svg {
position: relative;
top: 2px;
margin-right: $btn-xs-side-margin;
width: $gl-font-size;
height: $gl-font-size;
fill: $orange-600;
.md-area {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
......@@ -155,11 +140,6 @@
}
}
.issuable-note-warning + .md-area {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.discussion-form {
background-color: $white-light;
}
......
......@@ -312,57 +312,72 @@ ul.notes {
}
}
.diff-file .notes_holder {
font-family: $regular_font;
.diff-file {
.is-over {
.add-diff-note {
display: inline-block;
}
}
td {
border: 1px solid $white-normal;
border-left: 0;
// Merge request notes in diffs
// Diff is inline
.notes_content .note-header .note-headline-light {
display: inline-block;
position: relative;
}
&.notes_line {
vertical-align: middle;
text-align: center;
padding: 10px 0;
background: $gray-light;
color: $text-color;
}
.notes_holder {
font-family: $regular_font;
&.notes_line2 {
text-align: center;
padding: 10px 0;
border-left: 1px solid $note-line2-border !important;
}
td {
border: 1px solid $white-normal;
border-left: 0;
&.notes_content {
background-color: $gray-light;
border-width: 1px 0;
padding: 0;
vertical-align: top;
white-space: normal;
&.notes_line {
vertical-align: middle;
text-align: center;
padding: 10px 0;
background: $gray-light;
color: $text-color;
}
&.parallel {
border-width: 1px;
&.notes_line2 {
text-align: center;
padding: 10px 0;
border-left: 1px solid $note-line2-border !important;
}
.discussion-notes {
&:not(:first-child) {
border-top: 1px solid $white-normal;
margin-top: 20px;
&.notes_content {
background-color: $gray-light;
border-width: 1px 0;
padding: 0;
vertical-align: top;
white-space: normal;
&.parallel {
border-width: 1px;
}
&:not(:last-child) {
border-bottom: 1px solid $white-normal;
margin-bottom: 20px;
.discussion-notes {
&:not(:first-child) {
border-top: 1px solid $white-normal;
margin-top: 20px;
}
&:not(:last-child) {
border-bottom: 1px solid $white-normal;
margin-bottom: 20px;
}
}
}
.notes {
background-color: $white-light;
}
.notes {
background-color: $white-light;
}
a code {
top: 0;
margin-right: 0;
a code {
top: 0;
margin-right: 0;
}
}
}
}
......@@ -457,8 +472,9 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
.btn-group > .discussion-next-btn {
margin-left: -1px;
@include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
}
}
......@@ -499,13 +515,6 @@ ul.notes {
min-width: 180px;
}
.discussion-actions {
@include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
}
}
.note-actions-item {
margin-left: 12px;
display: flex;
......@@ -662,14 +671,6 @@ ul.notes {
}
}
.diff-file {
.is-over {
.add-diff-note {
display: inline-block;
}
}
}
.disabled-comment {
background-color: $gray-light;
border-radius: $border-radius-base;
......@@ -711,20 +712,20 @@ ul.notes {
svg path {
fill: $gray-darkest;
}
}
.btn.discussion-create-issue-btn {
margin-left: -4px;
border-radius: 0;
border-right: 0;
&.discussion-create-issue-btn {
margin-left: -4px;
border-radius: 0;
border-right: 0;
a {
padding: 0;
line-height: 0;
a {
padding: 0;
line-height: 0;
&:hover {
text-decoration: none;
border: 0;
&:hover {
text-decoration: none;
border: 0;
}
}
}
}
......@@ -798,12 +799,3 @@ ul.notes {
.line-resolve-text {
vertical-align: middle;
}
// Merge request notes in diffs
.diff-file {
// Diff is inline
.notes_content .note-header .note-headline-light {
display: inline-block;
position: relative;
}
}
......@@ -202,6 +202,25 @@
}
}
/**
* Play button with icon in dropdowns
*/
.no-btn {
border: 0;
background: none;
outline: none;
width: 100%;
text-align: left;
.icon-play {
position: relative;
top: 2px;
margin-right: 5px;
height: 13px;
width: 12px;
}
}
.duration,
.finished-at {
color: $gl-text-color-secondary;
......@@ -481,6 +500,9 @@
// Action Icons in big pipeline-graph nodes
.ci-action-icon-container.ci-action-icon-wrapper {
position: absolute;
right: 5px;
top: 5px;
height: 30px;
width: 30px;
background: $white-light;
......@@ -491,6 +513,10 @@
&:hover {
background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color;
svg {
fill: $gl-text-color;
}
}
svg {
......@@ -509,16 +535,6 @@
left: 8px;
}
}
&:hover svg {
fill: $gl-text-color;
}
}
.ci-action-icon-container {
position: absolute;
right: 5px;
top: 5px;
}
.ci-status-icon svg {
......@@ -765,6 +781,28 @@ a.linked-pipeline-mini-item {
left: -3px;
position: relative;
top: -2px;
&.icon-action-stop,
&.icon-action-cancel {
width: 12px;
height: 12px;
top: 1px;
left: -1px;
}
&.icon-action-play {
width: 11px;
height: 11px;
top: 1px;
left: 1px;
}
&.icon-action-retry {
width: 16px;
height: 16px;
top: 0;
left: -3px;
}
}
&:hover svg,
......@@ -781,27 +819,6 @@ a.linked-pipeline-mini-item {
}
}
svg.icon-action-stop,
svg.icon-action-cancel {
width: 12px;
height: 12px;
top: 1px;
left: -1px;
}
svg.icon-action-play {
width: 11px;
height: 11px;
top: 1px;
left: 1px;
}
svg.icon-action-retry {
width: 16px;
height: 16px;
top: 0;
left: -3px;
}
}
......@@ -870,13 +887,10 @@ a.linked-pipeline-mini-item {
left: 100%;
top: -10px;
box-shadow: 0 1px 5px $black-transparent;
}
/**
* Top arrow in the dropdown in the big pipeline graph
*/
.big-pipeline-graph-dropdown-menu {
/**
* Top arrow in the dropdown in the big pipeline graph
*/
&::before,
&::after {
content: '';
......@@ -938,22 +952,23 @@ a.linked-pipeline-mini-item {
margin-top: 1px;
border-bottom-color: $white-light;
}
}
/**
* Center dropdown menu in mini graph
*/
.mini-pipeline-graph-dropdown-menu.dropdown-menu {
transform: translate(-80%, 0);
min-width: 150px;
/**
* Center dropdown menu in mini graph
*/
&.dropdown-menu {
transform: translate(-80%, 0);
min-width: 150px;
@media(min-width: $screen-md-min) {
transform: translate(-50%, 0);
right: auto;
left: 50%;
min-width: 240px;
@media(min-width: $screen-md-min) {
transform: translate(-50%, 0);
right: auto;
left: 50%;
min-width: 240px;
}
}
}
/**
* Terminal
*/
......@@ -977,25 +992,6 @@ a.linked-pipeline-mini-item {
}
}
/**
* Play button with icon in dropdowns
*/
.ci-table .no-btn {
border: 0;
background: none;
outline: none;
width: 100%;
text-align: left;
.icon-play {
position: relative;
top: 2px;
margin-right: 5px;
height: 13px;
width: 12px;
}
}
.linked-pipeline-mini-list {
display: inline-block;
......
......@@ -96,7 +96,8 @@
transition: background 2s ease-out;
&:disabled {
opacity: 0.75;
opacity: 0.5;
pointer-events: none;
}
.highlight-changes & {
......@@ -785,35 +786,35 @@ a.deploy-project-label {
.nav {
padding-top: 12px;
padding-bottom: 12px;
}
.nav > li {
display: inline-block;
> li {
display: inline-block;
&:not(:last-child) {
margin-right: $gl-padding;
}
&:not(:last-child) {
margin-right: $gl-padding;
}
&.right {
vertical-align: top;
margin-top: 0;
&.right {
vertical-align: top;
margin-top: 0;
@media (min-width: $screen-lg-min) {
float: right;
@media (min-width: $screen-lg-min) {
float: right;
}
}
}
}
.nav > li > a {
padding: 0;
background-color: transparent;
font-size: 14px;
line-height: 29px;
color: $notes-light-color;
> a {
padding: 0;
background-color: transparent;
font-size: 14px;
line-height: 29px;
color: $notes-light-color;
&:hover,
&:focus {
color: $gl-text-color;
&:hover,
&:focus {
color: $gl-text-color;
}
}
}
}
......@@ -1202,13 +1203,6 @@ a.allowed-to-push {
}
}
.project-repo-select {
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
.variables-table {
table-layout: fixed;
......
......@@ -78,6 +78,10 @@ input[type="checkbox"]:hover {
}
.search-input-wrap {
// Fallback if flexbox is not supported
display: inline-block;
width: 100%;
.search-icon,
.clear-icon {
position: absolute;
......
......@@ -266,11 +266,11 @@
margin-left: 5px;
background: $badge-bg;
}
}
/* Ensure we don't add border if there's only single li */
li + li {
border-top: 1px solid $border-color;
/* Ensure we don't add border if there's only single li */
+ li {
border-top: 1px solid $border-color;
}
}
}
}
......@@ -5,10 +5,10 @@ table .sherlock-code {
.sherlock-code {
pre {
word-wrap: normal;
}
pre code {
white-space: pre;
code {
white-space: pre;
}
}
}
......@@ -21,13 +21,13 @@ table .sherlock-code {
text-align: right;
padding: 0 10px !important;
}
.slow {
color: $red-500;
font-weight: $gl-font-weight-bold;
}
}
.sherlock-file-sample pre {
padding-top: 28px !important;
}
.sherlock-line-samples-table .slow {
color: $red-500;
font-weight: $gl-font-weight-bold;
}
......@@ -40,16 +40,16 @@
@media (max-width: $screen-xs-max) {
width: 100%;
}
}
.person .spark {
display: block;
background: $stat-graph-common-bg;
width: 100%;
}
.spark {
display: block;
background: $stat-graph-common-bg;
width: 100%;
}
.person .area-contributor {
fill: $stat-graph-orange-fill;
.area-contributor {
fill: $stat-graph-orange-fill;
}
}
}
......
......@@ -161,10 +161,10 @@ ul.wiki-pages-list.content-list {
list-style: none;
margin-left: 0;
padding-left: 15px;
}
ul li {
padding: 5px 0;
li {
padding: 5px 0;
}
}
}
......
......@@ -74,9 +74,10 @@ module LfsRequest
def lfs_upload_access?
return false unless project.lfs_enabled?
return false unless has_authentication_ability?(:push_code)
return false if project.above_size_limit? || objects_exceed_repo_limit?
has_authentication_ability?(:push_code) && can?(user, :push_code, project)
lfs_deploy_token? || can?(user, :push_code, project)
end
def lfs_deploy_token?
......@@ -95,10 +96,9 @@ module LfsRequest
@storage_project ||= begin
result = project
loop do
break unless result.forked?
result = result.forked_from_project
end
# TODO: Make this go to the fork_network root immeadiatly
# dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
result = result.fork_source while result.forked?
result
end
......
......@@ -4,6 +4,7 @@ module NotesActions
included do
before_action :set_polling_interval_header, only: [:index]
before_action :noteable, only: :index
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :note_project, only: [:create]
end
......@@ -188,7 +189,7 @@ module NotesActions
end
def noteable
@noteable ||= notes_finder.target
@noteable ||= notes_finder.target || render_404
end
def last_fetched_at
......
......@@ -43,7 +43,7 @@ class Import::GithubController < Import::BaseController
@target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
if can?(current_user, :create_projects, @target_namespace)
@project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute
@project = Gitlab::LegacyGithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute
else
render 'unauthorized'
end
......@@ -52,7 +52,7 @@ class Import::GithubController < Import::BaseController
private
def client
@client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options)
@client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options)
end
def verify_import_enabled
......
......@@ -10,9 +10,6 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :set_commits
def show
@note_counts = project.notes.where(commit_id: @commits.map(&:id))
.group(:commit_id).count
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
......
......@@ -113,9 +113,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@commits = prepare_commits_for_rendering(@merge_request.commits)
@commit = @merge_request.diff_head_commit
@note_counts = Note.where(commit_id: @commits.map(&:id))
.group(:commit_id).count
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
set_pipeline_variables
......
......@@ -106,8 +106,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
# Get commits from repository
# or from cache if already merged
@commits = prepare_commits_for_rendering(@merge_request.commits)
@note_counts = Note.where(commit_id: @commits.map(&:id))
.group(:commit_id).count
render json: { html: view_to_html_string('projects/merge_requests/_commits') }
end
......
......@@ -112,7 +112,15 @@ module ProjectsHelper
def remove_fork_project_message(project)
_("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") %
{ forked_from_project: @project.forked_from_project.name_with_namespace }
{ forked_from_project: fork_source_name(project) }
end
def fork_source_name(project)
if @project.fork_source
@project.fork_source.full_name
else
@project.fork_network&.deleted_root_project_name
end
end
def project_nav_tabs
......@@ -142,8 +150,8 @@ module ProjectsHelper
def can_change_visibility_level?(project, current_user)
return false unless can?(current_user, :change_visibility_level, project)
if project.forked?
project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE
if project.fork_source
project.fork_source.visibility_level > Gitlab::VisibilityLevel::PRIVATE
else
true
end
......
......@@ -422,7 +422,7 @@ module Ci
end
def notes
Note.for_commit_id(sha)
project.notes.for_commit_id(sha)
end
def process!
......
......@@ -110,7 +110,7 @@ class Environment < ActiveRecord::Base
end
def ref_path
"refs/#{Repository::REF_ENVIRONMENTS}/#{generate_slug}"
"refs/#{Repository::REF_ENVIRONMENTS}/#{slug}"
end
def formatted_external_url
......@@ -168,6 +168,10 @@ class Environment < ActiveRecord::Base
end
end
def slug
super.presence || generate_slug
end
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
......
......@@ -47,4 +47,8 @@ class ExternalIssue
id
end
def notes
Note.none
end
end
......@@ -12,4 +12,8 @@ class ForkNetwork < ActiveRecord::Base
def find_forks_in(other_projects)
projects.where(id: other_projects)
end
def merge_requests
MergeRequest.where(target_project: projects)
end
end
......@@ -22,6 +22,10 @@ module Geo
class_name: 'Geo::RepositoriesChangedEvent',
foreign_key: :repositories_changed_event_id
belongs_to :hashed_storage_migrated_event,
class_name: 'Geo::HashedStorageMigratedEvent',
foreign_key: :hashed_storage_migrated_event_id
def self.latest_event
order(id: :desc).first
end
......@@ -31,7 +35,8 @@ module Geo
repository_updated_event ||
repository_deleted_event ||
repository_renamed_event ||
repositories_changed_event
repositories_changed_event ||
hashed_storage_migrated_event
end
def project_id
......
......@@ -2,4 +2,6 @@ class Geo::FileRegistry < Geo::BaseRegistry
scope :failed, -> { where(success: false) }
scope :synced, -> { where(success: true) }
scope :to_be_retried, -> { where('retry_at is NULL OR retry_at < ?', Time.now) }
scope :lfs_objects, -> { where(file_type: :lfs) }
scope :attachments, -> { where(file_type: Geo::FileService::DEFAULT_OBJECT_TYPES) }
end
module Geo
class HashedStorageMigratedEvent < ActiveRecord::Base
include Geo::Model
belongs_to :project
validates :project, :repository_storage_name, :repository_storage_path,
:old_disk_path, :new_disk_path, :old_wiki_disk_path,
:new_wiki_disk_path, :new_storage_version, presence: true
end
end
......@@ -6,6 +6,7 @@ class GeoNode < ActiveRecord::Base
has_many :geo_node_namespace_links
has_many :namespaces, through: :geo_node_namespace_links
has_one :status, class_name: 'GeoNodeStatus'
default_values schema: lambda { Gitlab.config.gitlab.protocol },
host: lambda { Gitlab.config.gitlab.host },
......@@ -41,7 +42,9 @@ class GeoNode < ActiveRecord::Base
encode: true
def current?
Gitlab::Geo.current_node == self
host == Gitlab.config.gitlab.host &&
port == Gitlab.config.gitlab.port &&
relative_url_root == Gitlab.config.gitlab.relative_url_root
end
def secondary?
......@@ -181,6 +184,43 @@ class GeoNode < ActiveRecord::Base
end
end
def lfs_objects_synced_count
return unless secondary?
relation = Geo::FileRegistry.lfs_objects.synced
if restricted_project_ids
relation = relation.where(file_id: lfs_objects.pluck(:id))
end
relation.count
end
def lfs_objects_failed_count
return unless secondary?
Geo::FileRegistry.lfs_objects.failed.count
end
def attachments_synced_count
return unless secondary?
upload_ids = uploads.pluck(:id)
synced_ids = Geo::FileRegistry.attachments.synced.pluck(:file_id)
(synced_ids & upload_ids).length
end
def attachments_failed_count
return unless secondary?
Geo::FileRegistry.attachments.failed.count
end
def find_or_build_status
status || build_status
end
private
def geo_api_url(suffix)
......
class GeoNodeStatus
include ActiveModel::Model
class GeoNodeStatus < ActiveRecord::Base
belongs_to :geo_node
attr_accessor :id, :success
attr_writer :health
# Whether we were successful in reaching this node
attr_accessor :success
def health
@health ||= HealthCheck::Utils.process_checks(['geo'])
rescue NotImplementedError => e
@health = e.to_s
end
# Be sure to keep this consistent with Prometheus naming conventions
PROMETHEUS_METRICS = {
db_replication_lag_seconds: 'Database replication lag (seconds)',
repositories_count: 'Total number of repositories available on primary',
repositories_synced_count: 'Number of repositories synced on secondary',
repositories_failed_count: 'Number of repositories failed to sync on secondary',
lfs_objects_count: 'Total number of LFS objects available on primary',
lfs_objects_synced_count: 'Number of LFS objects synced on secondary',
lfs_objects_failed_count: 'Number of LFS objects failed to sync on secondary',
attachments_count: 'Total number of file attachments available on primary',
attachments_synced_count: 'Number of attachments synced on secondary',
attachments_failed_count: 'Number of attachments failed to sync on secondary',
last_event_id: 'Database ID of the latest event log entry on the primary',
last_event_timestamp: 'Time of the latest event log entry on the primary',
cursor_last_event_id: 'Last database ID of the event log processed by the secondary',
cursor_last_event_timestamp: 'Time of the event log processed by the secondary',
last_successful_status_check_timestamp: 'Time when Geo node status was updated internally',
status_message: 'Summary of health status'
}.freeze
def healthy?
health.blank?
end
def self.current_node_status
current_node = Gitlab::Geo.current_node
def db_replication_lag_seconds
return @db_replication_lag_seconds if defined?(@db_replication_lag_seconds)
return unless current_node
@db_replication_lag_seconds = Gitlab::Geo::HealthCheck.db_replication_lag_seconds if Gitlab::Geo.secondary?
end
status = current_node.find_or_build_status
def db_replication_lag_seconds=(value)
@db_replication_lag_seconds = value
end
# Since we're retrieving our own data, we mark this as a successful load
status.success = true
status.load_data_from_current_node
def last_event_id
@last_event_id ||= latest_event&.id
end
status.save if Gitlab::Geo.primary?
def last_event_id=(value)
@last_event_id = value
status
end
def last_event_timestamp
@last_event_timestamp ||= Geo::EventLog.latest_event&.created_at&.to_i
end
def self.from_json(json_data)
json_data.slice!(*allowed_params)
def last_event_timestamp=(value)
@last_event_timestamp = value
GeoNodeStatus.new(json_data)
end
def cursor_last_event_id
return @cursor_last_event_id if defined?(@cursor_last_event_id)
@cursor_last_event_id = cursor_last_processed&.event_id if Gitlab::Geo.secondary?
end
def cursor_last_event_id=(value)
@cursor_last_event_id = value
end
def cursor_last_event_timestamp
event_id = cursor_last_event_id
return unless event_id
@cursor_last_event_timestamp ||= Geo::EventLog.find_by(id: event_id)&.created_at&.to_i
def self.allowed_params
excluded_params = %w(id last_successful_status_check_at created_at updated_at).freeze
extra_params = %w(success health last_event_timestamp cursor_last_event_timestamp).freeze
self.column_names - excluded_params + extra_params
end
def cursor_last_event_timestamp=(value)
@cursor_last_event_timestamp = value
end
def repositories_count
@repositories_count ||= repositories.count
end
def repositories_count=(value)
@repositories_count = value.to_i
end
def load_data_from_current_node
self.status_message =
begin
HealthCheck::Utils.process_checks(['geo'])
rescue NotImplementedError => e
e.to_s
end
def repositories_synced_count
@repositories_synced_count ||= project_registries.synced.count
end
latest_event = Geo::EventLog.latest_event
self.last_event_id = latest_event&.id
self.last_event_date = latest_event&.created_at
self.repositories_count = geo_node.projects.count
self.lfs_objects_count = geo_node.lfs_objects.count
self.attachments_count = geo_node.uploads.count
self.last_successful_status_check_at = Time.now
if Gitlab::Geo.secondary?
self.db_replication_lag_seconds = Gitlab::Geo::HealthCheck.db_replication_lag_seconds
self.cursor_last_event_id = Geo::EventLogState.last_processed&.event_id
self.cursor_last_event_date = Geo::EventLog.find_by(id: self.cursor_last_event_id)&.created_at
self.repositories_synced_count = geo_node.project_registries.synced.count
self.repositories_failed_count = geo_node.project_registries.failed.count
self.lfs_objects_synced_count = geo_node.lfs_objects_synced_count
self.lfs_objects_failed_count = geo_node.lfs_objects_failed_count
self.attachments_synced_count = geo_node.attachments_synced_count
self.attachments_failed_count = geo_node.attachments_failed_count
end
def repositories_synced_count=(value)
@repositories_synced_count = value.to_i
self
end
def repositories_synced_in_percentage
sync_percentage(repositories_count, repositories_synced_count)
end
alias_attribute :health, :status_message
def repositories_failed_count
@repositories_failed_count ||= project_registries.failed.count
def healthy?
status_message.blank? || status_message == 'Healthy'.freeze
end
def repositories_failed_count=(value)
@repositories_failed_count = value.to_i
def last_successful_status_check_timestamp
self.last_successful_status_check_at.to_i
end
def lfs_objects_count
@lfs_objects_count ||= lfs_objects.count
def last_successful_status_check_timestamp=(value)
self.last_successful_status_check_at = Time.at(value)
end
def lfs_objects_count=(value)
@lfs_objects_count = value.to_i
def last_event_timestamp
self.last_event_date.to_i
end
def lfs_objects_synced_count
@lfs_objects_synced_count ||= begin
relation = Geo::FileRegistry.synced.where(file_type: :lfs)
if Gitlab::Geo.current_node.restricted_project_ids
relation = relation.where(file_id: lfs_objects.pluck(:id))
end
relation.count
end
def last_event_timestamp=(value)
self.last_event_date = Time.at(value)
end
def lfs_objects_synced_count=(value)
@lfs_objects_synced_count = value.to_i
def cursor_last_event_timestamp
self.cursor_last_event_date.to_i
end
def lfs_objects_failed_count
@lfs_objects_failed_count ||= Geo::FileRegistry.failed.where(file_type: :lfs).count
def cursor_last_event_timestamp=(value)
self.cursor_last_event_date = Time.at(value)
end
def lfs_objects_failed_count=(value)
@lfs_objects_failed_count = value.to_i
def repositories_synced_in_percentage
sync_percentage(repositories_count, repositories_synced_count)
end
def lfs_objects_synced_in_percentage
sync_percentage(lfs_objects_count, lfs_objects_synced_count)
end
def attachments_count
@attachments_count ||= attachments.count
end
def attachments_count=(value)
@attachments_count = value.to_i
end
def attachments_synced_count
@attachments_synced_count ||= begin
upload_ids = attachments.pluck(:id)
synced_ids = Geo::FileRegistry.synced.where(file_type: Geo::FileService::DEFAULT_OBJECT_TYPES).pluck(:file_id)
(synced_ids & upload_ids).length
end
end
def attachments_synced_count=(value)
@attachments_synced_count = value.to_i
end
def attachments_failed_count
@attachments_failed_count ||= Geo::FileRegistry.failed.where(file_type: Geo::FileService::DEFAULT_OBJECT_TYPES).count
end
def attachments_failed_count=(value)
@attachments_failed_count = value.to_i
end
def attachments_synced_in_percentage
sync_percentage(attachments_count, attachments_synced_count)
end
......@@ -166,32 +132,8 @@ class GeoNodeStatus
private
def sync_percentage(total, synced)
return 0 if total.zero?
return 0 if !total.present? || total.zero?
(synced.to_f / total.to_f) * 100.0
end
def attachments
@attachments ||= Gitlab::Geo.current_node.uploads
end
def lfs_objects
@lfs_objects ||= Gitlab::Geo.current_node.lfs_objects
end
def project_registries
@project_registries ||= Gitlab::Geo.current_node.project_registries
end
def repositories
@repositories ||= Gitlab::Geo.current_node.projects
end
def latest_event
Geo::EventLog.latest_event
end
def cursor_last_processed
Geo::EventLogState.last_processed
end
end
......@@ -602,7 +602,7 @@ class MergeRequest < ActiveRecord::Base
commit_notes = Note
.except(:order)
.where(project_id: [source_project_id, target_project_id])
.where(noteable_type: 'Commit', commit_id: commit_ids)
.for_commit_id(commit_ids)
# We're using a UNION ALL here since this results in better performance
# compared to using OR statements. We're using UNION ALL since the queries
......
......@@ -366,6 +366,7 @@ class Project < ActiveRecord::Base
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
scope :excluding_project, ->(project) { where.not(id: project) }
scope :import_started, -> { where(import_status: 'started') }
state_machine :import_status, initial: :none do
event :import_schedule do
......@@ -1039,6 +1040,10 @@ class Project < ActiveRecord::Base
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end
def fork_source
forked_from_project || fork_network&.root_project
end
def personal?
!group
end
......@@ -1184,6 +1189,10 @@ class Project < ActiveRecord::Base
!!repository.exists?
end
def wiki_repository_exists?
wiki.repository_exists?
end
# update visibility_level of forks
def update_forks_visibility_level
return unless visibility_level < visibility_level_was
......@@ -1427,6 +1436,31 @@ class Project < ActiveRecord::Base
reload_repository!
end
def after_import
repository.after_import
import_finish
remove_import_jid
update_project_counter_caches
end
def update_project_counter_caches
classes = [
Projects::OpenIssuesCountService,
Projects::OpenMergeRequestsCountService
]
classes.each do |klass|
klass.new(self).refresh_cache
end
end
def remove_import_jid
return unless import_jid
Gitlab::SidekiqStatus.unset(import_jid)
update_column(:import_jid, nil)
end
def running_or_pending_build_count(force: false)
Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
builds.running_or_pending.count(:all)
......@@ -1488,7 +1522,8 @@ class Project < ActiveRecord::Base
{ key: 'CI_PROJECT_PATH', value: full_path, public: true },
{ key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: web_url, public: true }
{ key: 'CI_PROJECT_URL', value: web_url, public: true },
{ key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level), public: true }
]
end
......@@ -1679,6 +1714,17 @@ class Project < ActiveRecord::Base
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki))
end
# Refreshes the expiration time of the associated import job ID.
#
# This method can be used by asynchronous importers to refresh the status,
# preventing the StuckImportJobsWorker from marking the import as failed.
def refresh_import_jid_expiration
return unless import_jid
Gitlab::SidekiqStatus
.set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
end
private
def storage
......
......@@ -1028,6 +1028,10 @@ class Repository
raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref)
end
def remote_exists?(name)
raw_repository.remote_exists?(name)
end
def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight)
end
......
......@@ -277,18 +277,23 @@ class User < ActiveRecord::Base
end
end
def for_github_id(id)
joins(:identities)
.where(identities: { provider: :github, extern_uid: id.to_s })
end
# Find a User by their primary email or any associated secondary email
def find_by_any_email(email)
sql = 'SELECT *
FROM users
WHERE id IN (
SELECT id FROM users WHERE email = :email
UNION
SELECT emails.user_id FROM emails WHERE email = :email
)
LIMIT 1;'
by_any_email(email).take
end
# Returns a relation containing all the users for the given Email address
def by_any_email(email)
users = where(email: email)
emails = joins(:emails).where(emails: { email: email })
union = Gitlab::SQL::Union.new([users, emails])
User.find_by_sql([sql, { email: email }]).first
from("(#{union.to_sql}) #{table_name}")
end
def existing_member?(email)
......
class GeoNodeStatusEntity < Grape::Entity
include ActionView::Helpers::NumberHelper
expose :id
expose :geo_node_id
expose :healthy?, as: :healthy
expose :health do |node|
......@@ -35,4 +35,6 @@ class GeoNodeStatusEntity < Grape::Entity
expose :last_event_timestamp
expose :cursor_last_event_id
expose :cursor_last_event_timestamp
expose :last_successful_status_check_timestamp
end
module Geo
class HashedStorageMigratedEventStore < EventStore
self.event_type = :hashed_storage_migrated_event
private
def build_event
Geo::HashedStorageMigratedEvent.new(
project: project,
old_storage_version: old_storage_version,
new_storage_version: project.storage_version,
repository_storage_name: project.repository.storage,
repository_storage_path: project.repository_storage_path,
old_disk_path: old_disk_path,
new_disk_path: project.disk_path,
old_wiki_disk_path: old_wiki_disk_path,
new_wiki_disk_path: project.wiki.disk_path
)
end
def old_storage_version
params.fetch(:old_storage_version)
end
def old_disk_path
params.fetch(:old_disk_path)
end
def old_wiki_disk_path
params.fetch(:old_wiki_disk_path)
end
end
end
module Geo
class HashedStorageMigrationService
attr_reader :project_id, :old_disk_path, :new_disk_path, :old_storage_version
def initialize(project_id, old_disk_path:, new_disk_path:, old_storage_version:)
@project_id = project_id
@old_disk_path = old_disk_path
@new_disk_path = new_disk_path
@old_storage_version = old_storage_version
end
def async_execute
Geo::HashedStorageMigrationWorker.perform_async(
project_id,
old_disk_path,
new_disk_path,
old_storage_version
)
end
def execute
project = Project.find(project_id)
project.expire_caches_before_rename(old_disk_path)
if migrating_from_legacy_storage?(project)
Geo::MoveRepositoryService.new(project, old_disk_path, new_disk_path).execute
end
true
end
private
def migrating_from_legacy_storage?(project)
from_legacy_storage? && project.hashed_storage?(:repository)
end
def from_legacy_storage?
old_storage_version.nil? || old_storage_version.zero?
end
end
end
......@@ -23,6 +23,8 @@ module Geo
end
def fetch_geo_node_metrics(node)
return unless node.enabled?
status = node_status(node)
unless status.success
......@@ -30,30 +32,29 @@ module Geo
return
end
NodeStatusService::STATUS_DATA.each do |key, docstring|
value = status[key]
update_db_metrics(node, status) if Gitlab::Geo.primary?
update_prometheus_metrics(node, status)
end
def update_db_metrics(node, status)
db_status = node.find_or_build_status
db_status.update_attributes(status.attributes.compact.merge(last_successful_status_check_at: Time.now.utc))
end
def update_prometheus_metrics(node, status)
GeoNodeStatus::PROMETHEUS_METRICS.each do |column, docstring|
value = status[column]
next unless value.is_a?(Integer)
gauge = Gitlab::Metrics.gauge(gauge_metric_name(key), docstring, {}, :max)
gauge = Gitlab::Metrics.gauge(gauge_metric_name(column), docstring, {}, :max)
gauge.set(metric_labels(node), value)
end
set_last_updated_at(node)
end
def node_status(node)
NodeStatusService.new.call(node)
end
def set_last_updated_at(node)
gauge = Gitlab::Metrics.gauge(
:geo_status_last_updated_timestamp,
'UNIX timestamp of last time Geo node status was updated internally',
{},
:max)
gauge.set(metric_labels(node), Time.now.to_i)
NodeStatusFetchService.new.call(node)
end
def increment_failed_status_counter(node)
......
......@@ -2,34 +2,26 @@ module Geo
class MoveRepositoryService
include Gitlab::ShellAdapter
attr_reader :id, :name, :old_path_with_namespace, :new_path_with_namespace
attr_reader :project, :old_disk_path, :new_disk_path
def initialize(id, name, old_path_with_namespace, new_path_with_namespace)
@id = id
@name = name
@old_path_with_namespace = old_path_with_namespace
@new_path_with_namespace = new_path_with_namespace
end
def async_execute
GeoRepositoryMoveWorker.perform_async(id, name, old_path_with_namespace, new_path_with_namespace)
def initialize(project, old_disk_path, new_disk_path)
@project = project
@old_disk_path = old_disk_path
@new_disk_path = new_disk_path
end
def execute
project = Project.find(id)
project.expire_caches_before_rename(old_path_with_namespace)
# Make sure target directory exists (used when transfering repositories)
project.ensure_storage_path_exists
if gitlab_shell.mv_repository(project.repository_storage_path,
old_path_with_namespace, new_path_with_namespace)
old_disk_path, new_disk_path)
# If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository
# So we basically we mute exceptions in next actions
begin
gitlab_shell.mv_repository(project.repository_storage_path,
"#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
"#{old_disk_path}.wiki", "#{new_disk_path}.wiki")
rescue
# Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks
......@@ -38,7 +30,7 @@ module Geo
else
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
raise Exception.new('repository cannot be renamed')
raise StandardError.new('Repository cannot be renamed')
end
true
......
......@@ -8,7 +8,7 @@ module Geo
end
def execute
GeoNode.create(params).persisted?
GeoNode.create(params)
end
end
end
module Geo
class NodeStatusService
class NodeStatusFetchService
include Gitlab::CurrentSettings
include HTTParty
STATUS_DATA = {
health: 'Summary of health status',
db_replication_lag_seconds: 'Database replication lag (seconds)',
repositories_count: 'Total number of repositories available on primary',
repositories_synced_count: 'Number of repositories synced on secondary',
repositories_failed_count: 'Number of repositories failed to sync on secondary',
lfs_objects_count: 'Total number of LFS objects available on primary',
lfs_objects_synced_count: 'Number of LFS objects synced on secondary',
lfs_objects_failed_count: 'Number of LFS objects failed to sync on secondary',
attachments_count: 'Total number of file attachments available on primary',
attachments_synced_count: 'Number of attachments synced on secondary',
attachments_failed_count: 'Number of attachments failed to sync on secondary',
last_event_id: 'Database ID of the latest event log entry on the primary',
last_event_timestamp: 'UNIX timestamp of the latest event log entry on the primary',
cursor_last_event_id: 'Last database ID of the event log processed by the secondary',
cursor_last_event_timestamp: 'Last UNIX timestamp of the event log processed by the secondary'
}.freeze
def call(geo_node)
data = { id: geo_node.id }
return GeoNodeStatus.current_node_status if geo_node.current?
data = { success: false }
begin
response = self.class.get(geo_node.status_url, headers: headers, timeout: timeout)
data[:success] = response.success?
if response.success?
data.merge!(response.parsed_response.symbolize_keys.slice(*STATUS_DATA.keys))
data.merge!(response.parsed_response)
else
message = "Could not connect to Geo node - HTTP Status Code: #{response.code} #{response.message}"
payload = response.parsed_response
......@@ -51,11 +35,7 @@ module Geo
data[:health] = e.message
end
GeoNodeStatus.new(data)
end
def status_keys
STATUS_DATA.stringify_keys.keys
GeoNodeStatus.from_json(data.as_json)
end
private
......
module Geo
class RenameRepositoryService
attr_reader :project_id, :old_disk_path, :new_disk_path
def initialize(project_id, old_disk_path, new_disk_path)
@project_id = project_id
@old_disk_path = old_disk_path
@new_disk_path = new_disk_path
end
def async_execute
Geo::RenameRepositoryWorker.perform_async(project_id, old_disk_path, new_disk_path)
end
def execute
project = Project.find(project_id)
project.expire_caches_before_rename(old_disk_path)
return true if project.hashed_storage?(:repository)
Geo::MoveRepositoryService.new(project, old_disk_path, new_disk_path).execute
end
end
end
......@@ -4,7 +4,7 @@ module Projects
prepend ::EE::Projects::HashedStorageMigrationService
attr_reader :old_disk_path, :new_disk_path
attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version
def initialize(project, logger = nil)
@project = project
......@@ -17,6 +17,7 @@ module Projects
@old_disk_path = project.disk_path
has_wiki = project.wiki.repository_exists?
@old_storage_version = project.storage_version
project.storage_version = Storage::HashedProject::STORAGE_VERSION
project.ensure_storage_path_exists
......@@ -25,7 +26,8 @@ module Projects
result = move_repository(@old_disk_path, @new_disk_path)
if has_wiki
result &&= move_repository("#{@old_disk_path}.wiki", "#{@new_disk_path}.wiki")
@old_wiki_disk_path = "#{@old_disk_path}.wiki"
result &&= move_repository(@old_wiki_disk_path, "#{@new_disk_path}.wiki")
end
unless result
......
......@@ -4,6 +4,18 @@ module Projects
Error = Class.new(StandardError)
# Returns true if this importer is supposed to perform its work in the
# background.
#
# This method will only return `true` if async importing is explicitly
# supported by an importer class (`Gitlab::GithubImport::ParallelImporter`
# for example).
def async?
return false unless has_importer?
!!importer_class.try(:async?)
end
def execute
add_repository_to_project unless project.gitlab_project_import?
......@@ -75,12 +87,16 @@ module Projects
end
end
def importer_class
Gitlab::ImportSources.importer(project.import_type)
end
def has_importer?
Gitlab::ImportSources.importer_names.include?(project.import_type)
end
def importer
Gitlab::ImportSources.importer(project.import_type).new(project)
importer_class.new(project)
end
def unknown_url?
......
......@@ -3,18 +3,24 @@ module Projects
def execute
return unless @project.forked?
@project.forked_from_project.lfs_objects.find_each do |lfs_object|
lfs_object.projects << @project
if fork_source = @project.fork_source
fork_source.lfs_objects.find_each do |lfs_object|
lfs_object.projects << @project
end
refresh_forks_count(fork_source)
end
merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project)
merge_requests = @project.fork_network
.merge_requests
.opened
.where.not(target_project: @project)
.from_project(@project)
merge_requests.each do |mr|
::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end
refresh_forks_count(@project.forked_from_project)
@project.fork_network_member.destroy
@project.forked_project_link.destroy
end
......
......@@ -481,17 +481,7 @@ module SystemNoteService
#
# Returns Boolean
def cross_reference_exists?(noteable, mentioner)
# Initial scope should be system notes of this noteable type
notes = Note.system.where(noteable_type: noteable.class)
notes =
if noteable.is_a?(Commit)
# Commits have non-integer IDs, so they're stored in `commit_id`
notes.where(commit_id: noteable.id)
else
notes.where(noteable_id: noteable.id)
end
notes = noteable.notes.system
notes_for_mentioner(mentioner, noteable, notes).exists?
end
......
- empty_repo = @project.empty_repo?
- fork_network = @project.fork_network
- forked_from_project = @project.forked_from_project || fork_network&.root_project
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
.limit-container-width{ class: container_class }
.avatar-container.s70.project-avatar
......@@ -16,13 +15,13 @@
- if @project.forked?
%p
- if forked_from_project
- if @project.fork_source
#{ s_('ForkedFromProjectPath|Forked from') }
= link_to project_path(forked_from_project) do
= forked_from_project.full_name
= link_to project_path(@project.fork_source) do
= fork_source_name(@project)
- else
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
= deleted_message % { project_name: fork_network.deleted_root_project_name }
= deleted_message % { project_name: fork_source_name(@project) }
- if @project.mirror?
- import_url = @project.safe_import_url
......
- ref = local_assigns.fetch(:ref)
- show_project_name = local_assigns.fetch(:show_project_name, false)
- if @note_counts
- note_count = @note_counts.fetch(commit.id, 0)
- else
- notes = commit.notes
- note_count = notes.user.count
- cache_key = [project.full_path, commit.id, current_application_settings, note_count, @path.presence, current_controller?(:commits), I18n.locale]
- cache_key = [project.full_path, commit.id, current_application_settings, @path.presence, current_controller?(:commits), I18n.locale]
- cache_key.push(commit.status(ref)) if commit.status(ref)
-# EE-only
- show_project_name = local_assigns.fetch(:show_project_name, false)
- cache_key << show_project_name
= cache(cache_key, expires_in: 1.day) do
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
......
......@@ -185,7 +185,10 @@
%p
This will remove the fork relationship to source project
= succeed "." do
= link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)
- if @project.fork_source
= link_to(fork_source_name(@project), project_path(@project.fork_source))
- else
= fork_source_name(@project)
= form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f|
%p
%strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
......
# frozen_string_literal: true
module Gitlab
module GithubImport
# NotifyUponDeath can be included into a GitHub worker class if it should
# notify any JobWaiter instances upon being moved to the Sidekiq dead queue.
#
# Note that this will only notify the waiter upon graceful termination, a
# SIGKILL will still result in the waiter _not_ being notified.
#
# Workers including this module must have jobs passed where the last
# argument is the key to notify, as a String.
module NotifyUponDeath
extend ActiveSupport::Concern
included do
# If a job is being exhausted we still want to notify the
# AdvanceStageWorker. This prevents the entire import from getting stuck
# just because 1 job threw too many errors.
sidekiq_retries_exhausted do |job|
args = job['args']
jid = job['jid']
if args.length == 3 && (key = args.last) && key.is_a?(String)
JobWaiter.notify(key, jid)
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
# ObjectImporter defines the base behaviour for every Sidekiq worker that
# imports a single resource such as a note or pull request.
module ObjectImporter
extend ActiveSupport::Concern
included do
include Sidekiq::Worker
include GithubImport::Queue
include ReschedulingMethods
include NotifyUponDeath
end
# project - An instance of `Project` to import the data into.
# client - An instance of `Gitlab::GithubImport::Client`
# hash - A Hash containing the details of the object to import.
def import(project, client, hash)
object = representation_class.from_json_hash(hash)
importer_class.new(object, project, client).execute
counter.increment(project: project.path_with_namespace)
end
def counter
@counter ||= Gitlab::Metrics.counter(counter_name, counter_description)
end
# Returns the representation class to use for the object. This class must
# define the class method `from_json_hash`.
def representation_class
raise NotImplementedError
end
# Returns the class to use for importing the object.
def importer_class
raise NotImplementedError
end
# Returns the name (as a Symbol) of the Prometheus counter.
def counter_name
raise NotImplementedError
end
# Returns the description (as a String) of the Prometheus counter.
def counter_description
raise NotImplementedError
end
end
end
end
module Gitlab
module GithubImport
module Queue
extend ActiveSupport::Concern
included do
# If a job produces an error it may block a stage from advancing
# forever. To prevent this from happening we prevent jobs from going to
# the dead queue. This does mean some resources may not be imported, but
# this is better than a project being stuck in the "import" state
# forever.
sidekiq_options queue: 'github_importer', dead: false, retry: 5
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
# Module that provides methods shared by the various workers used for
# importing GitHub projects.
module ReschedulingMethods
# project_id - The ID of the GitLab project to import the note into.
# hash - A Hash containing the details of the GitHub object to imoprt.
# notify_key - The Redis key to notify upon completion, if any.
def perform(project_id, hash, notify_key = nil)
project = Project.find_by(id: project_id)
return notify_waiter(notify_key) unless project
client = GithubImport.new_client_for(project, parallel: true)
if try_import(project, client, hash)
notify_waiter(notify_key)
else
# In the event of hitting the rate limit we want to reschedule the job
# so its retried after our rate limit has been reset.
self.class
.perform_in(client.rate_limit_resets_in, project.id, hash, notify_key)
end
end
def try_import(*args)
import(*args)
true
rescue RateLimitError
false
end
def notify_waiter(key = nil)
JobWaiter.notify(key, jid) if key
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
module StageMethods
# project_id - The ID of the GitLab project to import the data into.
def perform(project_id)
return unless (project = find_project(project_id))
client = GithubImport.new_client_for(project)
try_import(client, project)
end
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def try_import(client, project)
import(client, project)
rescue RateLimitError
self.class.perform_in(client.rate_limit_resets_in, project.id)
end
def find_project(id)
# If the project has been marked as failed we want to bail out
# automatically.
Project.import_started.find_by(id: id)
end
end
end
end
module Geo
class HashedStorageMigrationWorker
include Sidekiq::Worker
include GeoQueue
def perform(project_id, old_disk_path, new_disk_path, old_storage_version)
Geo::HashedStorageMigrationService.new(
project_id,
old_disk_path: old_disk_path,
new_disk_path: new_disk_path,
old_storage_version: old_storage_version
).execute
end
end
end
......@@ -22,7 +22,7 @@ module Geo
end
cursor_last_event_ids = Gitlab::Geo.secondary_nodes.map do |node|
Geo::NodeStatusService.new.call(node).cursor_last_event_id
node.status&.cursor_last_event_id
end
if cursor_last_event_ids.include?(nil)
......
module Geo
class RenameRepositoryWorker
include Sidekiq::Worker
include GeoQueue
def perform(project_id, old_disk_path, new_disk_path)
Geo::RenameRepositoryService.new(project_id, old_disk_path, new_disk_path).execute
end
end
end
class GeoRepositoryMoveWorker
include Sidekiq::Worker
include GeoQueue
def perform(id, name, old_path_with_namespace, new_path_with_namespace)
Geo::MoveRepositoryService.new(id, name, old_path_with_namespace, new_path_with_namespace).execute
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
# AdvanceStageWorker is a worker used by the GitHub importer to wait for a
# number of jobs to complete, without blocking a thread. Once all jobs have
# been completed this worker will advance the import process to the next
# stage.
class AdvanceStageWorker
include Sidekiq::Worker
sidekiq_options queue: 'github_importer_advance_stage', dead: false
INTERVAL = 30.seconds.to_i
# The number of seconds to wait (while blocking the thread) before
# continueing to the next waiter.
BLOCKING_WAIT_TIME = 5
# The known importer stages and their corresponding Sidekiq workers.
STAGES = {
issues_and_diff_notes: Stage::ImportIssuesAndDiffNotesWorker,
notes: Stage::ImportNotesWorker,
finish: Stage::FinishImportWorker
}.freeze
# project_id - The ID of the project being imported.
# waiters - A Hash mapping Gitlab::JobWaiter keys to the number of
# remaining jobs.
# next_stage - The name of the next stage to start when all jobs have been
# completed.
def perform(project_id, waiters, next_stage)
return unless (project = find_project(project_id))
new_waiters = wait_for_jobs(waiters)
if new_waiters.empty?
# We refresh the import JID here so workers importing individual
# resources (e.g. notes) don't have to do this all the time, reducing
# the pressure on Redis. We _only_ do this once all jobs are done so
# we don't get stuck forever if one or more jobs failed to notify the
# JobWaiter.
project.refresh_import_jid_expiration
STAGES.fetch(next_stage.to_sym).perform_async(project_id)
else
self.class.perform_in(INTERVAL, project_id, new_waiters, next_stage)
end
end
def wait_for_jobs(waiters)
waiters.each_with_object({}) do |(key, remaining), new_waiters|
waiter = JobWaiter.new(remaining, key)
# We wait for a brief moment of time so we don't reschedule if we can
# complete the work fast enough.
waiter.wait(BLOCKING_WAIT_TIME)
next unless waiter.jobs_remaining.positive?
new_waiters[waiter.key] = waiter.jobs_remaining
end
end
def find_project(id)
# We only care about the import JID so we can refresh it. We also only
# want the project if it hasn't been marked as failed yet. It's possible
# the import gets marked as stuck when jobs of the current stage failed
# somehow.
Project.select(:import_jid).import_started.find_by(id: id)
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
class ImportDiffNoteWorker
include ObjectImporter
def representation_class
Representation::DiffNote
end
def importer_class
Importer::DiffNoteImporter
end
def counter_name
:github_importer_imported_diff_notes
end
def counter_description
'The number of imported GitHub pull request review comments'
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
class ImportIssueWorker
include ObjectImporter
def representation_class
Representation::Issue
end
def importer_class
Importer::IssueAndLabelLinksImporter
end
def counter_name
:github_importer_imported_issues
end
def counter_description
'The number of imported GitHub issues'
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
class ImportNoteWorker
include ObjectImporter
def representation_class
Representation::Note
end
def importer_class
Importer::NoteImporter
end
def counter_name
:github_importer_imported_notes
end
def counter_description
'The number of imported GitHub comments'
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
class ImportPullRequestWorker
include ObjectImporter
def representation_class
Representation::PullRequest
end
def importer_class
Importer::PullRequestImporter
end
def counter_name
:github_importer_imported_pull_requests
end
def counter_description
'The number of imported GitHub pull requests'
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
class RefreshImportJidWorker
include Sidekiq::Worker
include GithubImport::Queue
# The interval to schedule new instances of this job at.
INTERVAL = 1.minute.to_i
def self.perform_in_the_future(*args)
perform_in(INTERVAL, *args)
end
# project_id - The ID of the project that is being imported.
# check_job_id - The ID of the job for which to check the status.
def perform(project_id, check_job_id)
return unless (project = find_project(project_id))
if SidekiqStatus.running?(check_job_id)
# As long as the repository is being cloned we want to keep refreshing
# the import JID status.
project.refresh_import_jid_expiration
self.class.perform_in_the_future(project_id, check_job_id)
end
# If the job is no longer running there's nothing else we need to do. If
# the clone job completed successfully it will have scheduled the next
# stage, if it died there's nothing we can do anyway.
end
def find_project(id)
Project.select(:import_jid).import_started.find_by(id: id)
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
module Stage
class FinishImportWorker
include Sidekiq::Worker
include GithubImport::Queue
include StageMethods
# project - An instance of Project.
def import(_, project)
project.after_import
report_import_time(project)
end
def report_import_time(project)
duration = Time.zone.now - project.created_at
path = project.path_with_namespace
histogram.observe({ project: path }, duration)
counter.increment
logger.info("GitHub importer finished for #{path} in #{duration.round(2)} seconds")
end
def histogram
@histogram ||= Gitlab::Metrics.histogram(
:github_importer_total_duration_seconds,
'Total time spent importing GitHub projects, in seconds'
)
end
def counter
@counter ||= Gitlab::Metrics.counter(
:github_importer_imported_projects,
'The number of imported GitHub projects'
)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
module Stage
class ImportBaseDataWorker
include Sidekiq::Worker
include GithubImport::Queue
include StageMethods
# These importers are fast enough that we can just run them in the same
# thread.
IMPORTERS = [
Importer::LabelsImporter,
Importer::MilestonesImporter,
Importer::ReleasesImporter
].freeze
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
IMPORTERS.each do |klass|
klass.new(project, client).execute
end
project.refresh_import_jid_expiration
ImportPullRequestsWorker.perform_async(project.id)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
module Stage
class ImportIssuesAndDiffNotesWorker
include Sidekiq::Worker
include GithubImport::Queue
include StageMethods
# The importers to run in this stage. Issues can't be imported earlier
# on as we also use these to enrich pull requests with assigned labels.
IMPORTERS = [
Importer::IssuesImporter,
Importer::DiffNotesImporter
].freeze
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
waiters = IMPORTERS.each_with_object({}) do |klass, hash|
waiter = klass.new(project, client).execute
hash[waiter.key] = waiter.jobs_remaining
end
AdvanceStageWorker.perform_async(project.id, waiters, :notes)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module GithubImport
module Stage
class ImportNotesWorker
include Sidekiq::Worker
include GithubImport::Queue
include StageMethods
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
waiter = Importer::NotesImporter
.new(project, client)
.execute
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
:finish
)
end
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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