Commit 5fb962e3 authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into ce-to-ee-2017-12-06

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents 5dbecb8e 44921579
......@@ -181,7 +181,7 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0'
# Cache
gem 'redis-rails', '~> 5.0.1'
gem 'redis-rails', '~> 5.0.2'
# Redis
gem 'redis', '~> 3.2'
......
......@@ -728,24 +728,24 @@ GEM
recursive-open-struct (1.0.0)
redcarpet (3.4.0)
redis (3.3.3)
redis-actionpack (5.0.1)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0)
redis-activesupport (5.0.1)
redis-store (>= 1.1.0, < 2)
redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
redis-store (~> 1.2.0)
redis-store (>= 1.3, < 2)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
redis-rack (1.6.0)
rack (~> 1.5)
redis-store (~> 1.2.0)
redis-rails (5.0.1)
redis-actionpack (~> 5.0.0)
redis-activesupport (~> 5.0.0)
redis-store (~> 1.2.0)
redis-store (1.2.0)
redis (>= 2.2)
redis-rack (2.0.3)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
redis-store (1.4.1)
redis (>= 2.2, < 5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
......@@ -1168,7 +1168,7 @@ DEPENDENCIES
redcarpet (~> 3.4)
redis (~> 3.2)
redis-namespace (~> 1.5.2)
redis-rails (~> 5.0.1)
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
rouge (~> 2.0)
......
......@@ -130,7 +130,8 @@ freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the
subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts].
[Automatic CE->EE merge][automatic_ce_ee_merge] and
[Guidelines for implementing Enterprise Edition features][ee_features].
### After the 7th
......@@ -281,4 +282,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
[done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done
[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html
[automatic_ce_ee_merge]: https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
[ee_features]: https://docs.gitlab.com/ce/development/ee_features.html
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
window.Compare = (function() {
function Compare(opts) {
export default class Compare {
constructor(opts) {
this.opts = opts;
this.source_loading = $(".js-source-loading");
this.target_loading = $(".js-target-loading");
......@@ -34,12 +34,12 @@ window.Compare = (function() {
this.initialState();
}
Compare.prototype.initialState = function() {
initialState() {
this.getSourceHtml();
return this.getTargetHtml();
};
this.getTargetHtml();
}
Compare.prototype.getTargetProject = function() {
getTargetProject() {
return $.ajax({
url: this.opts.targetProjectUrl,
data: {
......@@ -52,22 +52,22 @@ window.Compare = (function() {
return $('.js-target-branch-dropdown .dropdown-content').html(html);
}
});
};
}
Compare.prototype.getSourceHtml = function() {
return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
getSourceHtml() {
return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
ref: $("input[name='merge_request[source_branch]']").val()
});
};
}
Compare.prototype.getTargetHtml = function() {
return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
getTargetHtml() {
return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
ref: $("input[name='merge_request[target_branch]']").val()
});
};
}
Compare.prototype.sendAjax = function(url, loading, target, data) {
static sendAjax(url, loading, target, data) {
var $target;
$target = $(target);
return $.ajax({
......@@ -84,7 +84,5 @@ window.Compare = (function() {
gl.utils.localTimeAgo($('.js-timeago', className));
}
});
};
return Compare;
})();
}
}
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
window.CompareAutocomplete = (function() {
function CompareAutocomplete() {
this.initDropdown();
}
CompareAutocomplete.prototype.initDropdown = function() {
return $('.js-compare-dropdown').each(function() {
export default function initCompareAutocomplete() {
$('.js-compare-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
selected = $dropdown.data('selected');
......@@ -62,7 +57,4 @@ window.CompareAutocomplete = (function() {
}
});
});
};
return CompareAutocomplete;
})();
}
......@@ -22,8 +22,8 @@ import NewCommitForm from './new_commit_form';
import Project from './project';
import projectAvatar from './project_avatar';
/* global MergeRequest */
/* global Compare */
/* global CompareAutocomplete */
import Compare from './compare';
import initCompareAutocomplete from './compare_autocomplete';
/* global PathLocks */
/* global ProjectFindFile */
import ProjectNew from './project_new';
......@@ -715,7 +715,7 @@ import initGroupAnalytics from './init_group_analytics';
projectAvatar();
switch (path[1]) {
case 'compare':
new CompareAutocomplete();
initCompareAutocomplete();
break;
case 'edit':
shortcut_handler = new ShortcutsNavigation();
......
This diff is collapsed.
......@@ -123,18 +123,23 @@ export default {
:title="group.fullName"
class="no-expand"
data-placement="top"
>
{{group.name}}
</a>
>{{
// ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending
group.name
}}</a>
<span
v-if="group.permission"
class="access-type"
class="user-access-role"
>
{{s__('GroupsTreeRole|as')}} {{group.permission}}
{{group.permission}}
</span>
</div>
<div
class="description">{{group.description}}</div>
v-if="group.description"
class="description">
{{group.description}}
</div>
</div>
<group-folder
v-if="group.isOpen && hasChildren"
......
......@@ -248,7 +248,6 @@ export default {
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/>
<div v-else>
......
......@@ -17,11 +17,6 @@
type: String,
required: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
......@@ -47,7 +42,6 @@
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
>
<textarea
......
......@@ -41,11 +41,6 @@
required: false,
default: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
......@@ -94,7 +89,6 @@
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/>
<edit-actions
......
......@@ -9,7 +9,7 @@ export default class Job {
this.state = null;
this.options = options || $('.js-build-options').data();
this.pageUrl = this.options.pageUrl;
this.pagePath = this.options.pagePath;
this.buildStatus = this.options.buildStatus;
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
......@@ -167,11 +167,11 @@ export default class Job {
getBuildTrace() {
return $.ajax({
url: `${this.pageUrl}/trace.json`,
url: `${this.pagePath}/trace.json`,
data: { state: this.state },
})
.done((log) => {
setCiStatusFavicon(`${this.pageUrl}/status.json`);
setCiStatusFavicon(`${this.pagePath}/status.json`);
if (log.state) {
this.state = log.state;
......@@ -209,7 +209,7 @@ export default class Job {
}
if (log.status !== this.buildStatus) {
gl.utils.visitUrl(this.pageUrl);
gl.utils.visitUrl(this.pagePath);
}
})
.fail(() => {
......
......@@ -40,9 +40,6 @@ import './admin';
import './aside';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
......
......@@ -86,7 +86,7 @@
<div class="note-actions">
<span
v-if="accessLevel"
class="note-role note-role-access">{{accessLevel}}</span>
class="note-role user-access-role">{{accessLevel}}</span>
<div
v-if="canAddAwardEmoji"
class="note-actions-item">
......
......@@ -8,10 +8,18 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
data() {
const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const currentUserData = parsedUserData ? {
id: parsedUserData.id,
name: parsedUserData.name,
username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path,
} : {};
return {
noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData),
currentUserData,
notesData: {
lastFetchedAt: notesDataset.lastFetchedAt,
discussionsPath: notesDataset.discussionsPath,
......
import Flash from '../../../flash';
import AssigneeTitle from './assignee_title';
import Assignees from './assignees';
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
export default {
name: 'SidebarAssignees',
data() {
return {
mediator: new Mediator(),
store: new Store(),
loading: false,
field: '',
};
},
props: {
mediator: {
type: Object,
required: true,
},
field: {
type: String,
required: true,
},
signedIn: {
type: Boolean,
required: false,
default: false,
},
},
components: {
'assignee-title': AssigneeTitle,
assignees: Assignees,
......@@ -61,10 +71,6 @@ export default {
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
},
beforeMount() {
this.field = this.$el.dataset.field;
this.signedIn = typeof this.$el.dataset.signedIn !== 'undefined';
},
template: `
<div>
<assignee-title
......
<script>
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import participants from './participants.vue';
export default {
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
},
props: {
mediator: {
type: Object,
required: true,
},
},
components: {
participants,
},
......@@ -21,6 +25,7 @@ export default {
<participants
:loading="store.isFetching.participants"
:participants="store.participants"
:number-of-less-participants="7" />
:number-of-less-participants="7"
/>
</div>
</template>
<script>
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
import { __ } from '../../../locale';
......@@ -9,11 +8,15 @@ import subscriptions from './subscriptions.vue';
export default {
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
},
props: {
mediator: {
type: Object,
required: true,
},
},
components: {
subscriptions,
},
......
......@@ -10,6 +10,27 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
function mountAssigneesComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-assignees');
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
SidebarAssignees,
},
render: createElement => createElement('sidebar-assignees', {
props: {
mediator,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
},
}),
});
}
function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point');
......@@ -49,9 +70,10 @@ function mountLockComponent(mediator) {
}).$mount(el);
}
function mountParticipantsComponent() {
function mountParticipantsComponent(mediator) {
const el = document.querySelector('.js-sidebar-participants-entry-point');
// eslint-disable-next-line no-new
if (!el) return;
// eslint-disable-next-line no-new
......@@ -60,11 +82,15 @@ function mountParticipantsComponent() {
components: {
sidebarParticipants,
},
render: createElement => createElement('sidebar-participants', {}),
render: createElement => createElement('sidebar-participants', {
props: {
mediator,
},
}),
});
}
function mountSubscriptionsComponent() {
function mountSubscriptionsComponent(mediator) {
const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
if (!el) return;
......@@ -75,22 +101,35 @@ function mountSubscriptionsComponent() {
components: {
sidebarSubscriptions,
},
render: createElement => createElement('sidebar-subscriptions', {}),
render: createElement => createElement('sidebar-subscriptions', {
props: {
mediator,
},
}),
});
}
function mount(mediator) {
const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) {
new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
}
function mountTimeTrackingComponent() {
const el = document.getElementById('issuable-time-tracker');
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
SidebarTimeTracking,
},
render: createElement => createElement('sidebar-time-tracking', {}),
});
}
export function mountSidebar(mediator) {
mountAssigneesComponent(mediator);
mountConfidentialComponent(mediator);
mountLockComponent(mediator);
mountParticipantsComponent();
mountSubscriptionsComponent();
mountParticipantsComponent(mediator);
mountSubscriptionsComponent(mediator);
new SidebarMoveIssue(
mediator,
......@@ -98,7 +137,9 @@ function mount(mediator) {
$('.js-move-issue-confirmation-button'),
).init();
new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
mountTimeTrackingComponent();
}
export default mount;
export function getSidebarOptions() {
return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
}
import mountSidebarEE from 'ee/sidebar/mount_sidebar';
import Mediator from 'ee/sidebar/sidebar_mediator';
import mountSidebar from './mount_sidebar';
import Mediator from './sidebar_mediator';
import { mountSidebar, getSidebarOptions } from './mount_sidebar';
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
const mediator = new Mediator(getSidebarOptions());
mediator.fetch();
mountSidebar(mediator);
mountSidebarEE(mediator);
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
......
......@@ -7,7 +7,6 @@ export default class SidebarMediator {
if (!SidebarMediator.singleton) {
this.initSingleton(options);
}
return SidebarMediator.singleton;
}
......
export default class SidebarStore {
constructor(store) {
constructor(options) {
if (!SidebarStore.singleton) {
const { currentUser, rootPath, editable } = store;
this.initSingleton(options);
}
return SidebarStore.singleton;
}
initSingleton(options) {
const { currentUser, rootPath, editable } = options;
this.currentUser = currentUser;
this.rootPath = rootPath;
this.editable = editable;
......@@ -25,9 +32,6 @@ export default class SidebarStore {
SidebarStore.singleton = this;
}
return SidebarStore.singleton;
}
setAssigneeData(data) {
this.isFetching.assignees = false;
if (data.assignees) {
......
......@@ -25,11 +25,6 @@
type: String,
required: false,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
......@@ -139,7 +134,6 @@
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
/>
</div>
</div>
......
......@@ -9,11 +9,6 @@
type: String,
required: false,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
};
</script>
......@@ -46,10 +41,7 @@
are supported
</template>
</div>
<span
v-if="canAttachFile"
class="uploading-container"
>
<span class="uploading-container">
<span class="uploading-progress-container hide">
<i
class="fa fa-file-image-o toolbar-button-icon"
......
......@@ -58,3 +58,4 @@
@import "framework/snippets";
@import "framework/memory_graph";
@import "framework/responsive_tables";
@import "framework/stacked-progress-bar";
......@@ -463,13 +463,11 @@ ul.indent-list {
ul.group-list-tree {
li.group-row {
&.has-description {
.title {
&.has-description .title {
line-height: inherit;
}
}
.title {
&:not(.has-description) .title {
line-height: $list-text-height;
}
}
......
......@@ -50,6 +50,11 @@
&:not(.disabled) {
cursor: pointer;
}
svg {
width: $gl-padding;
height: $gl-padding;
}
}
}
......
.stacked-progress-bar {
display: flex;
height: 20px;
width: 350px;
border-radius: 10px;
overflow: hidden;
background-color: $theme-gray-100;
.status-unavailable,
.status-green,
.status-neutral,
.status-red, {
height: 100%;
font-weight: normal;
color: $white-light;
line-height: 20px;
&.has-value {
padding: 0 10px;
}
&:hover {
cursor: pointer;
}
}
.status-unavailable {
padding: 0 10px;
color: $theme-gray-700;
}
.status-green {
background-color: $green-500;
&:hover {
background-color: $green-600;
}
}
.status-neutral {
background-color: $theme-gray-200;
color: $gl-gray-dark;
&:hover {
background-color: $theme-gray-300;
}
}
.status-red {
background-color: $red-500;
&:hover {
background-color: $red-600;
}
}
}
......@@ -415,7 +415,6 @@ $location-icon-color: #e7e9ed;
* Notes
*/
$notes-light-color: $gl-text-color-secondary;
$notes-role-color: $gl-text-color-secondary;
$note-disabled-comment-color: #b2b2b2;
$note-targe3-outside: #fffff0;
$note-targe3-inside: #ffffd3;
......@@ -564,6 +563,7 @@ $jq-ui-default-color: #777;
/*
* Label
*/
$label-padding: 7px;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1);
......
.geo-admin-container {
.page-title,
.page-title-separator {
margin-top: 10px;
}
.title-text {
line-height: 34px;
}
.page-subtitle {
margin-bottom: 24px;
}
}
.geo-node-status {
td {
vertical-align: top;
}
.help-block {
width: 135px;
text-align: right;
}
.node-info {
font-weight: $gl-font-weight-bold;
}
.event-timestamp {
font-weight: normal;
color: $theme-gray-800;
}
.sync-status {
font-weight: normal;
svg {
vertical-align: middle;
}
.sync-status-icon svg,
.sync-status-timestamp {
fill: $theme-gray-700;
color: $theme-gray-700;
}
&.sync-status-failure {
.sync-status-icon svg,
.sync-status-timestamp {
fill: $red-700;
color: $red-700;
}
}
}
}
.advanced-geo-node-status-toggler {
display: block;
.show-advance-chevron {
margin-top: 2px;
}
}
.geo-node-healthy {
color: $gl-success;
}
......@@ -52,6 +117,20 @@
white-space: pre-wrap;
}
.geo-nodes {
.health-message {
padding: 1px 8px;
background-color: $red-100;
color: $red-500;
border-radius: $border-radius-default;
font-weight: 500;
}
}
.geo-node-version-mismatch {
color: $gl-danger;
}
.node-badge {
color: $white-light;
display: inline-block;
......
......@@ -262,6 +262,18 @@ table.pipeline-project-metrics tr td {
}
}
.user-access-role {
display: inline-block;
color: $gl-text-color-secondary;
font-size: 12px;
line-height: 20px;
margin: -5px 3px;
padding: 0 $label-padding;
border: 1px solid $border-color;
border-radius: $label-border-radius;
font-weight: $gl-font-weight-normal;
}
.plan-badge {
margin-right: 15px;
......
......@@ -471,7 +471,8 @@
}
}
.milestone-title span {
.milestone-title span,
.collapse-truncated-title {
@include str-truncated(100%);
display: block;
margin: 0 4px;
......
......@@ -104,7 +104,7 @@
}
.color-label {
padding: 3px 7px;
padding: 3px $label-padding;
border-radius: $label-border-radius;
}
......
......@@ -811,6 +811,10 @@
.failed {
color: $red-500;
}
.neutral {
color: $gl-gray-light;
}
}
}
}
......
......@@ -619,26 +619,17 @@ ul.notes {
}
.note-role {
margin: 0 3px;
}
.note-role-special {
position: relative;
display: inline-block;
color: $notes-role-color;
color: $gl-text-color-secondary;
font-size: 12px;
line-height: 20px;
margin: 0 3px;
&.note-role-access {
padding: 0 7px;
border: 1px solid $border-color;
border-radius: $label-border-radius;
}
&.note-role-special {
text-shadow: 0 0 15px $gl-text-color-inverted;
}
}
/**
* Line note button on the side of diffs
*/
......
module RendersMemberAccess
def prepare_groups_for_rendering(groups)
preload_max_member_access_for_collection(Group, groups)
groups
end
def prepare_projects_for_rendering(projects)
preload_max_member_access_for_collection(Project, projects)
projects
end
private
def preload_max_member_access_for_collection(klass, collection)
return if !current_user || collection.blank?
method_name = "max_member_access_for_#{klass.name.underscore}_ids"
current_user.public_send(method_name, collection.ids) # rubocop:disable GitlabSecurity/PublicSend
end
end
module UploadsActions
include Gitlab::Utils::StrongMemoize
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
......@@ -24,4 +26,25 @@ module UploadsActions
send_file uploader.file.path, disposition: disposition
end
private
def uploader
strong_memoize(:uploader) do
return if show_model.nil?
file_uploader = FileUploader.new(show_model, params[:secret])
file_uploader.retrieve_from_store!(params[:filename])
file_uploader
end
end
def image_or_video?
uploader && uploader.exists? && uploader.image_or_video?
end
def uploader_class
FileUploader
end
end
class Dashboard::ProjectsController < Dashboard::ApplicationController
include ParamsBackwardCompatibility
include RendersMemberAccess
before_action :set_non_archived_param
before_action :default_sorting
......@@ -45,10 +46,12 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def load_projects(finder_params)
ProjectsFinder
projects = ProjectsFinder
.new(params: finder_params, current_user: current_user)
.execute
.includes(:route, :creator, namespace: [:route, :owner])
prepare_projects_for_rendering(projects)
end
def load_events
......
class Explore::ProjectsController < Explore::ApplicationController
include ParamsBackwardCompatibility
include RendersMemberAccess
before_action :set_non_archived_param
......@@ -49,10 +50,12 @@ class Explore::ProjectsController < Explore::ApplicationController
private
def load_projects
ProjectsFinder.new(current_user: current_user, params: params)
projects = ProjectsFinder.new(current_user: current_user, params: params)
.execute
.includes(:route, namespace: :route)
.page(params[:page])
.without_count
prepare_projects_for_rendering(projects)
end
end
class Groups::UploadsController < Groups::ApplicationController
include UploadsActions
skip_before_action :group, if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create]
private
def show_model
strong_memoize(:show_model) do
group_id = params[:group_id]
Group.find_by_full_path(group_id)
end
end
def authorize_upload_file!
render_404 unless can?(current_user, :upload_file, group)
end
def uploader
strong_memoize(:uploader) do
file_uploader = uploader_class.new(show_model, params[:secret])
file_uploader.retrieve_from_store!(params[:filename])
file_uploader
end
end
def uploader_class
NamespaceFileUploader
end
alias_method :model, :group
end
......@@ -8,31 +8,13 @@ class Projects::UploadsController < Projects::ApplicationController
private
def uploader
return @uploader if defined?(@uploader)
def show_model
strong_memoize(:show_model) do
namespace = params[:namespace_id]
id = params[:project_id]
file_project = Project.find_by_full_path("#{namespace}/#{id}")
if file_project.nil?
@uploader = nil
return
end
@uploader = FileUploader.new(file_project, params[:secret])
@uploader.retrieve_from_store!(params[:filename])
@uploader
end
def image_or_video?
uploader && uploader.exists? && uploader.image_or_video?
Project.find_by_full_path("#{namespace}/#{id}")
end
def uploader_class
FileUploader
end
alias_method :model, :project
......
class UsersController < ApplicationController
include RoutableActions
include RendersMemberAccess
skip_before_action :authenticate_user!
before_action :user, except: [:exists]
......@@ -116,14 +117,20 @@ class UsersController < ApplicationController
@projects =
PersonalProjectsFinder.new(user).execute(current_user)
.page(params[:page])
prepare_projects_for_rendering(@projects)
end
def load_contributed_projects
@contributed_projects = contributed_projects.joined(user)
prepare_projects_for_rendering(@contributed_projects)
end
def load_groups
@groups = JoinedGroupsFinder.new(user).execute(current_user)
prepare_groups_for_rendering(@groups)
end
def load_snippets
......
......@@ -20,8 +20,7 @@ module BuildsHelper
def javascript_build_options
{
page_url: project_job_url(@project, @build),
build_url: project_job_url(@project, @build, :json),
page_path: project_job_path(@project, @build),
build_status: @build.status,
build_stage: @build.stage,
log_state: ''
......
......@@ -86,6 +86,8 @@ module MarkupHelper
return '' unless text.present?
context[:project] ||= @project
context[:group] ||= @group
html = markdown_unsafe(text, context)
prepare_for_rendering(html, context)
end
......
......@@ -253,7 +253,7 @@ module Ci
variables += pipeline.predefined_variables
variables += runner.predefined_variables if runner
variables += project.container_registry_variables
variables += project.deployment_variables if has_environment?
variables += project.deployment_variables(environment: environment) if has_environment?
variables += project.auto_devops_variables
variables += yaml_variables
variables += user_variables
......
module Ci
class JobArtifact < ActiveRecord::Base
include AfterCommitQueue
extend Gitlab::Ci::Model
belongs_to :project
......@@ -9,6 +10,12 @@ module Ci
mount_uploader :file, JobArtifactUploader
after_save if: :file_changed?, on: [:create, :update] do
run_after_commit do
file.schedule_migration_to_object_storage
end
end
enum file_type: {
archive: 1,
metadata: 2
......
......@@ -3,7 +3,7 @@ module Ci
extend Gitlab::Ci::Model
include HasVariable
include Presentable
prepend EE::Ci::Variable
prepend HasEnvironmentScope
belongs_to :project
......
module Clusters
class Cluster < ActiveRecord::Base
include Presentable
prepend HasEnvironmentScope
self.table_name = 'clusters'
......@@ -26,6 +27,7 @@ module Clusters
accepts_nested_attributes_for :platform_kubernetes, update_only: true
validates :name, cluster_name: true
validate :unique_environment_scope, if: :has_project?
validate :restrict_modification, on: :update
delegate :status, to: :provider, allow_nil: true
......@@ -91,6 +93,15 @@ module Clusters
private
def unique_environment_scope
if project.clusters.where(environment_scope: environment_scope).where.not(id: self.id).exists?
errors.add(:base, "cannot add duplicated environment scope")
return false
end
true
end
def restrict_modification
if provider&.on_creation?
errors.add(:base, "cannot modify during creation")
......@@ -99,5 +110,9 @@ module Clusters
true
end
def has_project?
projects.exists?
end
end
end
# Returns and caches in thread max member access for a resource
#
module BulkMemberAccessLoad
extend ActiveSupport::Concern
included do
# Determine the maximum access level for a group of resources in bulk.
#
# Returns a Hash mapping resource ID -> maximum access level.
def max_member_access_for_resource_ids(resource_klass, resource_ids, memoization_index = self.id, &block)
raise 'Block is mandatory' unless block_given?
resource_ids = resource_ids.uniq
key = max_member_access_for_resource_key(resource_klass, memoization_index)
access = {}
if RequestStore.active?
RequestStore.store[key] ||= {}
access = RequestStore.store[key]
end
# Look up only the IDs we need
resource_ids = resource_ids - access.keys
return access if resource_ids.empty?
resource_access = yield(resource_ids)
access.merge!(resource_access)
missing_resource_ids = resource_ids - resource_access.keys
missing_resource_ids.each do |resource_id|
access[resource_id] = Gitlab::Access::NO_ACCESS
end
access
end
private
def max_member_access_for_resource_key(klass, memoization_index)
"max_member_access_for_#{klass.name.underscore.pluralize}:#{memoization_index}"
end
end
end
......@@ -61,7 +61,7 @@ module Mentionable
cache_key: [self, attr],
author: author,
skip_project_check: skip_project_check?
)
).merge(mentionable_params)
extractor.analyze(text, options)
end
......@@ -82,7 +82,7 @@ module Mentionable
return [] unless matches_cross_reference_regex?
refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits)
refs = (refs.issues + refs.merge_requests + refs.commits + refs.epics)
# We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the
......@@ -157,4 +157,8 @@ module Mentionable
def skip_project_check?
false
end
def mentionable_params
{}
end
end
......@@ -3,6 +3,7 @@ module Mentionable
def self.reference_pattern(link_patterns, issue_pattern)
Regexp.union(link_patterns,
issue_pattern,
Epic.reference_pattern,
Commit.reference_pattern,
MergeRequest.reference_pattern)
end
......
......@@ -137,16 +137,20 @@ class Environment < ActiveRecord::Base
end
end
def deployment_platform
project.deployment_platform(environment: self)
end
def has_terminals?
project.deployment_platform.present? && available? && last_deployment.present?
deployment_platform.present? && available? && last_deployment.present?
end
def terminals
project.deployment_platform.terminals(self) if has_terminals?
deployment_platform.terminals(self) if has_terminals?
end
def rollout_status
project.deployment_platform.rollout_status(self) if has_terminals?
deployment_platform.rollout_status(self) if has_terminals?
end
def has_metrics?
......
# Placeholder class for model that is implemented in EE
# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE
# It reserves '&' as a reference prefix, but the table does not exists in CE
class Epic < ActiveRecord::Base
prepend EE::Epic
# TODO: this will be implemented as part of #3853
def to_reference
def self.reference_prefix
'&'
end
def self.reference_prefix_escaped
'&amp;'
end
end
......@@ -120,104 +120,22 @@ class GeoNode < ActiveRecord::Base
end
end
def projects_include?(project_id)
return true if restricted_project_ids.nil?
restricted_project_ids.include?(project_id)
end
def restricted_project_ids
return unless namespaces.presence
relations = namespaces.map { |namespace| namespace.all_projects.select(:id) }
Project.unscoped
.from("(#{Gitlab::SQL::Union.new(relations).to_sql}) #{Project.table_name}")
.pluck(:id)
end
def lfs_objects
relation =
if restricted_project_ids
LfsObject.joins(:projects).where(projects: { id: restricted_project_ids })
else
LfsObject.all
end
relation.with_files_stored_locally
end
def projects
if restricted_project_ids
Project.where(id: restricted_project_ids)
if selective_sync?
Project.where(namespace_id: Gitlab::GroupHierarchy.new(namespaces).base_and_descendants.select(:id))
else
Project.all
end
end
def project_registries
if restricted_project_ids
Geo::ProjectRegistry.where(project_id: restricted_project_ids)
else
Geo::ProjectRegistry.all
end
end
def filtered_project_registries(type = nil)
case type
when 'repository'
project_registries.failed_repos
when 'wiki'
project_registries.failed_wikis
else
project_registries.failed
end
end
def uploads
if restricted_project_ids
uploads_table = Upload.arel_table
group_uploads = uploads_table[:model_type].eq('Namespace').and(uploads_table[:model_id].in(Gitlab::Geo.current_node.namespace_ids))
project_uploads = uploads_table[:model_type].eq('Project').and(uploads_table[:model_id].in(restricted_project_ids))
other_uploads = uploads_table[:model_type].not_in(%w[Namespace Project])
Upload.where(group_uploads.or(project_uploads).or(other_uploads))
else
Upload.all
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)
def projects_include?(project_id)
return true unless selective_sync?
(synced_ids & upload_ids).length
projects.where(id: project_id).exists?
end
def attachments_failed_count
return unless secondary?
Geo::FileRegistry.attachments.failed.count
def selective_sync?
namespaces.exists?
end
def find_or_build_status
......
......@@ -2,7 +2,7 @@ class GeoNodeStatus < ActiveRecord::Base
belongs_to :geo_node
# Whether we were successful in reaching this node
attr_accessor :success
attr_accessor :success, :health_status, :version, :revision
# Be sure to keep this consistent with Prometheus naming conventions
PROMETHEUS_METRICS = {
......@@ -26,7 +26,6 @@ class GeoNodeStatus < ActiveRecord::Base
def self.current_node_status
current_node = Gitlab::Geo.current_node
return unless current_node
status = current_node.find_or_build_status
......@@ -47,8 +46,8 @@ class GeoNodeStatus < ActiveRecord::Base
end
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
excluded_params = %w(id created_at updated_at).freeze
extra_params = %w(success health health_status last_event_timestamp cursor_last_event_timestamp version revision).freeze
self.column_names - excluded_params + extra_params
end
......@@ -63,21 +62,21 @@ class GeoNodeStatus < ActiveRecord::Base
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.repositories_count = projects_finder.count_projects
self.lfs_objects_count = lfs_objects_finder.count_lfs_objects
self.attachments_count = attachments_finder.count_attachments
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
self.repositories_synced_count = projects_finder.count_synced_project_registries
self.repositories_failed_count = projects_finder.count_failed_project_registries
self.lfs_objects_synced_count = lfs_objects_finder.count_synced_lfs_objects
self.lfs_objects_failed_count = lfs_objects_finder.count_failed_lfs_objects
self.attachments_synced_count = attachments_finder.count_synced_attachments
self.attachments_failed_count = attachments_finder.count_failed_attachments
end
self
......@@ -89,6 +88,10 @@ class GeoNodeStatus < ActiveRecord::Base
status_message.blank? || status_message == 'Healthy'.freeze
end
def health_status
@health_status || (healthy? ? 'Healthy' : 'Unhealthy')
end
def last_successful_status_check_timestamp
self.last_successful_status_check_at.to_i
end
......@@ -131,6 +134,18 @@ class GeoNodeStatus < ActiveRecord::Base
private
def attachments_finder
@attachments_finder ||= Geo::AttachmentRegistryFinder.new(current_node: geo_node)
end
def lfs_objects_finder
@lfs_objects_finder ||= Geo::LfsObjectRegistryFinder.new(current_node: geo_node)
end
def projects_finder
@projects_finder ||= Geo::ProjectRegistryFinder.new(current_node: geo_node)
end
def sync_percentage(total, synced)
return 0 if !total.present? || total.zero?
......
......@@ -344,6 +344,10 @@ class Group < Namespace
end
end
def hashed_storage?(_feature)
false
end
private
def update_two_factor_requirement
......
class LfsObject < ActiveRecord::Base
prepend EE::LfsObject
include AfterCommitQueue
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :lfs_objects_projects
......@@ -10,6 +11,12 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
after_save if: :file_changed?, on: [:create, :update] do
run_after_commit do
file.schedule_migration_to_object_storage
end
end
def project_allowed_access?(project)
projects.exists?(project.lfs_storage_project.id)
end
......
......@@ -41,6 +41,7 @@ class Namespace < ActiveRecord::Base
namespace_path: true
validate :nesting_level_allowed
validate :allowed_path_by_redirects
delegate :name, to: :owner, allow_nil: true, prefix: true
......@@ -270,4 +271,14 @@ class Namespace < ActiveRecord::Base
Namespace.where(id: descendants.select(:id))
.update_all(share_with_group_lock: true)
end
def allowed_path_by_redirects
return if path.nil?
errors.add(:path, "#{path} has been taken before. Please use another one") if namespace_previously_created_with_same_path?
end
def namespace_previously_created_with_same_path?
RedirectRoute.permanent.exists?(path: path)
end
end
......@@ -232,7 +232,6 @@ class Project < ActiveRecord::Base
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
delegate :empty_repo?, to: :repository
# Validations
validates :creator, presence: true, on: :create
......@@ -507,6 +506,10 @@ class Project < ActiveRecord::Base
auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
end
def empty_repo?
repository.empty?
end
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
end
......@@ -906,8 +909,7 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
# TODO: This will be extended for multiple enviroment clusters
def deployment_platform
def deployment_platform(environment: nil)
@deployment_platform ||= clusters.find_by(enabled: true)&.platform_kubernetes
@deployment_platform ||= services.where(category: :deployment).reorder(nil).find_by(active: true)
end
......@@ -1558,10 +1560,8 @@ class Project < ActiveRecord::Base
ProtectedTag.protected?(self, ref)
end
def deployment_variables
return [] unless deployment_platform
deployment_platform.predefined_variables
def deployment_variables(environment: nil)
deployment_platform(environment: environment)&.predefined_variables || []
end
def auto_devops_variables
......
class ProjectTeam
include BulkMemberAccessLoad
attr_accessor :project
def initialize(project)
......@@ -159,39 +161,16 @@ class ProjectTeam
#
# Returns a Hash mapping user ID -> maximum access level.
def max_member_access_for_user_ids(user_ids)
user_ids = user_ids.uniq
key = "max_member_access:#{project.id}"
access = {}
if RequestStore.active?
RequestStore.store[key] ||= {}
access = RequestStore.store[key]
end
# Look up only the IDs we need
user_ids = user_ids - access.keys
return access if user_ids.empty?
users_access = project.project_authorizations
max_member_access_for_resource_ids(User, user_ids, project.id) do |user_ids|
project.project_authorizations
.where(user: user_ids)
.group(:user_id)
.maximum(:access_level)
access.merge!(users_access)
missing_user_ids = user_ids - users_access.keys
missing_user_ids.each do |user_id|
access[user_id] = Gitlab::Access::NO_ACCESS
end
access
end
def max_member_access(user_id)
max_member_access_for_user_ids([user_id])[user_id] || Gitlab::Access::NO_ACCESS
max_member_access_for_user_ids([user_id])[user_id]
end
private
......
......@@ -17,4 +17,32 @@ class RedirectRoute < ActiveRecord::Base
where(wheres, path, "#{sanitize_sql_like(path)}/%")
end
scope :permanent, -> do
if column_permanent_exists?
where(permanent: true)
else
none
end
end
scope :temporary, -> do
if column_permanent_exists?
where(permanent: [false, nil])
else
all
end
end
default_value_for :permanent, false
def permanent=(value)
if self.class.column_permanent_exists?
super
end
end
def self.column_permanent_exists?
ActiveRecord::Base.connection.column_exists?(:redirect_routes, :permanent)
end
end
......@@ -44,7 +44,7 @@ class Repository
issue_template_names merge_request_template_names).freeze
# Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze
MEMOIZED_CACHED_METHODS = %i(license).freeze
# Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
......@@ -504,7 +504,11 @@ class Repository
end
cache_method :exists?
delegate :empty?, to: :raw_repository
def empty?
return true unless exists?
!has_visible_content?
end
cache_method :empty?
# The size of this repository in megabytes.
......@@ -993,13 +997,8 @@ class Repository
end
end
def empty_repo?
!exists? || !has_visible_content?
end
cache_method :empty_repo?, memoize_only: true
def search_files_by_content(query, ref)
return [] if empty_repo? || query.blank?
return [] if empty? || query.blank?
offset = 2
args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
......@@ -1008,7 +1007,7 @@ class Repository
end
def search_files_by_name(query, ref)
return [] if empty_repo? || query.blank?
return [] if empty? || query.blank?
args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
......
......@@ -8,6 +8,8 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
validate :ensure_permanent_paths
after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path
......@@ -40,7 +42,7 @@ class Route < ActiveRecord::Base
# We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already
# called it, which deletes conflicts for all descendants.
route.create_redirect(old_path) if attributes[:path]
route.create_redirect(old_path, permanent: permanent_redirect?) if attributes[:path]
end
end
end
......@@ -50,16 +52,30 @@ class Route < ActiveRecord::Base
end
def conflicting_redirects
RedirectRoute.matching_path_and_descendants(path)
RedirectRoute.temporary.matching_path_and_descendants(path)
end
def create_redirect(path)
RedirectRoute.create(source: source, path: path)
def create_redirect(path, permanent: false)
RedirectRoute.create(source: source, path: path, permanent: permanent)
end
private
def create_redirect_for_old_path
create_redirect(path_was) if path_changed?
create_redirect(path_was, permanent: permanent_redirect?) if path_changed?
end
def permanent_redirect?
source_type != "Project"
end
def ensure_permanent_paths
return if path.nil?
errors.add(:path, "#{path} has been taken before. Please use another one") if conflicting_redirect_exists?
end
def conflicting_redirect_exists?
RedirectRoute.permanent.matching_path_and_descendants(path).exists?
end
end
......@@ -16,6 +16,7 @@ class SystemNoteMetadata < ActiveRecord::Base
opened closed merged duplicate locked unlocked
outdated
approved unapproved relate unrelate
epic_issue_added issue_added_to_epic epic_issue_removed issue_removed_from_epic
].freeze
validates :note, presence: true
......
......@@ -17,6 +17,7 @@ class User < ActiveRecord::Base
include FeatureGate
include CreatedAtFilterable
include IgnorableColumn
include BulkMemberAccessLoad
prepend EE::GeoAwareAvatar
prepend EE::User
......@@ -1167,6 +1168,34 @@ class User < ActiveRecord::Base
super
end
# Determine the maximum access level for a group of projects in bulk.
#
# Returns a Hash mapping project ID -> maximum access level.
def max_member_access_for_project_ids(project_ids)
max_member_access_for_resource_ids(Project, project_ids) do |project_ids|
project_authorizations.where(project: project_ids)
.group(:project_id)
.maximum(:access_level)
end
end
def max_member_access_for_project(project_id)
max_member_access_for_project_ids([project_id])[project_id]
end
# Determine the maximum access level for a group of groups in bulk.
#
# Returns a Hash mapping project ID -> maximum access level.
def max_member_access_for_group_ids(group_ids)
max_member_access_for_resource_ids(Group, group_ids) do |group_ids|
group_members.where(source: group_ids).group(:source_id).maximum(:access_level)
end
end
def max_member_access_for_group(group_id)
max_member_access_for_group_ids([group_id])[group_id]
end
protected
# override, from Devise::Validatable
......
......@@ -40,6 +40,7 @@ class GroupPolicy < BasePolicy
rule { guest }.policy do
enable :read_group
enable :read_list
enable :upload_file
end
rule { admin } .enable :read_group
......
......@@ -7,6 +7,7 @@ class GeoNodeStatusEntity < Grape::Entity
expose :health do |node|
node.healthy? ? 'Healthy' : node.health
end
expose :health_status
expose :attachments_count
expose :attachments_synced_count
......@@ -37,4 +38,23 @@ class GeoNodeStatusEntity < Grape::Entity
expose :cursor_last_event_timestamp
expose :last_successful_status_check_timestamp
expose :version
expose :revision
expose :namespaces, using: NamespaceEntity
private
def namespaces
object.geo_node.namespaces
end
def version
Gitlab::VERSION
end
def revision
Gitlab::REVISION
end
end
class IssueSidebarEntity < IssuableSidebarEntity
prepend ::EE::IssueSidebarEntity
expose :assignees, using: API::Entities::UserBasic
end
class NamespaceEntity < Grape::Entity
expose :id, :name, :path, :kind, :full_path, :parent_id
end
......@@ -14,7 +14,8 @@ module Ci
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false, &block)
@pipeline = Ci::Pipeline.new
command = OpenStruct.new(source: source,
command = Gitlab::Ci::Pipeline::Chain::Command.new(
source: source,
origin_ref: params[:ref],
checkout_sha: params[:checkout_sha],
after_sha: params[:after],
......@@ -23,10 +24,13 @@ module Ci
schedule: schedule,
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
allow_mirror_update: mirror_update,
seeds_block: block,
project: project,
current_user: current_user)
current_user: current_user,
# EE specific
allow_mirror_update: mirror_update
)
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE)
......
......@@ -29,7 +29,13 @@ module Clusters
end
def can_create_cluster?
<<<<<<< HEAD
project.clusters.empty?
=======
return true if project.clusters.empty?
project.feature_available?(:multiple_clusters)
>>>>>>> origin/master
end
end
end
......@@ -2,7 +2,7 @@ module Geo
class FileService
attr_reader :object_type, :object_db_id
DEFAULT_OBJECT_TYPES = %w[attachment avatar file personal_file].freeze
DEFAULT_OBJECT_TYPES = %w[attachment avatar file namespace_file personal_file].freeze
DEFAULT_SERVICE_TYPE = 'file'.freeze
def initialize(object_type, object_db_id)
......
......@@ -6,7 +6,8 @@ module Geo
def call(geo_node)
return GeoNodeStatus.current_node_status if geo_node.current?
data = { success: false }
data = GeoNodeStatus.find_or_initialize_by(geo_node: geo_node).attributes
data = data.merge(success: false, health_status: 'Offline')
begin
response = self.class.get(geo_node.status_url, headers: headers, timeout: timeout)
......@@ -29,8 +30,10 @@ module Geo
end
rescue Gitlab::Geo::GeoNodeNotFoundError
data[:health] = 'This GitLab instance does not appear to be configured properly as a Geo node. Make sure the URLs are using the correct fully-qualified domain names.'
data[:health_status] = 'Unhealthy'
rescue OpenSSL::Cipher::CipherError
data[:health] = 'Error decrypting the Geo secret from the database. Check that the primary uses the correct db_key_base.'
data[:health_status] = 'Unhealthy'
rescue HTTParty::Error, Timeout::Error, SocketError, SystemCallError, OpenSSL::SSL::SSLError => e
data[:health] = e.message
end
......
module Projects
class ForkService < BaseService
def execute
def execute(fork_to_project = nil)
if fork_to_project
link_existing_project(fork_to_project)
else
fork_new_project
end
end
private
def link_existing_project(fork_to_project)
return if fork_to_project.forked?
link_fork_network(fork_to_project)
fork_to_project
end
def fork_new_project
new_params = {
forked_from_project_id: @project.id,
visibility_level: allowed_visibility_level,
......@@ -21,15 +39,11 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
refresh_forks_count
link_fork_network(new_project)
new_project
end
private
def fork_network
if @project.fork_network
@project.fork_network
......@@ -43,9 +57,17 @@ module Projects
end
end
def link_fork_network(new_project)
fork_network.fork_network_members.create(project: new_project,
def link_fork_network(fork_to_project)
fork_network.fork_network_members.create(project: fork_to_project,
forked_from_project: @project)
# TODO: remove this when ForkedProjectLink model is removed
unless fork_to_project.forked_project_link
fork_to_project.create_forked_project_link(forked_to_project: fork_to_project,
forked_from_project: @project)
end
refresh_forks_count
end
def refresh_forks_count
......
module ProtectedBranches
class AccessLevelParams
prepend EE::ProtectedBranches::AccessLevelParams
attr_reader :type, :params
def initialize(type, params)
@type = type
@params = params_with_default(params)
end
def access_levels
ce_style_access_level
end
private
def params_with_default(params)
params[:"#{type}_access_level"] ||= Gitlab::Access::MASTER if use_default_access_level?(params)
params
end
def use_default_access_level?(params)
true
end
def ce_style_access_level
access_level = params[:"#{type}_access_level"]
return [] unless access_level
[{ access_level: access_level }]
end
end
end
module ProtectedBranches
class ApiService < BaseService
prepend EE::ProtectedBranches::ApiService
def create
@push_params = AccessLevelParams.new(:push, params)
@merge_params = AccessLevelParams.new(:merge, params)
verify_params!
protected_branch_params = {
name: params[:name],
push_access_levels_attributes: @push_params.access_levels,
merge_access_levels_attributes: @merge_params.access_levels
}
::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute
end
private
def verify_params!
# EE-only
end
end
end
......@@ -566,6 +566,36 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'unrelate'))
end
def epic_issue(epic, issue, user, type)
return unless validate_epic_issue_action_type(type)
action = type == :added ? 'epic_issue_added' : 'epic_issue_removed'
body = "#{type} issue #{issue.to_reference(epic.group)}"
create_note(NoteSummary.new(epic, nil, user, body, action: action))
end
def issue_on_epic(issue, epic, user, type)
return unless validate_epic_issue_action_type(type)
if type == :added
direction = 'to'
action = 'issue_added_to_epic'
else
direction = 'from'
action = 'issue_removed_from_epic'
end
body = "#{type} #{direction} epic #{epic.to_reference(issue.project)}"
create_note(NoteSummary.new(issue, issue.project, user, body, action: action))
end
def validate_epic_issue_action_type(type)
[:added, :removed].include?(type)
end
# Called when the merge request is approved by user
#
# noteable - Noteable object
......
......@@ -29,11 +29,11 @@ class FileUploader < GitlabUploader
# model - Object that responds to `full_path` and `disk_path`
#
# Returns a String without a trailing slash
def self.dynamic_path_segment(project)
if project.hashed_storage?(:attachments)
dynamic_path_builder(project.disk_path)
def self.dynamic_path_segment(model)
if model.hashed_storage?(:attachments)
dynamic_path_builder(model.disk_path)
else
dynamic_path_builder(project.full_path)
dynamic_path_builder(model.full_path)
end
end
......
class LfsObjectUploader < ObjectStoreUploader
storage_options Gitlab.config.lfs
after :store, :schedule_migration_to_object_storage
def self.local_store_path
Gitlab.config.lfs.storage_path
......
class NamespaceFileUploader < FileUploader
def self.base_dir
File.join(root_dir, '-', 'system', 'namespace')
end
def self.dynamic_path_segment(model)
dynamic_path_builder(model.id.to_s)
end
private
def secure_url
File.join('/uploads', @secret, file.filename)
end
end
......@@ -128,7 +128,6 @@
- if License.feature_available?(:repository_mirrors)
= render partial: 'repository_mirrors_form', locals: { f: f }
= render partial: 'repository_remote_mirrors_form', locals: { f: f }
%fieldset
%legend Sign-up Restrictions
......
- page_title 'Geo nodes'
%h3.page-title
- @content_class = "geo-admin-container"
%h2.page-title.clearfix
%span.title-text.pull-left
Geo Nodes
= link_to "New node", new_admin_geo_node_path, class: 'btn btn-create pull-right'
%hr.page-title-separator
%p.light
%p.page-subtitle.light
With #{link_to 'GitLab Geo', help_page_path('gitlab-geo/README'), class: 'vlink'} you can install a special
read-only and replicated instance anywhere.
Before you add nodes, follow the
......@@ -11,16 +15,6 @@
%strong exact order
they appear.
%hr
- if Gitlab::Geo.license_allows?
= form_for [:admin, @node], as: :geo_node, url: admin_geo_nodes_path, html: { class: 'form-horizontal js-geo-node-form' } do |f|
= render partial: 'form', locals: { form: f, geo_node: @node }
.form-actions
= f.submit 'Add Node', class: 'btn btn-create'
%hr
- if @nodes.any?
.panel.panel-default
.panel-heading
......@@ -35,62 +29,100 @@
.node-badge.current-node Current node
- if node.primary?
.node-badge.primary-node Primary
%p
%span.help-block Primary node
- else
= status_loading_icon
- if node.restricted_project_ids
%p
%span.help-block
Namespaces to replicate:
%strong.node-info
= node_selected_namespaces_to_replicate(node)
.js-geo-node-status{ style: 'display: none' }
GitLab version:
%span.js-primary-version= Gitlab::VERSION
%small.js-primary-revision
= "(#{Gitlab::REVISION})"
- else
= status_loading_icon
%table.geo-node-status.js-geo-node-status.hidden
%tr
%td
.help-block
GitLab version:
%td
.node-info.prepend-top-5.prepend-left-5.js-secondary-version
- if node.enabled?
%p
%span.help-block
%tr
%td
.help-block.prepend-top-10
Health Status:
%span.js-health-status
%p
%span.help-block
Repositories synced:
%strong.node-info.js-repositories-synced
%p
%span.help-block
Repositories failed:
%strong.node-info.js-repositories-failed
%p
%span.help-block
LFS objects synced:
%strong.node-info.js-lfs-objects-synced
%p
%span.help-block
LFS objects failed:
%strong.node-info.js-lfs-objects-failed
%p
%span.help-block
Attachments synced:
%strong.node-info.js-attachments-synced
%p
%span.help-block
Attachments failed:
%strong.node-info.js-attachments-failed
%p
.advanced-geo-node-status-container
.advanced-status.hidden
%span.help-block
%td
.health-status.prepend-top-10.prepend-left-5.js-health-status
%tr
%td
.help-block.prepend-top-10
Repositories:
%td
.node-info.prepend-top-10.prepend-left-5.stacked-progress-bar.js-repositories
%span.status-unavailable.js-stats-unavailable
Not available
%span.status-green.js-synced
%span.status-neutral.js-waiting
%span.status-red.js-failed
%tr
%td
.help-block.prepend-top-10
LFS objects:
%td
.node-info.prepend-top-10.prepend-left-5.stacked-progress-bar.js-lfs-objects
%span.status-unavailable.js-stats-unavailable
Not available
%span.status-green.js-synced
%span.status-neutral.js-waiting
%span.status-red.js-failed
%tr
%td
.help-block.prepend-top-10
Attachments:
%td
.node-info.prepend-top-10.prepend-left-5.stacked-progress-bar.js-attachments
%span.status-unavailable.js-stats-unavailable
Not available
%span.status-green.js-synced
%span.status-neutral.js-waiting
%span.status-red.js-failed
%tr
%td
.help-block.prepend-top-10
Sync settings:
%td
.node-info.prepend-top-10.prepend-left-5.js-sync-settings
%span.js-sync-type
%span.has-tooltip.sync-status.js-sync-status
%i.sync-status-icon.js-sync-status-icon
%span.sync-status-timestamp.js-sync-status-timestamp
%tr.js-advanced-status.hidden
%td
.help-block.prepend-top-10
Database replication lag:
%strong.node-info.js-db-replication-lag
%span.help-block
%td
.node-info.prepend-top-10.prepend-left-5.js-db-replication-lag
%tr.js-advanced-status.hidden
%td
.help-block.prepend-top-10
Last event ID seen from primary:
%strong.node-info.js-last-event-seen
%span.help-block
%td
.node-info.prepend-top-10.prepend-left-5.js-last-event-seen
%span.js-event-id
%span.event-timestamp.js-event-timestamp.has-tooltip
%tr.js-advanced-status.hidden
%td
.help-block.prepend-top-10
Last event ID processed by cursor:
%strong.node-info.js-last-cursor-event
%button.btn-link.js-advanced-geo-node-status-toggler
%td
.node-info.prepend-top-10.prepend-left-5.js-last-cursor-event
%span.js-event-id
%span.event-timestamp.js-event-timestamp.has-tooltip
%button.btn-link.advanced-geo-node-status-toggler.js-advanced-geo-node-status-toggler
%span> Advanced
= icon('angle-down')
%p
.js-health
%span.js-advance-toggle.show-advance-chevron.pull-right.inline.prepend-left-5
= sprite_icon('angle-down', css_class: 's16')
%p.health-message.hidden.js-health-message
- if Gitlab::Database.read_write?
.node-actions
......
- page_title 'New node'
- @content_class = "geo-admin-container"
%h2.page-title
%span.title-text
New node
%hr.page-title-separator
- if Gitlab::Geo.license_allows?
= form_for [:admin, @node], as: :geo_node, url: admin_geo_nodes_path, html: { class: 'form-horizontal js-geo-node-form' } do |f|
= render partial: 'form', locals: { form: f, geo_node: @node }
.form-actions
= f.submit 'Add Node', class: 'btn btn-create'
= render 'shared/projects/list', projects: @projects, ci: true
= render 'shared/projects/list', projects: @projects, ci: true, user: current_user
= render 'shared/projects/list', projects: projects
= render 'shared/projects/list', projects: projects, user: current_user
......@@ -13,7 +13,15 @@
.location-badge= label
.search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } }
= search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' }
= search_field_tag 'search', nil, placeholder: 'Search',
class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options',
spellcheck: false,
tabindex: '1',
autocomplete: 'off',
data: { toggle: 'dropdown',
issues_path: issues_dashboard_path,
mr_path: merge_requests_dashboard_path },
aria: { label: 'Search' }
.dropdown-menu.dropdown-select
= dropdown_content do
%ul
......
......@@ -4,4 +4,10 @@
- nav "group"
- @left_sidebar = true
- content_for :page_specific_javascripts do
- if current_user
-# haml-lint:disable InlineJavaScript
:javascript
window.uploads_path = "#{group_uploads_path(@group)}";
= render template: "layouts/application"
......@@ -14,7 +14,7 @@
%td.branch-commit
- if can?(current_user, :read_build, job)
= link_to project_job_url(job.project, job) do
= link_to project_job_path(job.project, job) do
%span.build-link ##{job.id}
- else
%span.build-link ##{job.id}
......
......@@ -14,3 +14,11 @@
= link_to project_clusters_path(@project), class: "js-all-tab" do
= s_("ClusterIntegration|All")
%span.badge= @all_count
<<<<<<< HEAD
=======
.nav-controls
- if @project.feature_available?(:multiple_clusters)
= link_to s_("ClusterIntegration|Add cluster"), new_project_cluster_path(@project), class: "btn btn-success btn-add-cluster has-tooltip js-add-cluster"
- else
= link_to s_("ClusterIntegration|Add cluster"), new_project_cluster_path(@project), class: "btn btn-success btn-add-cluster disabled has-tooltip js-add-cluster", title: s_("ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate")
>>>>>>> origin/master
......@@ -3,7 +3,7 @@
- page_title _("Commits"), @ref
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
= auto_discovery_link_tag(:atom, project_commits_path(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
.js-project-commits-show{ 'data-commits-limit' => @limit }
%div{ class: container_class }
......
......@@ -14,4 +14,4 @@
notes_path: notes_url,
last_fetched_at: Time.now.to_i,
noteable_data: serialize_issuable(@issue),
current_user_data: UserSerializer.new.represent(current_user).to_json } }
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
......@@ -87,10 +87,11 @@
%h3.panel-title
Related issues
#merge-requests{ data: { url: referenced_merge_requests_project_issue_url(@project, @issue) } }
#merge-requests{ data: { url: referenced_merge_requests_project_issue_path(@project, @issue) } }
// This element is filled in using JavaScript.
#related-branches{ data: { url: related_branches_project_issue_url(@project, @issue) } }
#related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
// This element is filled in using JavaScript.
.content-block.emoji-block
......
......@@ -85,7 +85,7 @@
= icon('bug', text: 'Fogbugz')
%div
- if gitea_import_enabled?
= link_to new_import_gitea_url, class: 'btn import_gitea' do
= link_to new_import_gitea_path, class: 'btn import_gitea' do
= custom_icon('go_logo')
Gitea
%div
......
......@@ -3,7 +3,7 @@
%span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") }
= issuable_first_contribution_icon
- if access.nonzero?
%span.note-role.note-role-access= Gitlab::Access.human_access(access)
%span.note-role.user-access-role= Gitlab::Access.human_access(access)
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
......
......@@ -3,5 +3,5 @@
GitLab may not work properly because you are using an outdated web browser.
%br
Please install a
= link_to 'supported web browser', help_page_url('install/requirements', anchor: 'supported-web-browsers')
= link_to 'supported web browser', help_page_path('install/requirements', anchor: 'supported-web-browsers')
for a better experience.
- group_member = local_assigns[:group_member]
- full_name = true unless local_assigns[:full_name] == false
- group_name = full_name ? group.full_name : group.name
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if group.description.blank?
%li.group-row{ class: css_class }
- if group_member
.controls.hidden-xs
- if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn" do
= sprite_icon('settings')
= link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: s_("GroupsTree|Leave this group") do
= icon('sign-out')
- user = local_assigns.fetch(:user, current_user)
- access = user&.max_member_access_for_group(group.id)
%li.group-row{ class: ('no-description' if group.description.blank?) }
.stats
%span
= icon('bookmark')
......@@ -30,11 +18,10 @@
= link_to group do
= group_icon(group, class: "avatar s40 hidden-xs")
.title
= link_to group_name, group, class: 'group-name'
= link_to group.full_name, group, class: 'group-name'
- if group_member
as
%span= group_member.human_access
- if access&.nonzero?
%span.user-access-role= Gitlab::Access.human_access(access)
- if group.description.present?
.description
......
- if groups.any?
- user = local_assigns[:user]
%ul.content-list
- groups.each_with_index do |group, i|
= render "shared/groups/group", group: group
= render "shared/groups/group", group: group, user: user
- else
.nothing-here-block= s_("GroupsEmptyState|No groups found")
- todo = issuable_todo(issuable)
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sidebar')
= page_specific_javascript_bundle_tag('ee_sidebar')
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
......@@ -21,6 +21,7 @@
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true
.block.assignee
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
= render "shared/issuable/sidebar_item_epic", issuable: issuable
.block.milestone
.sidebar-collapsed-icon
= icon('clock-o', 'aria-hidden': 'true')
......
......@@ -5,18 +5,20 @@
- forks = false unless local_assigns[:forks] == true
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- user = local_assigns[:user]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true
- load_pipeline_status(projects)
.js-projects-list-holder
- if any_projects?(projects)
- load_pipeline_status(projects)
%ul.projects-list
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
= render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description
forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user
- if @private_forks_count && @private_forks_count > 0
%li.project-row.private-forks-notice
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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