Commit 99b95ea9 authored by Achilleas Pipinellis's avatar Achilleas Pipinellis

Merge branch 'master' into 'docs-replace-pipelines-cicd'

# Conflicts:
#   doc/ci/variables/README.md
parents 8211ce19 2defc7b9
...@@ -191,6 +191,9 @@ review-docs-deploy: ...@@ -191,6 +191,9 @@ review-docs-deploy:
stage: build stage: build
environment: environment:
name: review-docs/$CI_COMMIT_REF_NAME name: review-docs/$CI_COMMIT_REF_NAME
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://$CI_COMMIT_REF_SLUG-built-from-ce-ee.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup on_stop: review-docs-cleanup
script: script:
- gem install gitlab --no-doc - gem install gitlab --no-doc
......
...@@ -643,7 +643,7 @@ Metrics/ClassLength: ...@@ -643,7 +643,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method. # of test cases needed to validate a method.
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Enabled: true Enabled: true
Max: 14 Max: 13
# Limit lines to 80 characters. # Limit lines to 80 characters.
Metrics/LineLength: Metrics/LineLength:
...@@ -665,7 +665,7 @@ Metrics/ParameterLists: ...@@ -665,7 +665,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader. # A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Enabled: true Enabled: true
Max: 17 Max: 15
# Lint ######################################################################## # Lint ########################################################################
......
...@@ -286,7 +286,10 @@ might be edited to make them small and simple. ...@@ -286,7 +286,10 @@ might be edited to make them small and simple.
Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker. Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
For changes in the interface, it can be helpful to create a mockup first. For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
If you want to create something yourself, consider opening an issue first to If you want to create something yourself, consider opening an issue first to
discuss whether it is interesting to include this in GitLab. discuss whether it is interesting to include this in GitLab.
......
...@@ -407,4 +407,4 @@ gem 'flipper-active_record', '~> 0.10.2' ...@@ -407,4 +407,4 @@ gem 'flipper-active_record', '~> 0.10.2'
# Structured logging # Structured logging
gem 'lograge', '~> 0.5' gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.6' gem 'grape_logging', '~> 1.7'
...@@ -355,9 +355,9 @@ GEM ...@@ -355,9 +355,9 @@ GEM
activesupport activesupport
grape (>= 0.16.0) grape (>= 0.16.0)
rake rake
grape_logging (1.6.0) grape_logging (1.7.0)
grape grape
grpc (1.4.5) grpc (1.6.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleauth (~> 0.5.1) googleauth (~> 0.5.1)
haml (4.0.7) haml (4.0.7)
...@@ -1037,7 +1037,7 @@ DEPENDENCIES ...@@ -1037,7 +1037,7 @@ DEPENDENCIES
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.6) grape_logging (~> 1.7)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
hashie-forbidden_attributes hashie-forbidden_attributes
......
...@@ -7,6 +7,7 @@ class DeleteModal { ...@@ -7,6 +7,7 @@ class DeleteModal {
this.$branchName = $('.js-branch-name', this.$modal); this.$branchName = $('.js-branch-name', this.$modal);
this.$confirmInput = $('.js-delete-branch-input', this.$modal); this.$confirmInput = $('.js-delete-branch-input', this.$modal);
this.$deleteBtn = $('.js-delete-branch', this.$modal); this.$deleteBtn = $('.js-delete-branch', this.$modal);
this.$notMerged = $('.js-not-merged', this.$modal);
this.bindEvents(); this.bindEvents();
} }
...@@ -16,8 +17,10 @@ class DeleteModal { ...@@ -16,8 +17,10 @@ class DeleteModal {
} }
setModalData(e) { setModalData(e) {
this.branchName = e.currentTarget.dataset.branchName || ''; const branchData = e.currentTarget.dataset;
this.deletePath = e.currentTarget.dataset.deletePath || ''; this.branchName = branchData.branchName || '';
this.deletePath = branchData.deletePath || '';
this.isMerged = !!branchData.isMerged;
this.updateModal(); this.updateModal();
} }
...@@ -30,6 +33,7 @@ class DeleteModal { ...@@ -30,6 +33,7 @@ class DeleteModal {
this.$confirmInput.val(''); this.$confirmInput.val('');
this.$deleteBtn.attr('href', this.deletePath); this.$deleteBtn.attr('href', this.deletePath);
this.$deleteBtn.attr('disabled', true); this.$deleteBtn.attr('disabled', true);
this.$notMerged.toggleClass('hidden', this.isMerged);
} }
} }
......
...@@ -12,4 +12,5 @@ import 'core-js/fn/symbol'; ...@@ -12,4 +12,5 @@ import 'core-js/fn/symbol';
// Browser polyfills // Browser polyfills
import './polyfills/custom_event'; import './polyfills/custom_event';
import './polyfills/element'; import './polyfills/element';
import './polyfills/event';
import './polyfills/nodelist'; import './polyfills/nodelist';
if (typeof window.CustomEvent !== 'function') { if (typeof window.CustomEvent !== 'function') {
window.CustomEvent = function CustomEvent(event, params) { window.CustomEvent = function CustomEvent(event, params) {
const evt = document.createEvent('CustomEvent'); const evt = document.createEvent('CustomEvent');
const evtParams = params || { bubbles: false, cancelable: false, detail: undefined }; const evtParams = {
bubbles: false,
cancelable: false,
detail: undefined,
...params,
};
evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail); evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail);
return evt; return evt;
}; };
......
/**
* Polyfill for IE11 support.
* new Event() is not supported by IE11.
* Although `initEvent` is deprecated for modern browsers it is the one supported by IE
*/
if (typeof window.Event !== 'function') {
window.Event = function Event(event, params) {
const evt = document.createEvent('Event');
const evtParams = {
bubbles: false,
cancelable: false,
...params,
};
evt.initEvent(event, evtParams.bubbles, evtParams.cancelable);
return evt;
};
window.Event.prototype = Event;
}
...@@ -15,6 +15,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { ...@@ -15,6 +15,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
params: { params: {
per_page: 20, per_page: 20,
active: true, active: true,
group_id: this.getGroupId(),
project_id: this.getProjectId(), project_id: this.getProjectId(),
current_user: true, current_user: true,
}, },
...@@ -47,6 +48,10 @@ class DropdownUser extends gl.FilteredSearchDropdown { ...@@ -47,6 +48,10 @@ class DropdownUser extends gl.FilteredSearchDropdown {
super.renderContent(forceShowList); super.renderContent(forceShowList);
} }
getGroupId() {
return this.input.getAttribute('data-group-id');
}
getProjectId() { getProjectId() {
return this.input.getAttribute('data-project-id'); return this.input.getAttribute('data-project-id');
} }
......
...@@ -77,10 +77,11 @@ export const hideMenu = (el) => { ...@@ -77,10 +77,11 @@ export const hideMenu = (el) => {
export const moveSubItemsToPosition = (el, subItems) => { export const moveSubItemsToPosition = (el, subItems) => {
const boundingRect = el.getBoundingClientRect(); const boundingRect = el.getBoundingClientRect();
const top = calculateTop(boundingRect, subItems.offsetHeight); const top = calculateTop(boundingRect, subItems.offsetHeight);
const left = sidebar ? sidebar.offsetWidth : 50;
const isAbove = top < boundingRect.top; const isAbove = top < boundingRect.top;
subItems.classList.add('fly-out-list'); subItems.classList.add('fly-out-list');
subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign subItems.style.transform = `translate3d(${left}px, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign
const subItemsRect = subItems.getBoundingClientRect(); const subItemsRect = subItems.getBoundingClientRect();
...@@ -148,7 +149,7 @@ export const documentMouseMove = (e) => { ...@@ -148,7 +149,7 @@ export const documentMouseMove = (e) => {
export const subItemsMouseLeave = (relatedTarget) => { export const subItemsMouseLeave = (relatedTarget) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (!relatedTarget.closest(`.${IS_OVER_CLASS}`)) { if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
hideMenu(currentOpenMenu); hideMenu(currentOpenMenu);
} }
}; };
......
...@@ -72,10 +72,6 @@ export default { ...@@ -72,10 +72,6 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
isConfidential: {
type: Boolean,
required: true,
},
markdownPreviewPath: { markdownPreviewPath: {
type: String, type: String,
required: true, required: true,
...@@ -131,7 +127,6 @@ export default { ...@@ -131,7 +127,6 @@ export default {
this.showForm = true; this.showForm = true;
this.store.setFormState({ this.store.setFormState({
title: this.state.titleText, title: this.state.titleText,
confidential: this.isConfidential,
description: this.state.descriptionText, description: this.state.descriptionText,
lockedWarningVisible: false, lockedWarningVisible: false,
updateLoading: false, updateLoading: false,
...@@ -147,8 +142,6 @@ export default { ...@@ -147,8 +142,6 @@ export default {
.then((data) => { .then((data) => {
if (location.pathname !== data.web_url) { if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url); gl.utils.visitUrl(data.web_url);
} else if (data.confidential !== this.isConfidential) {
gl.utils.visitUrl(location.pathname);
} }
return this.service.getData(); return this.service.getData();
......
<script>
export default {
props: {
formState: {
type: Object,
required: true,
},
},
};
</script>
<template>
<fieldset class="checkbox">
<label for="issue-confidential">
<input
type="checkbox"
value="1"
id="issue-confidential"
v-model="formState.confidential" />
This issue is confidential and should only be visible to team members with at least Reporter access.
</label>
</fieldset>
</template>
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import descriptionField from './fields/description.vue'; import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue'; import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue'; import descriptionTemplate from './fields/description_template.vue';
import confidentialCheckbox from './fields/confidential_checkbox.vue';
export default { export default {
props: { props: {
...@@ -44,7 +43,6 @@ ...@@ -44,7 +43,6 @@
descriptionField, descriptionField,
descriptionTemplate, descriptionTemplate,
editActions, editActions,
confidentialCheckbox,
}, },
computed: { computed: {
hasIssuableTemplates() { hasIssuableTemplates() {
...@@ -81,8 +79,6 @@ ...@@ -81,8 +79,6 @@
:form-state="formState" :form-state="formState"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" /> :markdown-docs-path="markdownDocsPath" />
<confidential-checkbox
:form-state="formState" />
<edit-actions <edit-actions
:form-state="formState" :form-state="formState"
:can-destroy="canDestroy" /> :can-destroy="canDestroy" />
......
...@@ -35,7 +35,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -35,7 +35,6 @@ document.addEventListener('DOMContentLoaded', () => {
initialDescriptionHtml: this.initialDescriptionHtml, initialDescriptionHtml: this.initialDescriptionHtml,
initialDescriptionText: this.initialDescriptionText, initialDescriptionText: this.initialDescriptionText,
issuableTemplates: this.issuableTemplates, issuableTemplates: this.issuableTemplates,
isConfidential: this.isConfidential,
markdownPreviewPath: this.markdownPreviewPath, markdownPreviewPath: this.markdownPreviewPath,
markdownDocsPath: this.markdownDocsPath, markdownDocsPath: this.markdownDocsPath,
projectPath: this.projectPath, projectPath: this.projectPath,
......
...@@ -3,7 +3,6 @@ export default class Store { ...@@ -3,7 +3,6 @@ export default class Store {
this.state = initialState; this.state = initialState;
this.formState = { this.formState = {
title: '', title: '',
confidential: false,
description: '', description: '',
lockedWarningVisible: false, lockedWarningVisible: false,
updateLoading: false, updateLoading: false,
......
...@@ -127,13 +127,6 @@ import DropdownUtils from './filtered_search/dropdown_utils'; ...@@ -127,13 +127,6 @@ import DropdownUtils from './filtered_search/dropdown_utils';
$('.has-tooltip', $value).tooltip({ $('.has-tooltip', $value).tooltip({
container: 'body' container: 'body'
}); });
return $value.find('a').each(function(i) {
return setTimeout((function(_this) {
return function() {
return gl.animate.animate($(_this), 'pulse');
};
})(this), 200 * i);
});
}); });
}; };
$dropdown.glDropdown({ $dropdown.glDropdown({
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, max-len */
(function() {
(function(w) {
if (w.gl == null) {
w.gl = {};
}
if (gl.animate == null) {
gl.animate = {};
}
gl.animate.animate = function($el, animation, options, done) {
if ((options != null ? options.cssStart : void 0) != null) {
$el.css(options.cssStart);
}
$el.removeClass(animation + ' animated').addClass(animation + ' animated').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function() {
$(this).removeClass(animation + ' animated');
if (done != null) {
done();
}
if ((options != null ? options.cssEnd : void 0) != null) {
$el.css(options.cssEnd);
}
});
};
gl.animate.animateEach = function($els, animation, time, options, done) {
var dfd;
dfd = $.Deferred();
if (!$els.length) {
dfd.resolve();
}
$els.each(function(i) {
setTimeout((function(_this) {
return function() {
var $this;
$this = $(_this);
return gl.animate.animate($this, animation, options, function() {
if (i === $els.length - 1) {
dfd.resolve();
if (done != null) {
return done();
}
}
});
};
})(this), time * i);
});
return dfd.promise();
};
})(window);
}).call(window);
...@@ -39,7 +39,6 @@ import './commit/file'; ...@@ -39,7 +39,6 @@ import './commit/file';
import './commit/image_file'; import './commit/image_file';
// lib/utils // lib/utils
import './lib/utils/animate';
import './lib/utils/bootstrap_linked_tabs'; import './lib/utils/bootstrap_linked_tabs';
import './lib/utils/common_utils'; import './lib/utils/common_utils';
import './lib/utils/datetime_utility'; import './lib/utils/datetime_utility';
......
...@@ -45,7 +45,7 @@ import _ from 'underscore'; ...@@ -45,7 +45,7 @@ import _ from 'underscore';
if (issueUpdateURL) { if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
} }
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
...@@ -208,6 +208,7 @@ import _ from 'underscore'; ...@@ -208,6 +208,7 @@ import _ from 'underscore';
if (data.milestone != null) { if (data.milestone != null) {
data.milestone.full_path = _this.currentProject.full_path; data.milestone.full_path = _this.currentProject.full_path;
data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date); data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone)); $value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else { } else {
......
<script> <script>
/* global Flash */ /* global Flash */
import _ from 'underscore'; import _ from 'underscore';
import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service'; import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue'; import GraphGroup from './graph_group.vue';
import Graph from './graph.vue'; import Graph from './graph.vue';
...@@ -21,10 +20,9 @@ ...@@ -21,10 +20,9 @@
hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics), hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath, documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath, settingsPath: metricsData.settingsPath,
endpoint: metricsData.additionalMetrics, metricsEndpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint, deploymentEndpoint: metricsData.deploymentEndpoint,
showEmptyState: true, showEmptyState: true,
backOffRequestCounter: 0,
updateAspectRatio: false, updateAspectRatio: false,
updatedAspectRatios: 0, updatedAspectRatios: 0,
resizeThrottled: {}, resizeThrottled: {},
...@@ -39,50 +37,16 @@ ...@@ -39,50 +37,16 @@
methods: { methods: {
getGraphsData() { getGraphsData() {
const maxNumberOfRequests = 3;
this.state = 'loading'; this.state = 'loading';
gl.utils.backOff((next, stop) => { Promise.all([
this.service.get().then((resp) => { this.service.getGraphsData()
if (resp.status === statusCodes.NO_CONTENT) { .then(data => this.store.storeMetrics(data)),
this.backOffRequestCounter = this.backOffRequestCounter += 1; this.service.getDeploymentData()
if (this.backOffRequestCounter < maxNumberOfRequests) { .then(data => this.store.storeDeploymentData(data))
next(); .catch(() => new Flash('Error getting deployment information.')),
} else { ])
stop(new Error('Failed to connect to the prometheus server')); .then(() => { this.showEmptyState = false; })
} .catch(() => { this.state = 'unableToConnect'; });
} else {
stop(resp);
}
}).catch(stop);
})
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.state = 'unableToConnect';
return false;
}
return resp.json();
})
.then((metricGroupsData) => {
if (!metricGroupsData) return false;
this.store.storeMetrics(metricGroupsData.data);
return this.getDeploymentData();
})
.then((deploymentData) => {
if (deploymentData !== false) {
this.store.storeDeploymentData(deploymentData.deployments);
this.showEmptyState = false;
}
return {};
})
.catch(() => {
this.state = 'unableToConnect';
});
},
getDeploymentData() {
return this.service.getDeploymentData(this.deploymentEndpoint)
.then(resp => resp.json())
.catch(() => new Flash('Error getting deployment information.'));
}, },
resize() { resize() {
...@@ -99,7 +63,10 @@ ...@@ -99,7 +63,10 @@
}, },
created() { created() {
this.service = new MonitoringService(this.endpoint); this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
}, },
......
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import VueResource from 'vue-resource';
import statusCodes from '../../lib/utils/http_status';
Vue.use(VueResource); Vue.use(VueResource);
const MAX_REQUESTS = 3;
function backOffRequest(makeRequestCallback) {
let requestCounter = 0;
return gl.utils.backOff((next, stop) => {
makeRequestCallback().then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
requestCounter += 1;
if (requestCounter < MAX_REQUESTS) {
next();
} else {
stop(new Error('Failed to connect to the prometheus server'));
}
} else {
stop(resp);
}
}).catch(stop);
});
}
export default class MonitoringService { export default class MonitoringService {
constructor(endpoint) { constructor({ metricsEndpoint, deploymentEndpoint }) {
this.graphs = Vue.resource(endpoint); this.metricsEndpoint = metricsEndpoint;
this.deploymentEndpoint = deploymentEndpoint;
} }
get() { getGraphsData() {
return this.graphs.get(); return backOffRequest(() => Vue.http.get(this.metricsEndpoint))
.then(resp => resp.json())
.then((response) => {
if (!response || !response.data) {
throw new Error('Unexpected metrics data response from prometheus endpoint');
}
return response.data;
});
} }
// eslint-disable-next-line class-methods-use-this getDeploymentData() {
getDeploymentData(endpoint) { return backOffRequest(() => Vue.http.get(this.deploymentEndpoint))
return Vue.http.get(endpoint); .then(resp => resp.json())
.then((response) => {
if (!response || !response.deployments) {
throw new Error('Unexpected deployment data response from prometheus endpoint');
}
return response.deployments;
});
} }
} }
...@@ -15,7 +15,6 @@ export default class NewNavSidebar { ...@@ -15,7 +15,6 @@ export default class NewNavSidebar {
this.$openSidebar = $('.toggle-mobile-nav'); this.$openSidebar = $('.toggle-mobile-nav');
this.$closeSidebar = $('.close-nav-button'); this.$closeSidebar = $('.close-nav-button');
this.$sidebarToggle = $('.js-toggle-sidebar'); this.$sidebarToggle = $('.js-toggle-sidebar');
this.$topLevelLinks = $('.sidebar-top-level-items > li > a');
} }
bindEvents() { bindEvents() {
...@@ -56,10 +55,6 @@ export default class NewNavSidebar { ...@@ -56,10 +55,6 @@ export default class NewNavSidebar {
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
} }
NewNavSidebar.setCollapsedCookie(collapsed); NewNavSidebar.setCollapsedCookie(collapsed);
this.$topLevelLinks.attr('title', function updateTopLevelTitle() {
return collapsed ? this.getAttribute('aria-label') : '';
});
} }
render() { render() {
...@@ -68,7 +63,7 @@ export default class NewNavSidebar { ...@@ -68,7 +63,7 @@ export default class NewNavSidebar {
if (breakpoint === 'sm' || breakpoint === 'md') { if (breakpoint === 'sm' || breakpoint === 'md') {
this.toggleCollapsedSidebar(true); this.toggleCollapsedSidebar(true);
} else if (breakpoint === 'lg') { } else if (breakpoint === 'lg') {
const collapse = this.$sidebar.hasClass('sidebar-icons-only'); const collapse = Cookies.get('sidebar_collapsed') === 'true';
this.toggleCollapsedSidebar(collapse); this.toggleCollapsedSidebar(collapse);
} }
} }
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
@import "framework/flash"; @import "framework/flash";
@import "framework/forms"; @import "framework/forms";
@import "framework/gfm"; @import "framework/gfm";
@import "framework/gitlab-theme";
@import "framework/header"; @import "framework/header";
@import "framework/highlight"; @import "framework/highlight";
@import "framework/issue_box"; @import "framework/issue_box";
......
gl-emoji { gl-emoji {
font-style: normal;
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
......
...@@ -17,10 +17,13 @@ ...@@ -17,10 +17,13 @@
max-width: $limited-layout-width-sm; max-width: $limited-layout-width-sm;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@media (min-width: $screen-md-min) {
padding-top: 64px; padding-top: 64px;
padding-bottom: 64px; padding-bottom: 64px;
} }
} }
}
table { table {
@extend .table; @extend .table;
......
/**
* Styles the GitLab application with a specific color theme
*/
@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
// Header
header.navbar-gitlab-new {
background: linear-gradient(to right, $color-900, $color-800);
.navbar-collapse {
color: $color-200;
}
.container-fluid {
.navbar-toggle {
border-left: 1px solid lighten($color-700, 10%);
}
}
.navbar-sub-nav,
.navbar-nav {
> li {
> a:hover,
> a:focus {
background-color: rgba($color-200, .2);
}
&.active > a,
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
svg {
fill: currentColor;
}
}
&.line-separator {
border-left: 1px solid rgba($color-200, .2);
}
}
}
.navbar-sub-nav {
color: $color-200;
}
.nav {
> li {
color: $color-200;
> a {
svg {
fill: $color-200;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $color-200;
}
}
&:hover,
&:focus {
@media (min-width: $screen-sm-min) {
background-color: rgba($color-200, .2);
}
svg {
fill: currentColor;
}
}
}
&.active > a,
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
&:hover {
svg {
fill: $color-900;
}
}
}
.impersonated-user,
.impersonated-user:hover {
svg {
fill: $color-900;
}
}
}
}
}
.title {
> a {
&:hover,
&:focus {
background-color: rgba($color-200, .2);
}
}
}
.search {
form {
background-color: rgba($color-200, .2);
&:hover {
background-color: rgba($color-200, .3);
}
}
.location-badge {
color: $color-100;
background-color: rgba($color-200, .1);
border-right: 1px solid $color-800;
}
.search-input::placeholder {
color: rgba($color-200, .8);
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: rgba($color-200, .8);
}
}
&.search-active {
form {
background-color: $white-light;
}
.location-badge {
color: $gl-text-color;
}
.search-input-wrap {
.search-icon {
color: rgba($color-200, .8);
}
}
}
}
.btn-sign-in {
background-color: $color-100;
color: $color-900;
}
// Sidebar
.nav-sidebar li.active {
box-shadow: inset 4px 0 0 $color-700;
> a {
color: $color-800;
}
svg {
fill: $color-800;
}
}
.sidebar-top-level-items > li.active .badge {
color: $color-800;
}
.nav-links li.active a {
border-bottom-color: $color-500;
.badge {
font-weight: $gl-font-weight-bold;
}
}
}
body {
&.ui_indigo {
@include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light);
}
&.ui_dark {
@include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light);
}
&.ui_blue {
@include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light);
}
&.ui_green {
@include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light);
}
&.ui_light {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
header.navbar-gitlab-new {
background: $theme-gray-100;
box-shadow: 0 2px 0 0 $border-color;
.logo-text svg {
fill: $theme-gray-900;
}
.navbar-sub-nav,
.navbar-nav {
> li {
> a:hover,
> a:focus {
color: $theme-gray-900;
}
&.active > a {
color: $white-light;
&:hover {
color: $white-light;
}
}
}
}
.container-fluid {
.navbar-toggle,
.navbar-toggle:hover {
color: $theme-gray-700;
border-left: 1px solid $theme-gray-200;
}
}
}
.search {
form {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $border-color;
&:hover {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $blue-100;
.location-badge {
box-shadow: inset 0 0 0 1px $blue-100;
}
}
}
.search-input-wrap {
.search-icon {
color: $theme-gray-200;
}
}
.location-badge {
color: $theme-gray-700;
box-shadow: inset 0 0 0 1px $border-color;
background-color: $nav-badge-bg;
border-right: 0;
}
}
.nav-sidebar li.active {
> a {
color: $theme-gray-900;
}
svg {
fill: $theme-gray-900;
}
}
.sidebar-top-level-items > li.active .badge {
color: $theme-gray-900;
}
}
}
...@@ -111,7 +111,6 @@ header { ...@@ -111,7 +111,6 @@ header {
svg { svg {
height: 16px; height: 16px;
width: 23px; width: 23px;
fill: currentColor;
} }
} }
......
...@@ -328,7 +328,7 @@ ...@@ -328,7 +328,7 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
text-align: center; text-align: center;
margin-top: $header-height; margin-top: $new-navbar-height;
.container-fluid { .container-fluid {
position: relative; position: relative;
......
...@@ -13,6 +13,7 @@ $sidebar-breakpoint: 1024px; ...@@ -13,6 +13,7 @@ $sidebar-breakpoint: 1024px;
$darken-normal-factor: 7%; $darken-normal-factor: 7%;
$darken-dark-factor: 10%; $darken-dark-factor: 10%;
$darken-border-factor: 5%; $darken-border-factor: 5%;
$darken-border-dashed-factor: 25%;
$white-light: #fff; $white-light: #fff;
$white-normal: #f0f0f0; $white-normal: #f0f0f0;
...@@ -74,6 +75,8 @@ $red-700: #a62d19; ...@@ -74,6 +75,8 @@ $red-700: #a62d19;
$red-800: #8b2615; $red-800: #8b2615;
$red-900: #711e11; $red-900: #711e11;
// GitLab themes
$indigo-50: #f7f7ff; $indigo-50: #f7f7ff;
$indigo-100: #ebebfa; $indigo-100: #ebebfa;
$indigo-200: #d1d1f0; $indigo-200: #d1d1f0;
...@@ -86,6 +89,43 @@ $indigo-800: #393982; ...@@ -86,6 +89,43 @@ $indigo-800: #393982;
$indigo-900: #292961; $indigo-900: #292961;
$indigo-950: #1a1a40; $indigo-950: #1a1a40;
$theme-gray-50: #fafafa;
$theme-gray-100: #f2f2f2;
$theme-gray-200: #dfdfdf;
$theme-gray-300: #cccccc;
$theme-gray-400: #bababa;
$theme-gray-500: #a7a7a7;
$theme-gray-600: #949494;
$theme-gray-700: #707070;
$theme-gray-800: #4f4f4f;
$theme-gray-900: #2e2e2e;
$theme-gray-950: #1f1f1f;
$theme-blue-50: #f4f8fc;
$theme-blue-100: #e6edf5;
$theme-blue-200: #c8d7e6;
$theme-blue-300: #97b3cf;
$theme-blue-400: #648cb4;
$theme-blue-500: #4a79a8;
$theme-blue-600: #3e6fa0;
$theme-blue-700: #305c88;
$theme-blue-800: #25496e;
$theme-blue-900: #1a3652;
$theme-blue-950: #0f2235;
$theme-green-50: #f2faf6;
$theme-green-100: #e4f3ea;
$theme-green-200: #c0dfcd;
$theme-green-300: #8ac2a1;
$theme-green-400: #52a274;
$theme-green-500: #35935c;
$theme-green-600: #288a50;
$theme-green-700: #1c7441;
$theme-green-800: #145d33;
$theme-green-900: #0d4524;
$theme-green-950: #072d16;
$black: #000; $black: #000;
$black-transparent: rgba(0, 0, 0, 0.3); $black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424; $almost-black: #242424;
...@@ -95,6 +135,7 @@ $border-white-normal: darken($white-normal, $darken-border-factor); ...@@ -95,6 +135,7 @@ $border-white-normal: darken($white-normal, $darken-border-factor);
$border-gray-light: darken($gray-light, $darken-border-factor); $border-gray-light: darken($gray-light, $darken-border-factor);
$border-gray-normal: darken($gray-normal, $darken-border-factor); $border-gray-normal: darken($gray-normal, $darken-border-factor);
$border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor); $border-gray-dark: darken($white-normal, $darken-border-factor);
/* /*
......
...@@ -9,10 +9,20 @@ ...@@ -9,10 +9,20 @@
header.navbar-gitlab-new { header.navbar-gitlab-new {
color: $white-light; color: $white-light;
background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0; border-bottom: 0;
min-height: $new-navbar-height; min-height: $new-navbar-height;
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
.header-content { .header-content {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
...@@ -38,10 +48,10 @@ header.navbar-gitlab-new { ...@@ -38,10 +48,10 @@ header.navbar-gitlab-new {
img { img {
height: 28px; height: 28px;
margin-right: 10px; margin-right: 8px;
} }
> a { a {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -54,22 +64,6 @@ header.navbar-gitlab-new { ...@@ -54,22 +64,6 @@ header.navbar-gitlab-new {
margin-right: 8px; margin-right: 8px;
} }
} }
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
&:hover,
&:focus {
background-color: rgba($indigo-200, .2);
}
} }
} }
...@@ -106,7 +100,6 @@ header.navbar-gitlab-new { ...@@ -106,7 +100,6 @@ header.navbar-gitlab-new {
.navbar-collapse { .navbar-collapse {
padding-left: 0; padding-left: 0;
color: $indigo-200;
box-shadow: 0; box-shadow: 0;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -132,7 +125,6 @@ header.navbar-gitlab-new { ...@@ -132,7 +125,6 @@ header.navbar-gitlab-new {
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
color: currentColor; color: currentColor;
border-left: 1px solid lighten($indigo-700, 10%);
&:hover, &:hover,
&:focus, &:focus,
...@@ -167,42 +159,27 @@ header.navbar-gitlab-new { ...@@ -167,42 +159,27 @@ header.navbar-gitlab-new {
will-change: color; will-change: color;
margin: 4px 2px; margin: 4px 2px;
padding: 6px 8px; padding: 6px 8px;
color: $indigo-200;
height: 32px; height: 32px;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
padding: 0; padding: 0;
} }
svg {
fill: $indigo-200;
}
&.header-user-dropdown-toggle { &.header-user-dropdown-toggle {
margin-left: 2px; margin-left: 2px;
.header-user-avatar { .header-user-avatar {
border-color: $indigo-200;
margin-right: 0; margin-right: 0;
} }
} }
}
.header-new-dropdown-toggle {
margin-right: 0;
}
> a:hover, &:hover,
> a:focus { &:focus {
text-decoration: none; text-decoration: none;
outline: 0; outline: 0;
opacity: 1; opacity: 1;
color: $white-light; color: $white-light;
@media (min-width: $screen-sm-min) {
background-color: rgba($indigo-200, .2);
}
svg { svg {
fill: currentColor; fill: currentColor;
} }
...@@ -213,6 +190,11 @@ header.navbar-gitlab-new { ...@@ -213,6 +190,11 @@ header.navbar-gitlab-new {
} }
} }
} }
}
.header-new-dropdown-toggle {
margin-right: 0;
}
.impersonated-user, .impersonated-user,
.impersonated-user:hover { .impersonated-user:hover {
...@@ -220,10 +202,6 @@ header.navbar-gitlab-new { ...@@ -220,10 +202,6 @@ header.navbar-gitlab-new {
background-color: $white-light; background-color: $white-light;
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
svg {
fill: $indigo-900;
}
} }
.impersonation-btn, .impersonation-btn,
...@@ -241,8 +219,6 @@ header.navbar-gitlab-new { ...@@ -241,8 +219,6 @@ header.navbar-gitlab-new {
&.active > a, &.active > a,
&.dropdown.open > a { &.dropdown.open > a {
color: $indigo-900;
background-color: $white-light;
svg { svg {
fill: currentColor; fill: currentColor;
...@@ -256,7 +232,6 @@ header.navbar-gitlab-new { ...@@ -256,7 +232,6 @@ header.navbar-gitlab-new {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
margin: 0 0 0 6px; margin: 0 0 0 6px;
color: $indigo-200;
.dropdown-chevron { .dropdown-chevron {
position: relative; position: relative;
...@@ -274,17 +249,6 @@ header.navbar-gitlab-new { ...@@ -274,17 +249,6 @@ header.navbar-gitlab-new {
text-decoration: none; text-decoration: none;
outline: 0; outline: 0;
color: $white-light; color: $white-light;
background-color: rgba($indigo-200, .2);
svg {
fill: currentColor;
}
}
&.active > a,
&.dropdown.open > a {
color: $indigo-900;
background-color: $white-light;
svg { svg {
fill: currentColor; fill: currentColor;
...@@ -309,7 +273,6 @@ header.navbar-gitlab-new { ...@@ -309,7 +273,6 @@ header.navbar-gitlab-new {
} }
&.line-separator { &.line-separator {
border-left: 1px solid rgba($indigo-200, .2);
margin: 8px; margin: 8px;
} }
} }
...@@ -339,17 +302,14 @@ header.navbar-gitlab-new { ...@@ -339,17 +302,14 @@ header.navbar-gitlab-new {
height: 32px; height: 32px;
border: 0; border: 0;
border-radius: $border-radius-default; border-radius: $border-radius-default;
background-color: rgba($indigo-200, .2);
transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s; transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover { &:hover {
background-color: rgba($indigo-200, .3);
box-shadow: none; box-shadow: none;
} }
} }
&.search-active form { &.search-active form {
background-color: $white-light;
box-shadow: none; box-shadow: none;
.search-input { .search-input {
...@@ -377,43 +337,26 @@ header.navbar-gitlab-new { ...@@ -377,43 +337,26 @@ header.navbar-gitlab-new {
} }
.search-input::placeholder { .search-input::placeholder {
color: rgba($indigo-200, .8);
transition: color ease-in-out 0.15s; transition: color ease-in-out 0.15s;
} }
.location-badge { .location-badge {
font-size: 12px; font-size: 12px;
color: $indigo-100;
background-color: rgba($indigo-200, .1);
will-change: color;
margin: -4px 4px -4px -4px; margin: -4px 4px -4px -4px;
line-height: 25px; line-height: 25px;
padding: 4px 8px; padding: 4px 8px;
border-radius: 2px 0 0 2px; border-radius: 2px 0 0 2px;
border-right: 1px solid $indigo-800;
height: 32px; height: 32px;
transition: border-color ease-in-out 0.15s; transition: border-color ease-in-out 0.15s;
} }
.search-input-wrap {
.search-icon,
.clear-icon {
color: rgba($indigo-200, .8);
}
}
&.search-active { &.search-active {
.location-badge { .location-badge {
color: $gl-text-color;
background-color: $nav-badge-bg; background-color: $nav-badge-bg;
border-color: $border-color; border-color: $border-color;
} }
.search-input-wrap { .search-input-wrap {
.search-icon {
color: rgba($indigo-200, .8);
}
.clear-icon { .clear-icon {
color: $white-light; color: $white-light;
} }
...@@ -488,6 +431,7 @@ header.navbar-gitlab-new { ...@@ -488,6 +431,7 @@ header.navbar-gitlab-new {
.breadcrumb-item-text { .breadcrumb-item-text {
@include str-truncated(128px); @include str-truncated(128px);
text-decoration: inherit;
} }
.breadcrumbs-list-angle { .breadcrumbs-list-angle {
...@@ -517,8 +461,6 @@ header.navbar-gitlab-new { ...@@ -517,8 +461,6 @@ header.navbar-gitlab-new {
.btn-sign-in { .btn-sign-in {
margin-top: 3px; margin-top: 3px;
background-color: $indigo-100;
color: $indigo-900;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
&:hover { &:hover {
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
@import "bootstrap/variables"; @import "bootstrap/variables";
$active-background: rgba(0, 0, 0, .04); $active-background: rgba(0, 0, 0, .04);
$active-border: $indigo-500;
$active-color: $indigo-700;
$active-hover-background: $active-background; $active-hover-background: $active-background;
$active-hover-color: $gl-text-color; $active-hover-color: $gl-text-color;
$inactive-badge-background: rgba(0, 0, 0, .08); $inactive-badge-background: rgba(0, 0, 0, .08);
...@@ -107,7 +105,8 @@ $new-sidebar-collapsed-width: 50px; ...@@ -107,7 +105,8 @@ $new-sidebar-collapsed-width: 50px;
} }
&.sidebar-icons-only { &.sidebar-icons-only {
width: $new-sidebar-collapsed-width; width: auto;
min-width: $new-sidebar-collapsed-width;
.nav-sidebar-inner-scroll { .nav-sidebar-inner-scroll {
overflow-x: hidden; overflow-x: hidden;
...@@ -126,6 +125,10 @@ $new-sidebar-collapsed-width: 50px; ...@@ -126,6 +125,10 @@ $new-sidebar-collapsed-width: 50px;
.fly-out-top-item { .fly-out-top-item {
display: block; display: block;
} }
.avatar-container {
margin-right: 0;
}
} }
&.nav-sidebar-expanded { &.nav-sidebar-expanded {
...@@ -162,16 +165,9 @@ $new-sidebar-collapsed-width: 50px; ...@@ -162,16 +165,9 @@ $new-sidebar-collapsed-width: 50px;
} }
li.active { li.active {
box-shadow: inset 4px 0 0 $active-border;
> a { > a {
color: $active-color;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
svg {
fill: $active-color;
}
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -196,7 +192,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -196,7 +192,7 @@ $new-sidebar-collapsed-width: 50px;
.nav-sidebar-inner-scroll { .nav-sidebar-inner-scroll {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow: auto; overflow: scroll;
} }
.with-performance-bar .nav-sidebar { .with-performance-bar .nav-sidebar {
...@@ -224,7 +220,6 @@ $new-sidebar-collapsed-width: 50px; ...@@ -224,7 +220,6 @@ $new-sidebar-collapsed-width: 50px;
&:hover, &:hover,
&:focus { &:focus {
background: $active-background; background: $active-background;
color: $active-color;
} }
} }
} }
...@@ -258,7 +253,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -258,7 +253,7 @@ $new-sidebar-collapsed-width: 50px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
position: fixed; position: fixed;
top: 0; top: 0;
left: $new-sidebar-width; left: 0;
min-width: 150px; min-width: 150px;
margin-top: -1px; margin-top: -1px;
padding: 4px 1px; padding: 4px 1px;
...@@ -324,7 +319,6 @@ $new-sidebar-collapsed-width: 50px; ...@@ -324,7 +319,6 @@ $new-sidebar-collapsed-width: 50px;
} }
.badge { .badge {
color: $active-color;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
...@@ -397,10 +391,6 @@ $new-sidebar-collapsed-width: 50px; ...@@ -397,10 +391,6 @@ $new-sidebar-collapsed-width: 50px;
} }
.sidebar-sub-level-items { .sidebar-sub-level-items {
@media (min-width: $screen-sm-min) {
left: $new-sidebar-collapsed-width;
}
&:not(.flyout-list) { &:not(.flyout-list) {
display: none; display: none;
} }
...@@ -501,13 +491,3 @@ $new-sidebar-collapsed-width: 50px; ...@@ -501,13 +491,3 @@ $new-sidebar-collapsed-width: 50px;
.with-performance-bar .boards-list { .with-performance-bar .boards-list {
height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height}); height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height});
} }
// Change color of all horizontal tabs to match the new indigo color
.nav-links li.active a {
border-bottom-color: $active-border;
.badge {
font-weight: $gl-font-weight-bold;
}
}
...@@ -608,7 +608,7 @@ ...@@ -608,7 +608,7 @@
+ .files, + .files,
+ .alert { + .alert {
margin-top: 30px; margin-top: 32px;
} }
} }
} }
...@@ -634,8 +634,16 @@ ...@@ -634,8 +634,16 @@
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
} }
.diff-changed-file {
display: flex;
align-items: center;
}
} }
.diff-file-changes-path { .diff-file-changes-path {
@include str-truncated(78%); flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
...@@ -449,6 +449,12 @@ ...@@ -449,6 +449,12 @@
} }
} }
} }
.milestone-title span {
@include str-truncated(100%);
display: block;
margin: 0 4px;
}
} }
a { a {
......
...@@ -95,6 +95,8 @@ ...@@ -95,6 +95,8 @@
} }
.omniauth-container { .omniauth-container {
font-size: 13px;
p { p {
margin: 0; margin: 0;
} }
......
@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) {
.one {
background-color: $color-1;
border-top-left-radius: $border-radius-default;
}
.two {
background-color: $color-2;
border-top-right-radius: $border-radius-default;
}
.three {
background-color: $color-3;
border-bottom-left-radius: $border-radius-default;
}
.four {
background-color: $color-4;
border-bottom-right-radius: $border-radius-default;
}
}
.application-theme {
label {
margin-right: 20px;
text-align: center;
}
.preview {
font-size: 0;
margin-bottom: 10px;
&.indigo {
@include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500);
}
&.dark {
@include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600);
}
&.light {
@include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100);
}
&.blue {
@include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500);
}
&.green {
@include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500);
}
}
.preview-row {
display: block;
}
.quadrant {
display: inline-block;
height: 50px;
width: 80px;
}
}
.syntax-theme { .syntax-theme {
label { label {
margin-right: 20px; margin-right: 20px;
......
...@@ -752,7 +752,7 @@ a.deploy-project-label { ...@@ -752,7 +752,7 @@ a.deploy-project-label {
} }
li.missing { li.missing {
border: 1px dashed $border-gray-normal; border: 1px dashed $border-gray-normal-dashed;
border-radius: $border-radius-default; border-radius: $border-radius-default;
a { a {
......
...@@ -71,6 +71,11 @@ ...@@ -71,6 +71,11 @@
height: 100%; height: 100%;
.monaco-editor.vs { .monaco-editor.vs {
.current-line {
border: none;
background: $well-light-border;
}
.line-numbers { .line-numbers {
cursor: pointer; cursor: pointer;
...@@ -84,6 +89,13 @@ ...@@ -84,6 +89,13 @@
} }
} }
.blob-no-preview {
.vertical-center {
justify-content: center;
width: 100%;
}
}
&.edit-mode { &.edit-mode {
.blob-viewer-container { .blob-viewer-container {
overflow: hidden; overflow: hidden;
...@@ -103,7 +115,7 @@ ...@@ -103,7 +115,7 @@
overflow: auto; overflow: auto;
> div, > div,
.file-content { .file-content:not(.wiki) {
display: flex; display: flex;
} }
......
...@@ -10,9 +10,8 @@ class Admin::DeployKeysController < Admin::ApplicationController ...@@ -10,9 +10,8 @@ class Admin::DeployKeysController < Admin::ApplicationController
end end
def create def create
@deploy_key = deploy_keys.new(create_params.merge(user: current_user)) @deploy_key = DeployKeys::CreateService.new(current_user, create_params.merge(public: true)).execute
if @deploy_key.persisted?
if @deploy_key.save
redirect_to admin_deploy_keys_path redirect_to admin_deploy_keys_path
else else
render 'new' render 'new'
......
...@@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController
:provider, :provider,
:remember_me, :remember_me,
:skype, :skype,
:theme_id,
:twitter, :twitter,
:username, :username,
:website_url :website_url
......
...@@ -11,9 +11,15 @@ module Boards ...@@ -11,9 +11,15 @@ module Boards
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20) issues = issues.page(params[:page]).per(params[:per] || 20)
make_sure_position_is_set(issues) make_sure_position_is_set(issues)
issues = issues.preload(:project,
:milestone,
:assignees,
labels: [:priorities],
notes: [:award_emoji, :author]
)
render json: { render json: {
issues: serialize_as_json(issues.preload(:project)), issues: serialize_as_json(issues),
size: issues.total_count size: issues.total_count
} }
end end
...@@ -76,14 +82,13 @@ module Boards ...@@ -76,14 +82,13 @@ module Boards
def serialize_as_json(resource) def serialize_as_json(resource)
resource.as_json( resource.as_json(
labels: true,
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true,
include: { include: {
project: { only: [:id, :path] }, project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] } milestone: { only: [:id, :title] }
}, }
user: current_user
) )
end end
end end
......
...@@ -7,11 +7,11 @@ module Ci ...@@ -7,11 +7,11 @@ module Ci
def create def create
@content = params[:content] @content = params[:content]
@error = Ci::GitlabCiYamlProcessor.validation_message(@content) @error = Gitlab::Ci::YamlProcessor.validation_message(@content)
@status = @error.blank? @status = @error.blank?
if @error.blank? if @error.blank?
@config_processor = Ci::GitlabCiYamlProcessor.new(@content) @config_processor = Gitlab::Ci::YamlProcessor.new(@content)
@stages = @config_processor.stages @stages = @config_processor.stages
@builds = @config_processor.builds @builds = @config_processor.builds
@jobs = @config_processor.jobs @jobs = @config_processor.jobs
......
...@@ -48,7 +48,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -48,7 +48,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
ProjectsFinder ProjectsFinder
.new(params: finder_params, current_user: current_user) .new(params: finder_params, current_user: current_user)
.execute .execute
.includes(:route, :creator, namespace: :route) .includes(:route, :creator, namespace: [:route, :owner])
end end
def load_events def load_events
......
...@@ -7,9 +7,9 @@ class Profiles::GpgKeysController < Profiles::ApplicationController ...@@ -7,9 +7,9 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
end end
def create def create
@gpg_key = current_user.gpg_keys.new(gpg_key_params) @gpg_key = GpgKeys::CreateService.new(current_user, gpg_key_params).execute
if @gpg_key.save if @gpg_key.persisted?
redirect_to profile_gpg_keys_path redirect_to profile_gpg_keys_path
else else
@gpg_keys = current_user.gpg_keys.select(&:persisted?) @gpg_keys = current_user.gpg_keys.select(&:persisted?)
......
...@@ -11,9 +11,9 @@ class Profiles::KeysController < Profiles::ApplicationController ...@@ -11,9 +11,9 @@ class Profiles::KeysController < Profiles::ApplicationController
end end
def create def create
@key = current_user.keys.new(key_params) @key = Keys::CreateService.new(current_user, key_params).execute
if @key.save if @key.persisted?
redirect_to profile_key_path(@key) redirect_to profile_key_path(@key)
else else
@keys = current_user.keys.select(&:persisted?) @keys = current_user.keys.select(&:persisted?)
......
...@@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController ...@@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:color_scheme_id, :color_scheme_id,
:layout, :layout,
:dashboard, :dashboard,
:project_view :project_view,
:theme_id
) )
end end
end end
...@@ -27,7 +27,7 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -27,7 +27,7 @@ class Projects::CompareController < Projects::ApplicationController
def create def create
if params[:from].blank? || params[:to].blank? if params[:from].blank? || params[:to].blank?
flash[:alert] = "You must select from and to branches" flash[:alert] = "You must select a Source and a Target revision"
from_to_vars = { from_to_vars = {
from: params[:from].presence, from: params[:from].presence,
to: params[:to].presence to: params[:to].presence
......
...@@ -22,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -22,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end end
def create def create
@key = DeployKey.new(create_params.merge(user: current_user)) @key = DeployKeys::CreateService.new(current_user, create_params).execute
unless @key.valid? && @project.deploy_keys << @key unless @key.valid? && @project.deploy_keys << @key
flash[:alert] = @key.errors.full_messages.join(', ').html_safe flash[:alert] = @key.errors.full_messages.join(', ').html_safe
......
...@@ -132,10 +132,10 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -132,10 +132,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def charts def charts
@charts = {} @charts = {}
@charts[:week] = Ci::Charts::WeekChart.new(project) @charts[:week] = Gitlab::Ci::Charts::WeekChart.new(project)
@charts[:month] = Ci::Charts::MonthChart.new(project) @charts[:month] = Gitlab::Ci::Charts::MonthChart.new(project)
@charts[:year] = Ci::Charts::YearChart.new(project) @charts[:year] = Gitlab::Ci::Charts::YearChart.new(project)
@charts[:pipeline_times] = Ci::Charts::PipelineTime.new(project) @charts[:pipeline_times] = Gitlab::Ci::Charts::PipelineTime.new(project)
@counts = {} @counts = {}
@counts[:total] = @project.pipelines.count(:all) @counts[:total] = @project.pipelines.count(:all)
......
module AutoDevopsHelper module AutoDevopsHelper
def show_auto_devops_callout?(project) def show_auto_devops_callout?(project)
Feature.get(:auto_devops_banner_disabled).off? &&
show_callout?('auto_devops_settings_dismissed') && show_callout?('auto_devops_settings_dismissed') &&
can?(current_user, :admin_pipeline, project) && can?(current_user, :admin_pipeline, project) &&
project.has_auto_devops_implicitly_disabled? project.has_auto_devops_implicitly_disabled? &&
!project.repository.gitlab_ci_yml &&
project.ci_services.active.none?
end end
end end
...@@ -77,4 +77,8 @@ module BoardsHelper ...@@ -77,4 +77,8 @@ module BoardsHelper
'max-select': dropdown_options[:data][:'max-select'] 'max-select': dropdown_options[:data][:'max-select']
} }
end end
def boards_link_text
_("Board")
end
end end
...@@ -30,7 +30,7 @@ module BuildsHelper ...@@ -30,7 +30,7 @@ module BuildsHelper
def build_failed_issue_options def build_failed_issue_options
{ {
title: "Build Failed ##{@build.id}", title: "Job Failed ##{@build.id}",
description: project_job_url(@project, @build) description: project_job_url(@project, @build)
} }
end end
......
...@@ -21,7 +21,7 @@ module GroupsHelper ...@@ -21,7 +21,7 @@ module GroupsHelper
group.ancestors.reverse.each_with_index do |parent, index| group.ancestors.reverse.each_with_index do |parent, index|
if index > 0 if index > 0
add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true), location: :before) add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before)
else else
full_title += breadcrumb_list_item group_title_link(parent, hidable: false) full_title += breadcrumb_list_item group_title_link(parent, hidable: false)
end end
...@@ -85,8 +85,8 @@ module GroupsHelper ...@@ -85,8 +85,8 @@ module GroupsHelper
private private
def group_title_link(group, hidable: false, show_avatar: false) def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
link_to(group_path(group), class: "group-path breadcrumb-item-text js-breadcrumb-item-text #{'hidable' if hidable}") do link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
output = output =
if (group.try(:avatar_url) || show_avatar) && !Rails.env.test? if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15) image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15)
......
...@@ -213,7 +213,6 @@ module IssuablesHelper ...@@ -213,7 +213,6 @@ module IssuablesHelper
canUpdate: can?(current_user, :update_issue, issuable), canUpdate: can?(current_user, :update_issue, issuable),
canDestroy: can?(current_user, :destroy_issue, issuable), canDestroy: can?(current_user, :destroy_issue, issuable),
issuableRef: issuable.to_reference, issuableRef: issuable.to_reference,
isConfidential: issuable.confidential,
markdownPreviewPath: preview_markdown_path(@project), markdownPreviewPath: preview_markdown_path(@project),
markdownDocsPath: help_page_path('user/markdown'), markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable), issuableTemplates: issuable_templates(issuable),
......
...@@ -40,6 +40,10 @@ module PreferencesHelper ...@@ -40,6 +40,10 @@ module PreferencesHelper
] ]
end end
def user_application_theme
@user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class
end
def user_color_scheme def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class Gitlab::ColorSchemes.for_user(current_user).css_class
end end
......
...@@ -137,15 +137,7 @@ module ProjectsHelper ...@@ -137,15 +137,7 @@ module ProjectsHelper
end end
def last_push_event def last_push_event
return unless current_user current_user&.recent_push(@project)
return current_user.recent_push unless @project
project_ids = [@project.id]
if fork = current_user.fork_of(@project)
project_ids << fork.id
end
current_user.recent_push(project_ids)
end end
def project_feature_access_select(field) def project_feature_access_select(field)
...@@ -328,7 +320,7 @@ module ProjectsHelper ...@@ -328,7 +320,7 @@ module ProjectsHelper
def git_user_name def git_user_name
if current_user if current_user
current_user.name current_user.name.gsub('"', '\"')
else else
_("Your name") _("Your name")
end end
......
...@@ -119,8 +119,4 @@ module TabHelper ...@@ -119,8 +119,4 @@ module TabHelper
'active' if current_controller?('oauth/applications') 'active' if current_controller?('oauth/applications')
end end
def sidebar_link(href, title: nil, css: nil, &block)
link_to capture(&block), href, title: (title if collapsed_sidebar?), class: css, aria: { label: title }
end
end end
...@@ -137,11 +137,11 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -137,11 +137,11 @@ class ApplicationSetting < ActiveRecord::Base
validates :housekeeping_full_repack_period, validates :housekeeping_full_repack_period,
presence: true, presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_incremental_repack_period } numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_incremental_repack_period }
validates :housekeeping_gc_period, validates :housekeeping_gc_period,
presence: true, presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period } numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_full_repack_period }
validates :terminal_max_session_time, validates :terminal_max_session_time,
presence: true, presence: true,
...@@ -247,7 +247,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -247,7 +247,7 @@ class ApplicationSetting < ActiveRecord::Base
housekeeping_full_repack_period: 50, housekeeping_full_repack_period: 50,
housekeeping_gc_period: 200, housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10, housekeeping_incremental_repack_period: 10,
import_sources: Gitlab::ImportSources.values, import_sources: Settings.gitlab['import_sources'],
koding_enabled: false, koding_enabled: false,
koding_url: nil, koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
......
...@@ -13,7 +13,7 @@ module BlobViewer ...@@ -13,7 +13,7 @@ module BlobViewer
prepare! prepare!
@validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data) @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data)
end end
def valid? def valid?
......
...@@ -446,8 +446,8 @@ module Ci ...@@ -446,8 +446,8 @@ module Ci
return unless trace return unless trace
trace = trace.dup trace = trace.dup
Ci::MaskSecret.mask!(trace, project.runners_token) if project Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
Ci::MaskSecret.mask!(trace, token) Gitlab::Ci::MaskSecret.mask!(trace, token)
trace trace
end end
......
module Ci module Ci
class GroupVariable < ActiveRecord::Base class GroupVariable < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include HasVariable include HasVariable
include Presentable include Presentable
......
module Ci module Ci
class Pipeline < ActiveRecord::Base class Pipeline < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include HasStatus include HasStatus
include Importable include Importable
include AfterCommitQueue include AfterCommitQueue
...@@ -336,8 +336,8 @@ module Ci ...@@ -336,8 +336,8 @@ module Ci
return @config_processor if defined?(@config_processor) return @config_processor if defined?(@config_processor)
@config_processor ||= begin @config_processor ||= begin
Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path) Gitlab::Ci::YamlProcessor.new(ci_yaml_file, project.full_path)
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e
self.yaml_errors = e.message self.yaml_errors = e.message
nil nil
rescue rescue
...@@ -453,6 +453,10 @@ module Ci ...@@ -453,6 +453,10 @@ module Ci
.fabricate! .fabricate!
end end
def latest_builds_with_artifacts
@latest_builds_with_artifacts ||= builds.latest.with_artifacts
end
private private
def ci_yaml_from_repo def ci_yaml_from_repo
......
module Ci module Ci
class PipelineSchedule < ActiveRecord::Base class PipelineSchedule < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include Importable include Importable
acts_as_paranoid acts_as_paranoid
......
module Ci module Ci
class PipelineScheduleVariable < ActiveRecord::Base class PipelineScheduleVariable < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include HasVariable include HasVariable
belongs_to :pipeline_schedule belongs_to :pipeline_schedule
......
module Ci module Ci
class PipelineVariable < ActiveRecord::Base class PipelineVariable < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include HasVariable include HasVariable
belongs_to :pipeline belongs_to :pipeline
......
module Ci module Ci
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour ONLINE_CONTACT_TIMEOUT = 1.hour
......
module Ci module Ci
class RunnerProject < ActiveRecord::Base class RunnerProject < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
belongs_to :runner belongs_to :runner
belongs_to :project belongs_to :project
......
module Ci module Ci
class Stage < ActiveRecord::Base class Stage < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include Importable include Importable
include HasStatus include HasStatus
include Gitlab::OptimisticLocking include Gitlab::OptimisticLocking
......
module Ci module Ci
class Trigger < ActiveRecord::Base class Trigger < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
acts_as_paranoid acts_as_paranoid
......
module Ci module Ci
class TriggerRequest < ActiveRecord::Base class TriggerRequest < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
belongs_to :trigger belongs_to :trigger
belongs_to :pipeline, foreign_key: :commit_id belongs_to :pipeline, foreign_key: :commit_id
......
module Ci module Ci
class Variable < ActiveRecord::Base class Variable < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include HasVariable include HasVariable
include Presentable include Presentable
......
...@@ -28,10 +28,4 @@ class DeployKey < Key ...@@ -28,10 +28,4 @@ class DeployKey < Key
def can_push_to?(project) def can_push_to?(project)
can_push? && has_access_to?(project) can_push? && has_access_to?(project)
end end
private
# we don't want to notify the user for deploy keys
def notify_user
end
end end
...@@ -6,7 +6,10 @@ class Environment < ActiveRecord::Base ...@@ -6,7 +6,10 @@ class Environment < ActiveRecord::Base
belongs_to :project, required: true, validate: true belongs_to :project, required: true, validate: true
has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :deployments,
-> (env) { where(project_id: env.project_id) },
dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment' has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment'
before_validation :nullify_external_url before_validation :nullify_external_url
......
...@@ -49,7 +49,7 @@ class Event < ActiveRecord::Base ...@@ -49,7 +49,7 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :project belongs_to :project
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :push_event_payload, foreign_key: :event_id has_one :push_event_payload
# Callbacks # Callbacks
after_create :reset_project_activity after_create :reset_project_activity
...@@ -241,13 +241,7 @@ class Event < ActiveRecord::Base ...@@ -241,13 +241,7 @@ class Event < ActiveRecord::Base
def action_name def action_name
if push? if push?
if new_ref? push_action_name
"pushed new"
elsif rm_ref?
"deleted"
else
"pushed to"
end
elsif closed? elsif closed?
"closed" "closed"
elsif merged? elsif merged?
...@@ -263,11 +257,7 @@ class Event < ActiveRecord::Base ...@@ -263,11 +257,7 @@ class Event < ActiveRecord::Base
elsif commented? elsif commented?
"commented on" "commented on"
elsif created_project? elsif created_project?
if project.external_import? created_project_action_name
"imported"
else
"created"
end
else else
"opened" "opened"
end end
...@@ -360,6 +350,24 @@ class Event < ActiveRecord::Base ...@@ -360,6 +350,24 @@ class Event < ActiveRecord::Base
private private
def push_action_name
if new_ref?
"pushed new"
elsif rm_ref?
"deleted"
else
"pushed to"
end
end
def created_project_action_name
if project.external_import?
"imported"
else
"created"
end
end
def recent_update? def recent_update?
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
end end
......
...@@ -36,7 +36,6 @@ class GpgKey < ActiveRecord::Base ...@@ -36,7 +36,6 @@ class GpgKey < ActiveRecord::Base
before_validation :extract_fingerprint, :extract_primary_keyid before_validation :extract_fingerprint, :extract_primary_keyid
after_commit :update_invalid_gpg_signatures, on: :create after_commit :update_invalid_gpg_signatures, on: :create
after_commit :notify_user, on: :create
def primary_keyid def primary_keyid
super&.upcase super&.upcase
...@@ -107,8 +106,4 @@ class GpgKey < ActiveRecord::Base ...@@ -107,8 +106,4 @@ class GpgKey < ActiveRecord::Base
# only allows one key # only allows one key
self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first
end end
def notify_user
NotificationService.new.new_gpg_key(self)
end
end end
class GpgSignature < ActiveRecord::Base class GpgSignature < ActiveRecord::Base
include ShaAttribute include ShaAttribute
include IgnorableColumn
ignore_column :valid_signature
sha_attribute :commit_sha sha_attribute :commit_sha
sha_attribute :gpg_key_primary_keyid sha_attribute :gpg_key_primary_keyid
......
...@@ -30,9 +30,6 @@ class Issue < ActiveRecord::Base ...@@ -30,9 +30,6 @@ class Issue < ActiveRecord::Base
has_many :issue_assignees has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees has_many :assignees, class_name: "User", through: :issue_assignees
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
validates :project, presence: true validates :project, presence: true
scope :in_projects, ->(project_ids) { where(project_id: project_ids) } scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
......
...@@ -28,7 +28,6 @@ class Key < ActiveRecord::Base ...@@ -28,7 +28,6 @@ class Key < ActiveRecord::Base
delegate :name, :email, to: :user, prefix: true delegate :name, :email, to: :user, prefix: true
after_commit :add_to_shell, on: :create after_commit :add_to_shell, on: :create
after_commit :notify_user, on: :create
after_create :post_create_hook after_create :post_create_hook
after_commit :remove_from_shell, on: :destroy after_commit :remove_from_shell, on: :destroy
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
...@@ -118,8 +117,4 @@ class Key < ActiveRecord::Base ...@@ -118,8 +117,4 @@ class Key < ActiveRecord::Base
"type is forbidden. Must be #{allowed_types}" "type is forbidden. Must be #{allowed_types}"
end end
def notify_user
NotificationService.new.new_key(self)
end
end end
...@@ -127,7 +127,12 @@ class Label < ActiveRecord::Base ...@@ -127,7 +127,12 @@ class Label < ActiveRecord::Base
end end
def priority(project) def priority(project)
priorities.find_by(project: project).try(:priority) priority = if priorities.loaded?
priorities.first { |p| p.project == project }
else
priorities.find_by(project: project)
end
priority.try(:priority)
end end
def template? def template?
......
...@@ -231,6 +231,13 @@ class Namespace < ActiveRecord::Base ...@@ -231,6 +231,13 @@ class Namespace < ActiveRecord::Base
end end
def force_share_with_group_lock_on_descendants def force_share_with_group_lock_on_descendants
descendants.update_all(share_with_group_lock: true) return unless Group.supports_nested_groups?
# We can't use `descendants.update_all` since Rails will throw away the WITH
# RECURSIVE statement. We also can't use WHERE EXISTS since we can't use
# different table aliases, hence we're just using WHERE IN. Since we have a
# maximum of 20 nested groups this should be fine.
Namespace.where(id: descendants.select(:id))
.update_all(share_with_group_lock: true)
end end
end end
...@@ -28,7 +28,7 @@ class PersonalAccessToken < ActiveRecord::Base ...@@ -28,7 +28,7 @@ class PersonalAccessToken < ActiveRecord::Base
protected protected
def validate_scopes def validate_scopes
unless scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) } unless revoked || scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) }
errors.add :scopes, "can only contain available scopes" errors.add :scopes, "can only contain available scopes"
end end
end end
......
...@@ -161,7 +161,7 @@ class Project < ActiveRecord::Base ...@@ -161,7 +161,7 @@ class Project < ActiveRecord::Base
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics' has_one :statistics, class_name: 'ProjectStatistics'
# Container repositories need to remove data from the container registry, # Container repositories need to remove data from the container registry,
...@@ -190,7 +190,7 @@ class Project < ActiveRecord::Base ...@@ -190,7 +190,7 @@ class Project < ActiveRecord::Base
has_one :auto_devops, class_name: 'ProjectAutoDevops' has_one :auto_devops, class_name: 'ProjectAutoDevops'
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops accepts_nested_attributes_for :auto_devops
...@@ -1163,6 +1163,23 @@ class Project < ActiveRecord::Base ...@@ -1163,6 +1163,23 @@ class Project < ActiveRecord::Base
pipelines.order(id: :desc).find_by(sha: sha, ref: ref) pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end end
def latest_successful_pipeline_for_default_branch
if defined?(@latest_successful_pipeline_for_default_branch)
return @latest_successful_pipeline_for_default_branch
end
@latest_successful_pipeline_for_default_branch =
pipelines.latest_successful_for(default_branch)
end
def latest_successful_pipeline_for(ref = nil)
if ref && ref != default_branch
pipelines.latest_successful_for(ref)
else
latest_successful_pipeline_for_default_branch
end
end
def enable_ci def enable_ci
project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end end
......
class ProjectAutoDevops < ActiveRecord::Base class ProjectAutoDevops < ActiveRecord::Base
belongs_to :project belongs_to :project
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
def variables def variables
......
...@@ -41,6 +41,8 @@ class ProjectFeature < ActiveRecord::Base ...@@ -41,6 +41,8 @@ class ProjectFeature < ActiveRecord::Base
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) } belongs_to :project, -> { unscope(where: :pending_delete) }
validates :project, presence: true
validate :repository_children_level validate :repository_children_level
default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :builds_access_level, value: ENABLED, allows_nil: false
......
...@@ -80,6 +80,6 @@ class PipelinesEmailService < Service ...@@ -80,6 +80,6 @@ class PipelinesEmailService < Service
end end
def retrieve_recipients(data) def retrieve_recipients(data)
recipients.to_s.split(',').reject(&:blank?) recipients.to_s.split(/[,(?:\r?\n) ]+/).reject(&:empty?)
end end
end end
...@@ -30,6 +30,44 @@ class PushEvent < Event ...@@ -30,6 +30,44 @@ class PushEvent < Event
delegate :commit_count, to: :push_event_payload delegate :commit_count, to: :push_event_payload
alias_method :commits_count, :commit_count alias_method :commits_count, :commit_count
# Returns events of pushes that either pushed to an existing ref or created a
# new one.
def self.created_or_pushed
actions = [
PushEventPayload.actions[:pushed],
PushEventPayload.actions[:created]
]
joins(:push_event_payload)
.where(push_event_payloads: { action: actions })
end
# Returns events of pushes to a branch.
def self.branch_events
ref_type = PushEventPayload.ref_types[:branch]
joins(:push_event_payload)
.where(push_event_payloads: { ref_type: ref_type })
end
# Returns PushEvent instances for which no merge requests have been created.
def self.without_existing_merge_requests
existing_mrs = MergeRequest.except(:order)
.select(1)
.where('merge_requests.source_project_id = events.project_id')
.where('merge_requests.source_branch = push_event_payloads.ref')
# For reasons unknown the use of #eager_load will result in the
# "push_event_payload" association not being set. Because of this we're
# using "joins" here, which does mean an additional query needs to be
# executed in order to retrieve the "push_event_association" when the
# returned PushEvent is used.
joins(:push_event_payload)
.where('NOT EXISTS (?)', existing_mrs)
.created_or_pushed
.branch_events
end
def self.sti_name def self.sti_name
PUSHED PUSHED
end end
......
...@@ -90,6 +90,12 @@ class Repository ...@@ -90,6 +90,12 @@ class Repository
) )
end end
# we need to have this method here because it is not cached in ::Git and
# the method is called multiple times for every request
def has_visible_content?
branch_count > 0
end
def inspect def inspect
"#<#{self.class.name}:#{@disk_path}>" "#<#{self.class.name}:#{@disk_path}>"
end end
...@@ -166,7 +172,7 @@ class Repository ...@@ -166,7 +172,7 @@ class Repository
end end
def add_branch(user, branch_name, ref) def add_branch(user, branch_name, ref)
branch = raw_repository.add_branch(branch_name, committer: user, target: ref) branch = raw_repository.add_branch(branch_name, user: user, target: ref)
after_create_branch after_create_branch
...@@ -176,7 +182,7 @@ class Repository ...@@ -176,7 +182,7 @@ class Repository
end end
def add_tag(user, tag_name, target, message = nil) def add_tag(user, tag_name, target, message = nil)
raw_repository.add_tag(tag_name, committer: user, target: target, message: message) raw_repository.add_tag(tag_name, user: user, target: target, message: message)
rescue Gitlab::Git::Repository::InvalidRef rescue Gitlab::Git::Repository::InvalidRef
false false
end end
...@@ -184,7 +190,7 @@ class Repository ...@@ -184,7 +190,7 @@ class Repository
def rm_branch(user, branch_name) def rm_branch(user, branch_name)
before_remove_branch before_remove_branch
raw_repository.rm_branch(branch_name, committer: user) raw_repository.rm_branch(branch_name, user: user)
after_remove_branch after_remove_branch
true true
...@@ -193,7 +199,7 @@ class Repository ...@@ -193,7 +199,7 @@ class Repository
def rm_tag(user, tag_name) def rm_tag(user, tag_name)
before_remove_tag before_remove_tag
raw_repository.rm_tag(tag_name, committer: user) raw_repository.rm_tag(tag_name, user: user)
after_remove_tag after_remove_tag
true true
......
...@@ -35,6 +35,7 @@ class User < ActiveRecord::Base ...@@ -35,6 +35,7 @@ class User < ActiveRecord::Base
default_value_for :project_view, :files default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false default_value_for :notified_of_own_activity, false
default_value_for :preferred_language, I18n.default_locale default_value_for :preferred_language, I18n.default_locale
default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret, attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base, key: Gitlab::Application.secrets.otp_key_base,
...@@ -650,20 +651,13 @@ class User < ActiveRecord::Base ...@@ -650,20 +651,13 @@ class User < ActiveRecord::Base
@personal_projects_count ||= personal_projects.count @personal_projects_count ||= personal_projects.count
end end
def recent_push(project_ids = nil) def recent_push(project = nil)
# Get push events not earlier than 2 hours ago service = Users::LastPushEventService.new(self)
events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
events = events.where(project_id: project_ids) if project_ids
# Use the latest event that has not been pushed or merged recently if project
events.includes(:project).recent.find do |event| service.last_event_for_project(project)
next unless event.project.repository.branch_exists?(event.branch_name) else
service.last_event_for_user
merge_requests = MergeRequest.where("created_at >= ?", event.created_at)
.where(source_project_id: event.project.id,
source_branch: event.branch_name)
merge_requests.empty?
end end
end end
......
...@@ -32,8 +32,8 @@ class BuildDetailsEntity < JobEntity ...@@ -32,8 +32,8 @@ class BuildDetailsEntity < JobEntity
private private
def build_failed_issue_options def build_failed_issue_options
{ title: "Build Failed ##{build.id}", { title: "Job Failed ##{build.id}",
description: project_job_path(project, build) } description: "Job [##{build.id}](#{project_job_path(project, build)}) failed for #{build.sha}:\n" }
end end
def current_user def current_user
......
...@@ -14,7 +14,7 @@ module Ci ...@@ -14,7 +14,7 @@ module Ci
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref]) pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
.execute(:trigger, ignore_skip_ci: true) do |pipeline| .execute(:trigger, ignore_skip_ci: true) do |pipeline|
trigger.trigger_requests.create!(pipeline: pipeline) pipeline.trigger_requests.create!(trigger: trigger)
create_pipeline_variables!(pipeline) create_pipeline_variables!(pipeline)
end end
......
module DeployKeys
class CreateService < Keys::BaseService
def execute
DeployKey.create(params.merge(user: user))
end
end
end
...@@ -74,12 +74,19 @@ class EventCreateService ...@@ -74,12 +74,19 @@ class EventCreateService
# We're using an explicit transaction here so that any errors that may occur # We're using an explicit transaction here so that any errors that may occur
# when creating push payload data will result in the event creation being # when creating push payload data will result in the event creation being
# rolled back as well. # rolled back as well.
Event.transaction do event = Event.transaction do
event = create_event(project, current_user, Event::PUSHED) new_event = create_event(project, current_user, Event::PUSHED)
PushEventPayloadService.new(event, push_data).execute PushEventPayloadService
.new(new_event, push_data)
.execute
new_event
end end
Users::LastPushEventService.new(current_user)
.cache_last_push_event(event)
Users::ActivityService.new(current_user, 'push').execute Users::ActivityService.new(current_user, 'push').execute
end end
......
module GpgKeys
class CreateService < Keys::BaseService
def execute
key = user.gpg_keys.create(params)
notification_service.new_gpg_key(key) if key.persisted?
key
end
end
end
module Keys
class BaseService
attr_accessor :user, :params
def initialize(user, params)
@user, @params = user, params
end
def notification_service
NotificationService.new
end
end
end
module Keys
class CreateService < ::Keys::BaseService
def execute
key = user.keys.create(params)
notification_service.new_key(key) if key.persisted?
key
end
end
end
...@@ -24,7 +24,10 @@ module Projects ...@@ -24,7 +24,10 @@ module Projects
success success
else else
error('Project could not be updated!') model_errors = project.errors.full_messages.to_sentence
error_message = model_errors.presence || 'Project could not be updated!'
error(error_message)
end end
end end
......
module Users
# Service class for caching and retrieving the last push event of a user.
class LastPushEventService
EXPIRATION = 2.hours
def initialize(user)
@user = user
end
# Caches the given push event for the current user in the Rails cache.
#
# event - An instance of PushEvent to cache.
def cache_last_push_event(event)
keys = [
project_cache_key(event.project),
user_cache_key
]
if event.project.forked?
keys << project_cache_key(event.project.forked_from_project)
end
keys.each { |key| set_key(key, event.id) }
end
# Returns the last PushEvent for the current user.
#
# This method will return nil if no event was found.
def last_event_for_user
find_cached_event(user_cache_key)
end
# Returns the last PushEvent for the current user and the given project.
#
# project - An instance of Project for which to retrieve the PushEvent.
#
# This method will return nil if no event was found.
def last_event_for_project(project)
find_cached_event(project_cache_key(project))
end
def find_cached_event(cache_key)
event_id = get_key(cache_key)
return unless event_id
unless (event = find_event_in_database(event_id))
# We don't want to keep querying the same data over and over when a
# merge request has been created, thus we remove the key if no event
# (meaning an MR was created) is returned.
Rails.cache.delete(cache_key)
end
event
end
private
def find_event_in_database(id)
PushEvent
.without_existing_merge_requests
.find_by(id: id)
end
def user_cache_key
"last-push-event/#{@user.id}"
end
def project_cache_key(project)
"last-push-event/#{@user.id}/#{project.id}"
end
def get_key(key)
Rails.cache.read(key, raw: true)
end
def set_key(key, value)
# We're using raw values here since this takes up less space and we don't
# store complex objects.
Rails.cache.write(key, value, raw: true, expires_in: EXPIRATION)
end
end
end
...@@ -111,6 +111,11 @@ ...@@ -111,6 +111,11 @@
GitLab API GitLab API
%span.pull-right %span.pull-right
= API::API::version = API::API::version
- if Gitlab.config.pages.enabled
%p
GitLab Pages
%span.pull-right
= Gitlab::Pages::VERSION
%p %p
Git Git
%span.pull-right %span.pull-right
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
%span.light %span.light
- has_icon = provider_has_icon?(provider) - has_icon = provider_has_icon?(provider)
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}" = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}"
%fieldset.prepend-top-10 %fieldset.prepend-top-10.checkbox.remember-me
= check_box_tag :remember_me %label
= label_tag :remember_me, 'Remember me' = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
%span
Remember me
!!! 5 !!! 5
%html{ lang: I18n.locale, class: page_class } %html{ lang: I18n.locale, class: page_class }
= render "layouts/head" = render "layouts/head"
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form = render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar' = render 'peek/bar'
= render "layouts/header/default" = render "layouts/header/default"
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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