Commit 3569294e authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 342f33be e9b8fb43
......@@ -75,7 +75,7 @@ stages:
.use-mysql: &use-mysql
services:
- mysql:latest
- mysql:5.7
- redis:alpine
.rails5-variables: &rails5-variables
......
......@@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
Lint/NestedPercentLiteral:
Exclude:
- 'lib/gitlab/git/repository.rb'
- 'spec/support/email_format_shared_examples.rb'
- 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1
Lint/ReturnInVoidContext:
......@@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
- 'spec/lib/gitlab/diff/parser_spec.rb'
- 'spec/lib/json_web_token/rsa_token_spec.rb'
- 'spec/models/commit_spec.rb'
- 'spec/support/repo_helpers.rb'
- 'spec/support/seed_repo.rb'
- 'spec/support/helpers/repo_helpers.rb'
- 'spec/support/helpers/seed_repo.rb'
# Offense count: 112
# Configuration parameters: Blacklist.
......@@ -496,7 +496,7 @@ Style/EmptyLiteral:
- 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/requests/api/jobs_spec.rb'
- 'spec/support/chat_slash_commands_shared_examples.rb'
- 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
# Offense count: 102
# Cop supports --auto-correct.
......
......@@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release)
- [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker)
......@@ -127,6 +129,8 @@ Most issues will have labels for at least one of the following:
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
All labels, their meaning and priority are defined on the
[labels page][labels-page].
......@@ -210,7 +214,7 @@ This label documents the planned timeline & urgency which is used to measure aga
| Label | Meaning | Estimate time to fix | Guidance |
|-------|-----------------|------------------------------------------------------------------|----------|
| ~P1 | Urgent Priority | The current release | |
| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
| ~P2 | High Priority | The next release | |
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
......
......@@ -178,7 +178,7 @@ GEM
docile (1.1.5)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.1)
doorkeeper (4.3.2)
railties (>= 4.2)
doorkeeper-openid_connect (1.3.0)
doorkeeper (~> 4.3)
......
......@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) {
this.available();
this.enable();
this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
} else if (data.has_related_branch) {
} else {
this.hide();
}
})
.catch(() => {
this.unavailable();
this.disable();
Flash('Failed to check if a new branch can be created.');
Flash(__('Failed to check related branches.'));
});
}
......@@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.remove('hide');
}
updateBranchName(suggestedBranchName) {
this.branchInput.value = suggestedBranchName;
this.updateCreatePaths('branch', suggestedBranchName);
}
updateInputState(target, ref, result) {
// target - 'branch' or 'ref' - which the input field we are searching a ref for.
// ref - string - what a user typed.
// result - string - what has been found on backend.
const pathReplacement = `$1${ref}`;
// If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists.
if (ref === result) {
......@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true;
this.refInput.dataset.value = ref;
this.showAvailableMessage('ref');
this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
pathReplacement);
this.updateCreatePaths(target, ref);
}
} else if (target === 'branch') {
this.branchIsValid = true;
this.showAvailableMessage('branch');
this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
pathReplacement);
this.updateCreatePaths(target, ref);
} else {
this.refIsValid = false;
this.refInput.dataset.value = ref;
......@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
this.disableCreateAction();
}
}
// target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) {
const pathReplacement = `$1${ref}`;
this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
pathReplacement);
}
}
......@@ -32,26 +32,38 @@ export default {
required: true,
},
buttonDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
data() {
return {
isDisabled: false,
linkRequested: '',
};
},
computed: {
cssClass() {
const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
isDisabled() {
return this.buttonDisabled === this.link;
},
watch: {
requestFinishedFor() {
if (this.requestFinishedFor === this.linkRequested) {
this.isDisabled = false;
}
},
},
methods: {
onClickAction() {
$(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
},
},
};
......@@ -62,7 +74,8 @@ export default {
@click="onClickAction"
v-tooltip
:title="tooltipText"
class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
class="js-ci-action btn btn-blank
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass"
data-container="body"
:disabled="isDisabled"
......
<script>
import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
*/
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
tooltipText: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
actionMethod: {
type: String,
required: true,
},
actionIcon: {
type: String,
required: true,
},
},
};
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body"
aria-label="Job's action"
>
<icon :name="actionIcon" />
</a>
</template>
<script>
import $ from 'jquery';
import jobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import $ from 'jquery';
import JobNameComponent from './job_name_component.vue';
import JobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
/**
* Renders the dropdown for the pipeline graph.
*
* The following object should be provided as `job`:
......@@ -27,14 +27,14 @@
* }
* }
*/
export default {
export default {
directives: {
tooltip,
},
components: {
jobComponent,
jobNameComponent,
JobComponent,
JobNameComponent,
},
props: {
......@@ -42,6 +42,11 @@
type: Object,
required: true,
},
requestFinishedFor: {
type: String,
required: false,
default: '',
},
},
computed: {
......@@ -56,22 +61,23 @@
methods: {
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
* When the user right clicks or cmd/ctrl + click in the job name or the action icon
* the dropdown should not be closed so we stop propagation
* of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
$(
'.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
this.$el,
).on('click', e => {
e.stopPropagation();
});
},
},
};
};
</script>
<template>
<div class="ci-job-dropdown-container">
......@@ -101,8 +107,8 @@
:key="i">
<job-component
:job="item"
:is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item"
:request-finished-for="requestFinishedFor"
/>
</li>
</ul>
......
......@@ -7,7 +7,6 @@ export default {
StageColumnComponent,
LoadingIcon,
},
props: {
isLoading: {
type: Boolean,
......@@ -17,10 +16,10 @@ export default {
type: Object,
required: true,
},
actionDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
......@@ -75,7 +74,7 @@ export default {
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
:action-disabled="actionDisabled"
:request-finished-for="requestFinishedFor"
/>
</ul>
</div>
......
<script>
import ActionComponent from './action_component.vue';
import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
......@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default {
components: {
ActionComponent,
DropdownActionComponent,
JobNameComponent,
},
directives: {
tooltip,
},
......@@ -44,26 +41,17 @@ export default {
type: Object,
required: true,
},
cssClassJobName: {
type: String,
required: false,
default: '',
},
isDropdown: {
type: Boolean,
required: false,
default: false,
},
actionDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
computed: {
status() {
return this.job && this.job.status ? this.job.status : {};
......@@ -134,19 +122,11 @@ export default {
</div>
<action-component
v-if="hasAction && !isDropdown"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:button-disabled="actionDisabled"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:action-method="status.action.method"
:request-finished-for="requestFinishedFor"
/>
</div>
</template>
......@@ -29,10 +29,11 @@ export default {
required: false,
default: '',
},
actionDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
......@@ -74,12 +75,12 @@ export default {
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
:action-disabled="actionDisabled"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
:request-finished-for="requestFinishedFor"
/>
</li>
......
......@@ -25,7 +25,7 @@ export default () => {
data() {
return {
mediator,
actionDisabled: null,
requestFinishedFor: null,
};
},
created() {
......@@ -36,15 +36,17 @@ export default () => {
},
methods: {
postAction(action) {
this.actionDisabled = action;
// Click was made, reset this variable
this.requestFinishedFor = null;
this.mediator.service.postAction(action)
this.mediator.service
.postAction(action)
.then(() => {
this.mediator.refreshPipeline();
this.actionDisabled = null;
this.requestFinishedFor = action;
})
.catch(() => {
this.actionDisabled = null;
this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.'));
});
},
......@@ -54,7 +56,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
actionDisabled: this.actionDisabled,
requestFinishedFor: this.requestFinishedFor,
},
});
},
......@@ -79,7 +81,8 @@ export default () => {
},
methods: {
postAction(action) {
this.mediator.service.postAction(action.path)
this.mediator.service
.postAction(action.path)
.then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.')));
},
......
<script>
import ciIcon from './ci_icon.vue';
import tooltip from '../directives/tooltip';
/**
import CiIcon from './ci_icon.vue';
import tooltip from '../directives/tooltip';
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
*
......@@ -22,9 +22,9 @@
* - MR widget
*/
export default {
export default {
components: {
ciIcon,
CiIcon,
},
directives: {
tooltip,
......@@ -46,7 +46,7 @@
return className ? `ci-status ci-${className}` : 'ci-status';
},
},
};
};
</script>
<template>
<a
......
<script>
import icon from '../../vue_shared/components/icon.vue';
import Icon from '../../vue_shared/components/icon.vue';
/**
/**
* Renders CI icon based on API response shared between all places where it is used.
*
* Receives status object containing:
......@@ -22,9 +22,9 @@
* - Jobs show view header
* - Jobs show view sidebar
*/
export default {
export default {
components: {
icon,
Icon,
},
props: {
status: {
......@@ -32,14 +32,13 @@
required: true,
},
},
computed: {
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
},
};
};
</script>
<template>
<span :class="cssClass">
......
<script>
/**
/**
* Falls back to the code used in `copy_to_clipboard.js`
*
* Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
* when clicked.
*
* @example
* <clipboard-button
* title="Copy to clipbard"
* text="Content to be copied"
* css-class="btn-transparent"
* />
*/
import tooltip from '../directives/tooltip';
import tooltip from '../directives/tooltip';
export default {
export default {
name: 'ClipboardButton',
directives: {
tooltip,
......@@ -34,7 +44,7 @@
default: 'btn-default',
},
},
};
};
</script>
<template>
......
<script>
import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
import tooltip from '../directives/tooltip';
import icon from '../../vue_shared/components/icon.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
import tooltip from '../directives/tooltip';
import Icon from '../../vue_shared/components/icon.vue';
export default {
export default {
directives: {
tooltip,
},
components: {
userAvatarLink,
icon,
UserAvatarLink,
Icon,
},
props: {
/**
......@@ -94,10 +93,7 @@
* @returns {Boolean}
*/
hasAuthor() {
return this.author &&
this.author.avatar_url &&
this.author.path &&
this.author.username;
return this.author && this.author.avatar_url && this.author.path && this.author.username;
},
/**
* If information about the author is provided will return a string
......@@ -106,14 +102,10 @@
* @returns {String}
*/
userImageAltDescription() {
return this.author &&
this.author.username ? `${this.author.username}'s avatar` : null;
return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
},
},
created() {
this.commitIconSvg = commitIconSvg;
},
};
};
</script>
<template>
<div class="branch-commit">
......@@ -141,11 +133,10 @@
{{ commitRef.name }}
</a>
</template>
<div
v-html="commitIconSvg"
<icon
name="commit"
class="commit-icon js-commit-icon"
>
</div>
/>
<a
class="commit-sha"
......
<script>
import { __ } from '~/locale';
/**
import { __ } from '~/locale';
/**
* Port of detail_behavior expand button.
*
* @example
......@@ -10,7 +10,7 @@
* </template>
* </expand-button>
*/
export default {
export default {
name: 'ExpandButton',
data() {
return {
......@@ -27,7 +27,7 @@
this.isCollapsed = !this.isCollapsed;
},
},
};
};
</script>
<template>
<span>
......
<script>
import ciIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue';
import CiIconBadge from './ci_badge_link.vue';
import LoadingIcon from './loading_icon.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip';
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
/**
/**
* Renders header component for job and pipeline page based on UI mockups
*
* Used in:
* - job show page
* - pipeline show page
*/
export default {
export default {
components: {
ciIconBadge,
loadingIcon,
timeagoTooltip,
userAvatarImage,
CiIconBadge,
LoadingIcon,
TimeagoTooltip,
UserAvatarImage,
},
directives: {
tooltip,
......@@ -72,7 +72,7 @@
this.$emit('actionClicked', action);
},
},
};
};
</script>
<template>
......
<script>
/* This is a re-usable vue component for rendering a svg sprite
/* This is a re-usable vue component for rendering a svg sprite
icon
Sample configuration:
......@@ -11,11 +10,11 @@
css-classes="top"
/>
*/
// only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
*/
// only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default {
export default {
props: {
name: {
type: String,
......@@ -70,7 +69,7 @@
return this.size ? `s${this.size}` : '';
},
},
};
};
</script>
<template>
......@@ -79,7 +78,8 @@
:width="width"
:height="height"
:x="x"
:y="y">
:y="y"
>
<use v-bind="{ 'xlink:href':spriteHref }" />
</svg>
</template>
<script>
import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue';
......@@ -98,11 +99,18 @@ export default {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
},
methods: {
handleClick(label) {
this.$emit('onLabelClick', label);
},
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
handleDropdownHidden() {
this.$emit('onDropdownClose');
},
},
};
</script>
......@@ -112,6 +120,7 @@ export default {
<dropdown-value-collapsed
v-if="showCreate"
:labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/>
<dropdown-title
:can-edit="canEdit"
......@@ -133,7 +142,10 @@ export default {
:name="hiddenInputName"
:label="label"
/>
<div class="dropdown">
<div
class="dropdown"
ref="dropdown"
>
<dropdown-button
:ability-name="abilityName"
:field-name="hiddenInputName"
......
......@@ -26,6 +26,11 @@ export default {
return labelsString;
},
},
methods: {
handleClick() {
this.$emit('onValueClick');
},
},
};
</script>
......@@ -36,6 +41,7 @@ export default {
data-placement="left"
data-container="body"
:title="labelsList"
@click="handleClick"
>
<i
aria-hidden="true"
......
......@@ -468,6 +468,14 @@
margin-bottom: 10px;
white-space: normal;
.ci-job-dropdown-container {
// override dropdown.scss
.dropdown-menu li button {
padding: 0;
text-align: center;
}
}
// ensure .build-content has hover style when action-icon is hovered
.ci-job-dropdown-container:hover .build-content {
@extend .build-content:hover;
......
......@@ -134,11 +134,11 @@ class Projects::IssuesController < Projects::ApplicationController
def can_create_branch
can_create = current_user &&
can?(current_user, :push_code, @project) &&
@issue.can_be_worked_on?(current_user)
@issue.can_be_worked_on?
respond_to do |format|
format.json do
render json: { can_create_branch: can_create, has_related_branch: @issue.has_related_branch? }
render json: { can_create_branch: can_create, suggested_branch_name: @issue.suggested_branch_name }
end
end
end
......@@ -177,7 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?
end
def render_issue_json
......
......@@ -78,8 +78,6 @@ class Projects::JobsController < Projects::ApplicationController
result.merge!(trace.to_h)
end
result[:html] = result[:html].presence || 'No job log'
render json: result
end
end
......
......@@ -32,6 +32,7 @@ class UsersFinder
users = by_active(users)
users = by_external_identity(users)
users = by_external(users)
users = by_2fa(users)
users = by_created_at(users)
users = by_custom_attributes(users)
......@@ -76,4 +77,15 @@ class UsersFinder
users.external
end
def by_2fa(users)
case params[:two_factor]
when 'enabled'
users.with_two_factor
when 'disabled'
users.without_two_factor
else
users
end
end
end
......@@ -27,8 +27,9 @@ module AtomicInternalId
module ClassMethods
def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName
before_validation(on: :create) do
if read_attribute(column).blank?
scope_attrs = { scope => association(scope).reader }
scope_value = association(scope).reader
if read_attribute(column).blank? && scope_value
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
usage = self.class.table_name.to_sym
new_iid = InternalId.generate_next(self, scope_attrs, usage, init)
......
......@@ -31,12 +31,13 @@ module Avatarable
asset_host = ActionController::Base.asset_host
use_asset_host = asset_host.present?
use_authentication = respond_to?(:public?) && !public?
# Avatars for private and internal groups and projects require authentication to be viewed,
# which means they can only be served by Rails, on the regular GitLab host.
# If an asset host is configured, we need to return the fully qualified URL
# instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
if use_asset_host && respond_to?(:public?) && !public?
if use_asset_host && use_authentication
use_asset_host = false
only_path = false
end
......@@ -49,6 +50,6 @@ module Avatarable
url_base << gitlab_config.relative_url_root
end
url_base + avatar.url
url_base + avatar.local_url
end
end
module NonatomicInternalId
extend ActiveSupport::Concern
included do
validate :set_iid, on: :create
validates :iid, presence: true, numericality: true
end
def set_iid
if iid.blank?
parent = project || group
records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend
max_iid = records.maximum(:iid)
self.iid = max_iid.to_i + 1
end
end
def to_param
iid.to_s
end
end
......@@ -102,7 +102,7 @@ module Routable
# the route. Caching this per request ensures that even if we have multiple instances,
# we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path
return uncached_full_path unless RequestStore.active?
return uncached_full_path unless RequestStore.active? && persisted?
RequestStore[full_path_key] ||= uncached_full_path
end
......@@ -124,6 +124,11 @@ module Routable
end
end
# Group would override this to check from association
def owned_by?(user)
owner == user
end
private
def set_path_errors
......
# Uniquify
#
# Return a version of the given 'base' string that is unique
# by appending a counter to it. Uniqueness is determined by
# repeated calls to the passed block.
#
# You can pass an initial value for the counter, if not given
# counting starts from 1.
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
class Uniquify
# Return a version of the given 'base' string that is unique
# by appending a counter to it. Uniqueness is determined by
# repeated calls to the passed block.
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
def initialize(counter = nil)
@counter = counter
end
def string(base)
@base = base
@counter = nil
increment_counter! while yield(base_string)
base_string
......
class Deployment < ActiveRecord::Base
include NonatomicInternalId
include AtomicInternalId
belongs_to :project, required: true
belongs_to :environment, required: true
belongs_to :user
belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.deployments&.maximum(:iid) }
validates :sha, presence: true
validates :ref, presence: true
......
......@@ -125,6 +125,10 @@ class Group < Namespace
self[:lfs_enabled]
end
def owned_by?(user)
owners.include?(user)
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users(
self,
......
......@@ -12,8 +12,9 @@
# * (Optionally) add columns to `internal_ids` if needed for scope.
class InternalId < ActiveRecord::Base
belongs_to :project
belongs_to :namespace
enum usage: { issues: 0 }
enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4 }
validates :usage, presence: true
......
......@@ -194,6 +194,15 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request
end
def suggested_branch_name
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
start_counting_from = 2
Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
project.repository.branch_exists?(suggested_branch_name)
end
end
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
def has_related_branch?
......@@ -248,11 +257,8 @@ class Issue < ActiveRecord::Base
end
end
def can_be_worked_on?(current_user)
!self.closed? &&
!self.project.forked? &&
self.related_branches(current_user).empty? &&
self.closed_by_merge_requests(current_user).empty?
def can_be_worked_on?
!self.closed? && !self.project.forked?
end
# Returns `true` if the current issue can be viewed by either a logged in User
......
class MergeRequest < ActiveRecord::Base
include NonatomicInternalId
include AtomicInternalId
include Issuable
include Noteable
include Referable
......@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User"
has_internal_id :iid, scope: :target_project, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }
has_many :merge_request_diffs
has_one :merge_request_diff,
......
......@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base
Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField
include NonatomicInternalId
include AtomicInternalId
include Sortable
include Referable
include StripAttribute
......@@ -21,6 +21,9 @@ class Milestone < ActiveRecord::Base
belongs_to :project
belongs_to :group
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.milestones&.maximum(:iid) }
has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) }
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
......
require "flowdock-git-hook"
# Flow dock depends on Grit to compute the number of commits between two given
# commits. To make this depend on Gitaly, a monkey patch is applied
module Flowdock
class Git
# pass down a Repository all the way down
def repo
@options[:repo]
end
def config
{}
end
def messages
Git::Builder.new(repo: repo,
ref: @ref,
before: @from,
after: @to,
commit_url: @commit_url,
branch_url: @branch_url,
diff_url: @diff_url,
repo_url: @repo_url,
repo_name: @repo_name,
permanent_refs: @permanent_refs,
tags: tags
).to_hashes
end
class Builder
def commits
@repo.commits_between(@before, @after).map do |commit|
{
url: @opts[:commit_url] ? @opts[:commit_url] % [commit.sha] : nil,
id: commit.sha,
message: commit.message,
author: {
name: commit.author_name,
email: commit.author_email
}
}
end
end
end
end
end
class FlowdockService < Service
prop_accessor :token
validates :token, presence: true, if: :activated?
......@@ -34,7 +80,7 @@ class FlowdockService < Service
data[:before],
data[:after],
token: token,
repo: project.repository.path_to_repo,
repo: project.repository,
repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s",
diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
......
......@@ -260,7 +260,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
OpenStruct.new(enabled: auto_devops_enabled?,
label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
link: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
elsif auto_devops_enabled?
OpenStruct.new(enabled: true,
label: _('Auto DevOps enabled'),
......
......@@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
!!model
end
def local_url
File.join('/', self.class.base_dir, dynamic_segment, filename)
end
private
# Designed to be overridden by child uploaders that have a dynamic path
......
......@@ -21,7 +21,7 @@
.help-block
Manage repository storage paths. Learn more in the
= succeed "." do
= link_to "repository storages documentation", help_page_path("administration/repository_storages")
= link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
.sub-section
%h4 Circuit breaker
.form-group
......
......@@ -22,7 +22,7 @@
%hr
%p
- link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'))
- link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'))
- link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project))
= s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster }
......
......@@ -15,7 +15,7 @@
- unless @repository.gitlab_ci_yml
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do
= link_to project_ci_lint_path(@project), class: 'btn btn-default' do
%span CI lint
.content-list.builds-content-list
......
......@@ -26,7 +26,7 @@
%ul
- pipeline.yaml_errors.split(",").each do |error|
%li= error
You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)}
- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
.bs-callout.bs-callout-warning
......
.row.prepend-top-default
.col-lg-12
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
%fieldset.builds-feature
.form-group
- message = auto_devops_warning_message(@project)
- ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong= s_('CICD|Enable Auto DevOps')
%br
= s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong= s_('CICD|Disable Auto DevOps')
%br
= s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
.radio
= form.label :enabled_ do
= form.radio_button :enabled, ''
%strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
%br
= s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
= form.label :domain, class:"prepend-top-10" do
= _('Domain')
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
.help-block
= s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
= f.submit 'Save changes', class: "btn btn-success prepend-top-15"
......@@ -3,44 +3,6 @@
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
%fieldset.builds-feature
.form-group
%h5 Auto DevOps (Beta)
%p
Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
- message = auto_devops_warning_message(@project)
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong Disable Auto DevOps
%br
%span.descr
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
.radio
= form.label :enabled_ do
= form.radio_button :enabled, ''
%strong Instance default (#{Gitlab::CurrentSettings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
%p
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr
.form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, "Runner token", class: 'label-light'
.form-control.js-secret-value-placeholder
......
......@@ -12,10 +12,22 @@
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Update your CI/CD configuration, like job timeout or Auto DevOps.
Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.
.settings-content
= render 'form'
%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('CICD|Auto DevOps (Beta)')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
= link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
.settings-content
= render 'autodevops_form'
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
......
......@@ -9,7 +9,7 @@
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
.banner-buttons
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout'
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn js-close-callout'
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss Auto DevOps box' }
......
---
title: Prevent pipeline actions in dropdown to redirct to a new page
merge_request:
author:
type: fixed
---
title: Add an API endpoint to download git repository snapshots
merge_request: 18173
author:
type: added
---
title: Show new branch/mr button even when branch exists
merge_request: 17712
author: Jacopo Beschi @jacopo-beschi
type: added
---
title: Fix discussions API setting created_at for notable in a group or notable in
a project in a group with owners
merge_request: 18464
author:
type: fixed
---
title: Create settings section for autodevops
merge_request: 18321
author:
type: changed
---
title: Fixed wrong avatar URL when the avatar is on object storage.
merge_request: 18092
author:
type: fixed
---
title: Fix project creation for user endpoint when jobs_enabled parameter supplied
merge_request:
author:
type: fixed
---
title: Removes 'No Job log' message from build trace
merge_request: 18523
author:
type: fixed
---
title: Update links to /ci/lint with ones to project ci/lint
merge_request: 18539
author: Takuya Noguchi
type: fixed
---
title: Transition to atomic internal ids for all models.
merge_request: 44259
author:
type: other
---
title: Add missing changelog type to docs
merge_request: 18526
author: "@blackst0ne"
type: other
---
title: Add 2FA filter to users API for admins only
merge_request: 18503
author:
type: changed
---
title: Fix missing namespace for some internal users
merge_request: 18357
author:
type: fixed
---
title: Don't include lfs_file_locks data in export bundle
merge_request: 18495
author:
type: fixed
---
title: Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication
merge_request: 18543
author:
type: fixed
# rubocop:disable GitlabSecurity/PublicSend
require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible
class Settings < Settingslogic
source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" }
namespace Rails.env
class << self
def gitlab_on_standard_port?
on_standard_port?(gitlab)
end
def host_without_www(url)
host(url).sub('www.', '')
end
def build_gitlab_ci_url
custom_port =
if on_standard_port?(gitlab)
nil
else
":#{gitlab.port}"
end
[
gitlab.protocol,
"://",
gitlab.host,
custom_port,
gitlab.relative_url_root
].join('')
end
def build_pages_url
base_url(pages).join('')
end
def build_gitlab_shell_ssh_path_prefix
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
if gitlab_shell.ssh_port != 22
"ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
else
if gitlab_shell.ssh_host.include? ':'
"[#{user_host}]:"
else
"#{user_host}:"
end
end
end
def build_base_gitlab_url
base_url(gitlab).join('')
end
def build_gitlab_url
(base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
# check that values in `current` (string or integer) is a contant in `modul`.
def verify_constant_array(modul, current, default)
values = default || []
unless current.nil?
values = []
current.each do |constant|
values.push(verify_constant(modul, constant, nil))
end
values.delete_if { |value| value.nil? }
end
values
end
# check that `current` (string or integer) is a contant in `modul`.
def verify_constant(modul, current, default)
constant = modul.constants.find { |name| modul.const_get(name) == current }
value = constant.nil? ? default : modul.const_get(constant)
if current.is_a? String
value = modul.const_get(current.upcase) rescue default
end
value
end
def absolute(path)
File.expand_path(path, Rails.root)
end
private
def base_url(config)
custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
[
config.protocol,
"://",
config.host,
custom_port
]
end
def on_standard_port?(config)
config.port.to_i == (config.https ? 443 : 80)
end
# Extract the host part of the given +url+.
def host(url)
url = url.downcase
url = "http://#{url}" unless url.start_with?('http')
# Get rid of the path so that we don't even have to encode it
url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
URI.parse(url_without_path).host
end
# Runs every minute in a random ten-minute period on Sundays, to balance the
# load on the server receiving these pings. The usage ping is safe to run
# multiple times because of a 24 hour exclusive lock.
def cron_for_usage_ping
hour = rand(24)
minute = rand(6)
"#{minute}0-#{minute}9 #{hour} * * 0"
end
end
end
require_dependency File.expand_path('../../lib/gitlab', __dir__) # Load Gitlab as soon as possible
# Default settings
Settings['ldap'] ||= Settingslogic.new({})
......
module Gitlab
def self.config
Settings
end
VERSION = File.read(Rails.root.join("VERSION")).strip.freeze
REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze
end
require './spec/support/sidekiq'
require './spec/support/test_env'
require './spec/support/helpers/test_env'
class Gitlab::Seeder::CycleAnalytics
def initialize(project, perf: false)
......
class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration
DOWNTIME = false
def up
connection.exec_query(users_query.to_sql).rows.each do |id, username|
create_namespace(id, username)
# When testing locally I've noticed that these internal users are missing
# the notification email, for more details visit the below link:
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18357#note_68327560
set_notification_email(id)
end
end
def down
# no-op
end
private
def users
@users ||= Arel::Table.new(:users)
end
def namespaces
@namespaces ||= Arel::Table.new(:namespaces)
end
def users_query
condition = users[:ghost].eq(true)
if column_exists?(:users, :support_bot)
condition = condition.or(users[:support_bot].eq(true))
end
users.join(namespaces, Arel::Nodes::OuterJoin)
.on(namespaces[:type].eq(nil).and(namespaces[:owner_id].eq(users[:id])))
.where(namespaces[:owner_id].eq(nil))
.where(condition)
.project(users[:id], users[:username])
end
def create_namespace(user_id, username)
path = Uniquify.new.string(username) do |str|
query = "SELECT id FROM namespaces WHERE parent_id IS NULL AND path='#{str}' LIMIT 1"
connection.exec_query(query).present?
end
insert_query = "INSERT INTO namespaces(owner_id, path, name) VALUES(#{user_id}, '#{path}', '#{path}')"
namespace_id = connection.insert_sql(insert_query)
create_route(namespace_id)
end
def create_route(namespace_id)
return unless namespace_id
row = connection.exec_query("SELECT id, path FROM namespaces WHERE id=#{namespace_id}").first
id, path = row.values_at('id', 'path')
execute("INSERT INTO routes(source_id, source_type, path, name) VALUES(#{id}, 'Namespace', '#{path}', '#{path}')")
end
def set_notification_email(user_id)
execute "UPDATE users SET notification_email = email WHERE notification_email IS NULL AND id = #{user_id}"
end
end
class AddFurtherScopeColumnsToInternalIdTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
change_column_null :internal_ids, :project_id, true
add_column :internal_ids, :namespace_id, :integer, null: true
end
def down
change_column_null :internal_ids, :project_id, false
remove_column :internal_ids, :namespace_id
end
end
class AddIndexConstraintsToInternalIdTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :internal_ids, [:usage, :namespace_id], unique: true, where: 'namespace_id IS NOT NULL'
replace_index(:internal_ids, [:usage, :project_id], name: 'index_internal_ids_on_usage_and_project_id') do
add_concurrent_index :internal_ids, [:usage, :project_id], unique: true, where: 'project_id IS NOT NULL'
end
add_concurrent_foreign_key :internal_ids, :namespaces, column: :namespace_id, on_delete: :cascade
end
def down
remove_concurrent_index :internal_ids, [:usage, :namespace_id]
replace_index(:internal_ids, [:usage, :project_id], name: 'index_internal_ids_on_usage_and_project_id') do
add_concurrent_index :internal_ids, [:usage, :project_id], unique: true
end
remove_foreign_key :internal_ids, column: :namespace_id
end
private
def replace_index(table, columns, name:)
temporary_name = "#{name}_old"
if index_exists?(table, columns, name: name)
rename_index table, name, temporary_name
end
yield
remove_concurrent_index_by_name table, temporary_name
end
end
......@@ -896,12 +896,14 @@ ActiveRecord::Schema.define(version: 20180418053107) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
create_table "internal_ids", id: :bigserial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "project_id"
t.integer "usage", null: false
t.integer "last_value", null: false
t.integer "namespace_id"
end
add_index "internal_ids", ["usage", "project_id"], name: "index_internal_ids_on_usage_and_project_id", unique: true, using: :btree
add_index "internal_ids", ["usage", "namespace_id"], name: "index_internal_ids_on_usage_and_namespace_id", unique: true, where: "(namespace_id IS NOT NULL)", using: :btree
add_index "internal_ids", ["usage", "project_id"], name: "index_internal_ids_on_usage_and_project_id", unique: true, where: "(project_id IS NOT NULL)", using: :btree
create_table "issue_assignees", id: false, force: :cascade do |t|
t.integer "user_id", null: false
......@@ -2113,6 +2115,7 @@ ActiveRecord::Schema.define(version: 20180418053107) do
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "internal_ids", "namespaces", name: "fk_162941d509", on_delete: :cascade
add_foreign_key "internal_ids", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
......
......@@ -650,6 +650,47 @@ gitlab_rails['redis_sentinels'] = [
Omnibus GitLab configures some things behind the curtains to make the sysadmins'
lives easier. If you want to know what happens underneath keep reading.
### Running multiple Redis clusters
GitLab supports running [separate Redis clusters for different persistent
classes](https://docs.gitlab.com/omnibus/settings/redis.html#running-with-multiple-redis-instances):
cache, queues, and shared_state. To make this work with Sentinel:
1. Set the appropriate variable in `/etc/gitlab/gitlab.rb` for each instance you are using:
```ruby
gitlab_rails['redis_cache_instance'] = REDIS_CACHE_URL
gitlab_rails['redis_queues_instance'] = REDIS_QUEUES_URL
gitlab_rails['redis_shared_state_instance'] = REDIS_SHARED_STATE_URL
```
**Note**: Redis URLs should be in the format: `redis://:PASSWORD@SENTINEL_MASTER_NAME`
1. PASSWORD is the plaintext password for the Redis instance
2. SENTINEL_MASTER_NAME is the Sentinel master name (e.g. `gitlab-redis-cache`)
1. Include an array of hashes with host/port combinations, such as the following:
```ruby
gitlab_rails['redis_cache_sentinels'] = [
{ host: REDIS_CACHE_SENTINEL_HOST, port: PORT1 },
{ host: REDIS_CACHE_SENTINEL_HOST2, port: PORT2 }
]
gitlab_rails['redis_queues_sentinels'] = [
{ host: REDIS_QUEUES_SENTINEL_HOST, port: PORT1 },
{ host: REDIS_QUEUES_SENTINEL_HOST2, port: PORT2 }
]
gitlab_rails['redis_shared_state_sentinels'] = [
{ host: SHARED_STATE_SENTINEL_HOST, port: PORT1 },
{ host: SHARED_STATE_SENTINEL_HOST2, port: PORT2 }
]
```
1. Note that for each persistence class, GitLab will default to using the
configuration specified in `gitlab_rails['redis_sentinels']` unless
overriden by the settings above.
1. Be sure to include BOTH configuration options for each persistent classes. For example,
if you choose to configure a cache instance, you must specify both `gitlab_rails['redis_cache_instance']`
and `gitlab_rails['redis_cache_sentinels']` for GitLab to generate the proper configuration files.
1. Run `gitlab-ctl reconfigure`
### Control running services
In the previous example, we've used `redis_sentinel_role` and
......
......@@ -119,11 +119,17 @@ The Pages daemon doesn't listen to the outside world.
1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`:
```ruby
```shell
pages_external_url 'http://example.io'
```
1. [Reconfigure GitLab][reconfigure]
1. Restart gitlab-pages by running the following command:
```shell
sudo gitlab-ctl restart gitlab-pages
```
Watch the [video tutorial][video-admin] for this configuration.
......@@ -143,7 +149,7 @@ outside world.
1. Place the certificate and key inside `/etc/gitlab/ssl`
1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
```ruby
```shell
pages_external_url 'https://example.io'
pages_nginx['redirect_http_to_https'] = true
......@@ -155,6 +161,11 @@ outside world.
respectively.
1. [Reconfigure GitLab][reconfigure]
1. Restart gitlab-pages by running the following command:
```shell
sudo gitlab-ctl restart gitlab-pages
```
## Advanced configuration
......@@ -180,7 +191,7 @@ world. Custom domains are supported, but no TLS.
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
```shell
pages_external_url "http://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false
......@@ -192,6 +203,11 @@ world. Custom domains are supported, but no TLS.
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
1. Restart gitlab-pages by running the following command:
```shell
sudo gitlab-ctl restart gitlab-pages
```
### Custom domains with TLS support
......@@ -210,7 +226,7 @@ world. Custom domains and TLS are supported.
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
```shell
pages_external_url "https://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false
......@@ -225,6 +241,11 @@ world. Custom domains and TLS are supported.
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
1. Restart gitlab-pages by running the following command:
```shell
sudo gitlab-ctl restart gitlab-pages
```
### Custom domain verification
......@@ -247,11 +268,16 @@ are stored.
If you wish to store them in another location you must set it up in
`/etc/gitlab/gitlab.rb`:
```ruby
```shell
gitlab_rails['pages_path'] = "/mnt/storage/pages"
```
1. [Reconfigure GitLab][reconfigure]
1. Restart gitlab-pages by running the following command:
```shell
sudo gitlab-ctl restart gitlab-pages
```
## Set maximum pages size
......
......@@ -112,13 +112,13 @@ POST /projects/import
| `file` | string | yes | The file to be uploaded |
| `path` | string | yes | Name and path for new project |
| `overwrite` | boolean | no | If there is a project with the same path the import will overwrite it. Default to false |
| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] |
| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md) |
The override params passed will take precendence over all values defined inside the export file.
The override params passed will take precedence over all values defined inside the export file.
To upload a file from your filesystem, use the `--form` argument. This causes
To upload a file from your file system, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`.
The `file=` parameter must point to a file on your filesystem and be preceded
The `file=` parameter must point to a file on your file system and be preceded
by `@`. For example:
```console
......
......@@ -1399,3 +1399,26 @@ Read more in the [Project Badges](project_badges.md) documentation.
## Issue and merge request description templates
The non-default [issue and merge request description templates](../user/project/description_templates.md) are managed inside the project's repository. So you can manage them via the API through the [Repositories API](repositories.md) and the [Repository Files API](repository_files.md).
## Download snapshot of a git repository
> Introduced in GitLab 10.7
This endpoint may only be accessed by an administrative user.
Download a snapshot of the project (or wiki, if requested) git repository. This
snapshot is always in uncompressed [tar](https://en.wikipedia.org/wiki/Tar_(computing))
format.
If a repository is corrupted to the point where `git clone` does not work, the
snapshot may allow some of the data to be retrieved.
```
GET /projects/:id/snapshot
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `wiki` | boolean | no | Whether to download the wiki, rather than project, repository |
......@@ -55,6 +55,7 @@ GET /users
| --------- | ---- | -------- | ----------- |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users |
```json
[
......
......@@ -22,7 +22,7 @@ The `merge_request` value is a reference to a merge request that adds this
entry, and the `author` key is used to give attribution to community
contributors. **Both are optional**.
The `type` field maps the category of the change,
valid options are: added, fixed, changed, deprecated, removed, security, other. **Type field is mandatory**.
valid options are: added, fixed, changed, deprecated, removed, security, performance, other. **Type field is mandatory**.
Community contributors and core team members are encouraged to add their name to
the `author` field. GitLab team members **should not**.
......
......@@ -90,6 +90,25 @@ Finished in 34.51 seconds (files took 0.76702 seconds to load)
Note: `live_debug` only works on javascript enabled specs.
### Fast unit tests
Some classes are well-isolated from Rails and you should be able to test them
without the overhead added by the Rails environment and Bundler's `:default`
group's gem loading. In these cases, you can `require 'fast_spec_helper'`
instead of `require 'spec_helper'` in your test file, and your test should run
really fast since:
- Gems loading is skipped
- Rails app boot is skipped
- gitlab-shell and Gitaly setup are skipped
- Test repositories setup are skipped
Note that in some cases, you might have to add some `require_dependency 'foo'`
in your file under test since Rails autoloading is not available in these cases.
This shouldn't be a problem since explicitely listing dependencies should be
considered a good practice anyway.
### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce
......@@ -281,14 +300,13 @@ All fixtures should be be placed under `spec/fixtures/`.
RSpec config files are files that change the RSpec config (i.e.
`RSpec.configure do |config|` blocks). They should be placed under
`spec/support/config/`.
`spec/support/`.
Each file should be related to a specific domain, e.g.
`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc.
`spec/support/capybara.rb`, `spec/support/carrierwave.rb`, etc.
Helpers can be included in the `spec/support/config/rspec.rb` file. If a
helpers module applies only to a certain kind of specs, it should add modifiers
to the `config.include` call. For instance if
If a helpers module applies only to a certain kind of specs, it should add
modifiers to the `config.include` call. For instance if
`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and
`type: :model` specs only, you would write the following:
......@@ -299,6 +317,14 @@ RSpec.configure do |config|
end
```
If a config file only consists of `config.include`, you can add these
`config.include` directly in `spec/spec_helper.rb`.
For very generic helpers, consider including them in the `spec/support/rspec.rb`
file which is used by the `spec/fast_spec_helper.rb` file. See
[Fast unit tests](#fast-unit-tests) for more details about the
`spec/fast_spec_helper.rb` file.
---
[Return to Testing documentation](index.md)
......@@ -9,7 +9,7 @@ At a minimum, requiring the Rake helper will redirect `stdout`, include the
runtime task helpers, and include the `RakeHelpers` Spec support module.
The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
executing tasks simple. See `spec/support/rake_helpers.rb` for all available
executing tasks simple. See `spec/support/helpers/rake_helpers.rb` for all available
methods.
Example:
......
......@@ -23,7 +23,7 @@ You can create as many deploy tokens as you like from the settings of your proje
![Personal access tokens page](img/deploy_tokens.png)
## Revoking a personal access token
## Revoking a deploy token
At any time, you can revoke any deploy token by just clicking the
respective **Revoke** button under the 'Active deploy tokens' area.
......
......@@ -12,7 +12,11 @@ end
WebMock.enable!
%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper webmock).each do |f|
%w(select2_helper test_env repo_helpers wait_for_requests project_forks_helper).each do |f|
require Rails.root.join('spec', 'support', 'helpers', f)
end
%w(sidekiq webmock).each do |f|
require Rails.root.join('spec', 'support', f)
end
......
......@@ -154,6 +154,7 @@ module API
mount ::API::ProjectHooks
mount ::API::Projects
mount ::API::ProjectMilestones
mount ::API::ProjectSnapshots
mount ::API::ProjectSnippets
mount ::API::ProtectedBranches
mount ::API::Repositories
......
......@@ -64,8 +64,10 @@ module API
authorize! :create_note, noteable
parent = noteable_parent(noteable)
if opts[:created_at]
opts.delete(:created_at) unless current_user.admin? || parent.owner == current_user
opts.delete(:created_at) unless
current_user.admin? || parent.owned_by?(current_user)
end
project = parent if parent.is_a?(Project)
......
module API
module Helpers
module ProjectSnapshotsHelpers
def authorize_read_git_snapshot!
authenticated_with_full_private_access!
end
def send_git_snapshot(repository)
header(*Gitlab::Workhorse.send_git_snapshot(repository))
end
def snapshot_project
user_project
end
def snapshot_repository
if to_boolean(params[:wiki])
snapshot_project.wiki.repository
else
snapshot_project.repository
end
end
end
end
end
module API
class ProjectSnapshots < Grape::API
helpers ::API::Helpers::ProjectSnapshotsHelpers
before { authorize_read_git_snapshot! }
resource :projects do
desc 'Download a (possibly inconsistent) snapshot of a repository' do
detail 'This feature was introduced in GitLab 10.7'
end
params do
optional :wiki, type: Boolean, desc: 'Set to true to receive the wiki repository'
end
get ':id/snapshot' do
send_git_snapshot(snapshot_repository)
end
end
end
end
......@@ -74,6 +74,11 @@ module API
present options[:with].prepare_relation(projects, options), options
end
def translate_params_for_compatibility(params)
params[:builds_enabled] = params.delete(:jobs_enabled) if params.key?(:jobs_enabled)
params
end
end
resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
......@@ -123,7 +128,7 @@ module API
end
post do
attrs = declared_params(include_missing: false)
attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled)
attrs = translate_params_for_compatibility(attrs)
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
......@@ -155,6 +160,7 @@ module API
not_found!('User') unless user
attrs = declared_params(include_missing: false)
attrs = translate_params_for_compatibility(attrs)
project = ::Projects::CreateService.new(user, attrs).execute
if project.saved?
......@@ -276,7 +282,7 @@ module API
authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility].present?
attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled)
attrs = translate_params_for_compatibility(attrs)
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
......
......@@ -77,7 +77,7 @@ module API
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
unless current_user&.admin?
params.except!(:created_after, :created_before, :order_by, :sort)
params.except!(:created_after, :created_before, :order_by, :sort, :two_factor)
end
users = UsersFinder.new(current_user, params).execute
......
require_dependency 'gitlab/git'
require_dependency 'settings'
require_dependency 'gitlab/popen'
module Gitlab
def self.root
Pathname.new(File.expand_path('..', __dir__))
end
def self.config
Settings
end
COM_URL = 'https://gitlab.com'.freeze
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
VERSION = File.read(root.join("VERSION")).strip.freeze
REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze
def self.com?
# Check `gl_subdomain?` as well to keep parity with gitlab.com
......
require_dependency 'gitlab/encoding_helper'
module Gitlab
module Git
# The ID of empty tree.
......
module Gitlab
module Wiki
module Git
class CommitterWithHooks < Gollum::Committer
attr_reader :gl_wiki
......@@ -9,6 +9,9 @@ module Gitlab
end
def commit
# TODO: Remove after 10.8
return super unless allowed_to_run_hooks?
result = Gitlab::Git::OperationService.new(git_user, gl_wiki.repository).with_branch(
@wiki.ref,
start_branch_name: @wiki.ref
......@@ -24,6 +27,11 @@ module Gitlab
private
# TODO: Remove after 10.8
def allowed_to_run_hooks?
@options[:user_id] != 0 && @options[:username].present?
end
def git_user
@git_user ||= Gitlab::Git::User.new(@options[:username],
@options[:name],
......
......@@ -1258,6 +1258,10 @@ module Gitlab
true
end
def create_from_snapshot(url, auth)
gitaly_repository_client.create_from_snapshot(url, auth)
end
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
gitaly_migrate(:rebase) do |is_enabled|
if is_enabled
......
......@@ -290,7 +290,7 @@ module Gitlab
end
def committer_with_hooks(commit_details)
Gitlab::Wiki::CommitterWithHooks.new(self, commit_details.to_h)
Gitlab::Git::CommitterWithHooks.new(self, commit_details.to_h)
end
def with_committer_with_hooks(commit_details, &block)
......
......@@ -235,6 +235,22 @@ module Gitlab
)
end
def create_from_snapshot(http_url, http_auth)
request = Gitaly::CreateRepositoryFromSnapshotRequest.new(
repository: @gitaly_repo,
http_url: http_url,
http_auth: http_auth
)
GitalyClient.call(
@storage,
:repository_service,
:create_repository_from_snapshot,
request,
timeout: GitalyClient.default_timeout
)
end
def write_ref(ref_path, ref, old_ref, shell)
request = Gitaly::WriteRefRequest.new(
repository: @gitaly_repo,
......
......@@ -27,8 +27,6 @@ project_tree:
- :releases
- project_members:
- :user
- lfs_file_locks:
- :user
- merge_requests:
- notes:
- :author
......
......@@ -81,6 +81,20 @@ module Gitlab
]
end
def send_git_snapshot(repository)
params = {
'GitalyServer' => gitaly_server_hash(repository),
'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new(
repository: repository.gitaly_repository
).to_json
}
[
SEND_DATA_HEADER,
"git-snapshot:#{encode(params)}"
]
end
def send_git_diff(repository, diff_refs)
params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
......
require 'settingslogic'
class Settings < Settingslogic
source ENV.fetch('GITLAB_CONFIG') { Pathname.new(File.expand_path('..', __dir__)).join('config/gitlab.yml') }
namespace ENV.fetch('GITLAB_ENV') { Rails.env }
class << self
def gitlab_on_standard_port?
on_standard_port?(gitlab)
end
def host_without_www(url)
host(url).sub('www.', '')
end
def build_gitlab_ci_url
custom_port =
if on_standard_port?(gitlab)
nil
else
":#{gitlab.port}"
end
[
gitlab.protocol,
"://",
gitlab.host,
custom_port,
gitlab.relative_url_root
].join('')
end
def build_pages_url
base_url(pages).join('')
end
def build_gitlab_shell_ssh_path_prefix
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
if gitlab_shell.ssh_port != 22
"ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
else
if gitlab_shell.ssh_host.include? ':'
"[#{user_host}]:"
else
"#{user_host}:"
end
end
end
def build_base_gitlab_url
base_url(gitlab).join('')
end
def build_gitlab_url
(base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
# check that values in `current` (string or integer) is a contant in `modul`.
def verify_constant_array(modul, current, default)
values = default || []
unless current.nil?
values = []
current.each do |constant|
values.push(verify_constant(modul, constant, nil))
end
values.delete_if { |value| value.nil? }
end
values
end
# check that `current` (string or integer) is a contant in `modul`.
def verify_constant(modul, current, default)
constant = modul.constants.find { |name| modul.const_get(name) == current }
value = constant.nil? ? default : modul.const_get(constant)
if current.is_a? String
value = modul.const_get(current.upcase) rescue default
end
value
end
def absolute(path)
File.expand_path(path, Rails.root)
end
private
def base_url(config)
custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
[
config.protocol,
"://",
config.host,
custom_port
]
end
def on_standard_port?(config)
config.port.to_i == (config.https ? 443 : 80)
end
# Extract the host part of the given +url+.
def host(url)
url = url.downcase
url = "http://#{url}" unless url.start_with?('http')
# Get rid of the path so that we don't even have to encode it
url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
URI.parse(url_without_path).host
end
# Runs every minute in a random ten-minute period on Sundays, to balance the
# load on the server receiving these pings. The usage ping is safe to run
# multiple times because of a 24 hour exclusive lock.
def cron_for_usage_ping
hour = rand(24)
minute = rand(6)
"#{minute}0-#{minute}9 #{hour} * * 0"
end
end
end
......@@ -1776,6 +1776,9 @@ msgstr ""
msgid "Failed to change the owner"
msgstr ""
msgid "Failed to check related branches."
msgstr ""
msgid "Failed to remove issue from board, please try again."
msgstr ""
......
module RuboCop
module SpecHelpers
SPEC_HELPERS = %w[spec_helper.rb rails_helper.rb].freeze
SPEC_HELPERS = %w[fast_spec_helper.rb rails_helper.rb spec_helper.rb].freeze
# Returns true if the given node originated from the spec directory.
def in_spec?(node)
......
......@@ -223,11 +223,12 @@ describe Import::BitbucketController do
end
context 'user has chosen an existing nested namespace and name for the project', :postgresql do
let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' }
before do
parent_namespace.add_owner(user)
nested_namespace.add_owner(user)
end
......@@ -273,7 +274,7 @@ describe Import::BitbucketController do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
let!(:parent_namespace) { create(:group, name: 'foo') }
before do
parent_namespace.add_owner(user)
......
......@@ -196,10 +196,11 @@ describe Import::GitlabController do
end
context 'user has chosen an existing nested namespace for the project', :postgresql do
let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
before do
parent_namespace.add_owner(user)
nested_namespace.add_owner(user)
end
......@@ -245,7 +246,7 @@ describe Import::GitlabController do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
let!(:parent_namespace) { create(:group, name: 'foo') }
before do
parent_namespace.add_owner(user)
......
......@@ -4,7 +4,11 @@ describe Projects::ForksController do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:forked_project) { Projects::ForkService.new(project, user).execute }
let(:group) { create(:group, owner: forked_project.creator) }
let(:group) { create(:group) }
before do
group.add_owner(user)
end
describe 'GET index' do
def get_forks
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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