Commit 2fc79403 authored by Jarka Kadlecova's avatar Jarka Kadlecova

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into 3551-epic-issues

# Conflicts:
#	ee/app/assets/javascripts/epics/epic_show/components/epic_show_app.vue
parents 8bf82c8d 7362ea9a
...@@ -478,6 +478,7 @@ db:migrate:reset-mysql: ...@@ -478,6 +478,7 @@ db:migrate:reset-mysql:
stage: test stage: test
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
CREATE_DB_USER: "true"
script: script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ee.git v9.3.0-ee - git fetch https://gitlab.com/gitlab-org/gitlab-ee.git v9.3.0-ee
- git checkout -f FETCH_HEAD - git checkout -f FETCH_HEAD
...@@ -522,6 +523,7 @@ db:rollback-mysql: ...@@ -522,6 +523,7 @@ db:rollback-mysql:
variables: variables:
SIZE: "1" SIZE: "1"
SETUP_DB: "false" SETUP_DB: "false"
CREATE_DB_USER: "true"
script: script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git - git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git /home/git/repositories/gitlab-org/gitlab-test.git
...@@ -557,7 +559,6 @@ gitlab:assets:compile: ...@@ -557,7 +559,6 @@ gitlab:assets:compile:
NODE_ENV: "production" NODE_ENV: "production"
RAILS_ENV: "production" RAILS_ENV: "production"
SETUP_DB: "false" SETUP_DB: "false"
USE_DB: "false"
SKIP_STORAGE_VALIDATION: "true" SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true" WEBPACK_REPORT: "true"
NO_COMPRESSION: "true" NO_COMPRESSION: "true"
...@@ -612,6 +613,16 @@ codequality: ...@@ -612,6 +613,16 @@ codequality:
artifacts: artifacts:
paths: [codeclimate.json] paths: [codeclimate.json]
qa:internal:
stage: test
variables:
SETUP_DB: "false"
services: []
script:
- cd qa/
- bundle install
- bundle exec rspec
coverage: coverage:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 10.1.3 (2017-11-10)
- [FIXED] Fix: Failed to rebase MR from forked repo.
## 10.1.2 (2017-11-08)
- [SECURITY] Fix vulnerability that could allow any user of a Geo instance to clone any repository on the secondary instance.
- [SECURITY] Geo JSON web tokens now expire after two minutes to reduce risk of compromise.
## 10.1.1 (2017-10-31) ## 10.1.1 (2017-10-31)
- No changes.
- [FIXED] Fix LDAP group sync for nested groups e.g. when base has uppercase or extraneous spaces. !3217 - [FIXED] Fix LDAP group sync for nested groups e.g. when base has uppercase or extraneous spaces. !3217
- [FIXED] Geo: read-only safeguards was not working on Secondary node. !3227 - [FIXED] Geo: read-only safeguards was not working on Secondary node. !3227
- [FIXED] fix height of rebase and approve buttons. - [FIXED] fix height of rebase and approve buttons.
......
...@@ -2,6 +2,28 @@ ...@@ -2,6 +2,28 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.1.3 (2017-11-10)
- [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
- [FIXED] Fix cancel button not working while uploading on the new issue page. !15137
- [FIXED] Fix webhooks recent deliveries. !15146 (Alexander Randa (@randaalex))
- [FIXED] Fix issues with forked projects of which the source was deleted. !15150
- [FIXED] Fix GPG signature popup info in Safari and Firefox. !15228
- [FIXED] Make sure group and project creation is blocked for new users that are external by default.
- [FIXED] Fix arguments Import/Export error importing project merge requests.
- [FIXED] Fix diff parser so it tolerates to diff special markers in the content.
- [FIXED] Fix a migration that adds merge_requests_ff_only_enabled column to MR table.
- [FIXED] Render 404 when polling commit notes without having permissions.
- [FIXED] Show error message when fast-forward merge is not possible.
- [FIXED] Avoid regenerating the ref path for the environment.
- [PERFORMANCE] Remove Filesystem check metrics that use too much CPU to handle requests.
## 10.1.2 (2017-11-08)
- [SECURITY] Add X-Content-Type-Options header in API responses to make it more difficult to find other vulnerabilities.
- [SECURITY] Properly translate IP addresses written in decimal, octal, or other formats in SSRF protections in project imports.
- [FIXED] Fix TRIGGER checks for MySQL.
## 10.1.1 (2017-10-31) ## 10.1.1 (2017-10-31)
- [FIXED] Auto Devops kubernetes default namespace is now correctly built out of gitlab project group-name. !14642 (Mircea Danila Dumitrescu) - [FIXED] Auto Devops kubernetes default namespace is now correctly built out of gitlab project group-name. !14642 (Mircea Danila Dumitrescu)
......
/* eslint-disable no-new*/ /* eslint-disable no-new*/
import './smart_interval'; import axios from 'axios';
import SmartInterval from '~/smart_interval';
import { parseSeconds, stringifyTime } from './lib/utils/pretty_time'; import { parseSeconds, stringifyTime } from './lib/utils/pretty_time';
const healthyClass = 'geo-node-healthy'; const healthyClass = 'geo-node-healthy';
...@@ -31,7 +32,7 @@ class GeoNodeStatus { ...@@ -31,7 +32,7 @@ class GeoNodeStatus {
this.$advancedStatus = $('.js-advanced-geo-node-status-toggler', this.$status); this.$advancedStatus = $('.js-advanced-geo-node-status-toggler', this.$status);
this.$advancedStatus.on('click', GeoNodeStatus.toggleShowAdvancedStatus); this.$advancedStatus.on('click', GeoNodeStatus.toggleShowAdvancedStatus);
this.statusInterval = new gl.SmartInterval({ this.statusInterval = new SmartInterval({
callback: this.getStatus.bind(this), callback: this.getStatus.bind(this),
startingInterval: 30000, startingInterval: 30000,
maxInterval: 120000, maxInterval: 120000,
...@@ -59,78 +60,105 @@ class GeoNodeStatus { ...@@ -59,78 +60,105 @@ class GeoNodeStatus {
static formatCount(count) { static formatCount(count) {
if (count !== null) { if (count !== null) {
gl.text.addDelimiter(count); return gl.text.addDelimiter(count);
} }
return notAvailable; return notAvailable;
} }
getStatus() { getStatus() {
$.getJSON(this.endpoint, (status) => { return axios.get(this.endpoint)
this.setStatusIcon(status.healthy); .then((response) => {
this.setHealthStatus(status.healthy); this.handleStatus(response.data);
return response;
})
.catch((err) => {
this.handleError(err);
});
}
handleStatus(status) {
this.setStatusIcon(status.healthy);
this.setHealthStatus(status.healthy);
// Replication lag can be nil if the secondary isn't actually streaming // Replication lag can be nil if the secondary isn't actually streaming
if (status.db_replication_lag_seconds !== null && status.db_replication_lag_seconds >= 0) { if (status.db_replication_lag_seconds !== null && status.db_replication_lag_seconds >= 0) {
const parsedTime = parseSeconds(status.db_replication_lag_seconds, { const parsedTime = parseSeconds(status.db_replication_lag_seconds, {
hoursPerDay: 24, hoursPerDay: 24,
daysPerWeek: 7, daysPerWeek: 7,
}); });
this.$dbReplicationLag.text(stringifyTime(parsedTime)); this.$dbReplicationLag.text(stringifyTime(parsedTime));
} else { } else {
this.$dbReplicationLag.text('UNKNOWN'); this.$dbReplicationLag.text('UNKNOWN');
} }
const repoText = GeoNodeStatus.formatCountAndPercentage( const repoText = GeoNodeStatus.formatCountAndPercentage(
status.repositories_synced_count, status.repositories_synced_count,
status.repositories_count, status.repositories_count,
status.repositories_synced_in_percentage); status.repositories_synced_in_percentage);
const repoFailedText = GeoNodeStatus.formatCount(status.repositories_failed_count); const repoFailedText = GeoNodeStatus.formatCount(status.repositories_failed_count);
const lfsText = GeoNodeStatus.formatCountAndPercentage( const lfsText = GeoNodeStatus.formatCountAndPercentage(
status.lfs_objects_synced_count, status.lfs_objects_synced_count,
status.lfs_objects_count, status.lfs_objects_count,
status.lfs_objects_synced_in_percentage); status.lfs_objects_synced_in_percentage);
const lfsFailedText = GeoNodeStatus.formatCount(status.lfs_objects_failed_count); const lfsFailedText = GeoNodeStatus.formatCount(status.lfs_objects_failed_count);
const attachmentText = GeoNodeStatus.formatCountAndPercentage( const attachmentText = GeoNodeStatus.formatCountAndPercentage(
status.attachments_synced_count, status.attachments_synced_count,
status.attachments_count, status.attachments_count,
status.attachments_synced_in_percentage); status.attachments_synced_in_percentage);
const attachmentFailedText = GeoNodeStatus.formatCount(status.attachments_failed_count); const attachmentFailedText = GeoNodeStatus.formatCount(status.attachments_failed_count);
this.$repositoriesSynced.text(repoText); this.$repositoriesSynced.text(repoText);
this.$repositoriesFailed.text(repoFailedText); this.$repositoriesFailed.text(repoFailedText);
this.$lfsObjectsSynced.text(lfsText); this.$lfsObjectsSynced.text(lfsText);
this.$lfsObjectsFailed.text(lfsFailedText); this.$lfsObjectsFailed.text(lfsFailedText);
this.$attachmentsSynced.text(attachmentText); this.$attachmentsSynced.text(attachmentText);
this.$attachmentsFailed.text(attachmentFailedText); this.$attachmentsFailed.text(attachmentFailedText);
let eventDate = notAvailable; let eventDate = notAvailable;
let cursorDate = notAvailable; let cursorDate = notAvailable;
let lastEventSeen = notAvailable;
if (status.last_event_timestamp !== null && status.last_event_timestamp > 0) { let lastCursorEvent = notAvailable;
eventDate = gl.utils.formatDate(new Date(status.last_event_timestamp * 1000));
} if (status.last_event_timestamp !== null && status.last_event_timestamp > 0) {
eventDate = gl.utils.formatDate(new Date(status.last_event_timestamp * 1000));
if (status.cursor_last_event_timestamp !== null && status.cursor_last_event_timestamp > 0) { }
cursorDate = gl.utils.formatDate(new Date(status.cursor_last_event_timestamp * 1000));
} if (status.cursor_last_event_timestamp !== null && status.cursor_last_event_timestamp > 0) {
cursorDate = gl.utils.formatDate(new Date(status.cursor_last_event_timestamp * 1000));
this.$lastEventSeen.text(`${status.last_event_id} (${eventDate})`); }
this.$lastCursorEvent.text(`${status.cursor_last_event_id} (${cursorDate})`);
if (status.health === 'Healthy') { if (status.last_event_id !== null) {
this.$health.text(''); lastEventSeen = `${status.last_event_id} (${eventDate})`;
} else { }
const strippedData = $('<div>').html(`${status.health}`).text();
this.$health.html(`<code class="geo-health">${strippedData}</code>`); if (status.cursor_last_event_id !== null) {
} lastCursorEvent = `${status.cursor_last_event_id} (${cursorDate})`;
}
this.$status.show();
}); this.$lastEventSeen.text(lastEventSeen);
this.$lastCursorEvent.text(lastCursorEvent);
if (status.health === 'Healthy') {
this.$health.text('');
} else {
const strippedData = $('<div>').html(`${status.health}`).text();
this.$health.html(`<code class="geo-health">${strippedData}</code>`);
}
this.$status.show();
}
handleError(err) {
this.setStatusIcon(false);
this.setHealthStatus(false);
this.$health.html(`<code class="geo-health">${err}</code>`);
this.$status.show();
} }
setStatusIcon(healthy) { setStatusIcon(healthy) {
......
...@@ -331,7 +331,7 @@ GitLabDropdown = (function() { ...@@ -331,7 +331,7 @@ GitLabDropdown = (function() {
if (_this.dropdown.find('.dropdown-toggle-page').length) { if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector; selector = ".dropdown-page-one " + selector;
} }
return $(selector); return $(selector, this.instance.dropdown);
}; };
})(this), })(this),
data: (function(_this) { data: (function(_this) {
......
...@@ -135,7 +135,6 @@ window.dateFormat = dateFormat; ...@@ -135,7 +135,6 @@ window.dateFormat = dateFormat;
* @param {Number} seconds * @param {Number} seconds
* @return {String} * @return {String}
*/ */
// eslint-disable-next-line import/prefer-default-export
export function timeIntervalInWords(intervalInSeconds) { export function timeIntervalInWords(intervalInSeconds) {
const secondsInteger = parseInt(intervalInSeconds, 10); const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60); const minutes = Math.floor(secondsInteger / 60);
...@@ -149,3 +148,17 @@ export function timeIntervalInWords(intervalInSeconds) { ...@@ -149,3 +148,17 @@ export function timeIntervalInWords(intervalInSeconds) {
} }
return text; return text;
} }
export function dateInWords(date, abbreviated = false) {
if (!date) return date;
const month = date.getMonth();
const year = date.getFullYear();
const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
return `${monthName} ${date.getDate()}, ${year}`;
}
...@@ -24,6 +24,10 @@ export function highCountTrim(count) { ...@@ -24,6 +24,10 @@ export function highCountTrim(count) {
return count > 99 ? '99+' : count; return count > 99 ? '99+' : count;
} }
export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
gl.text.randomString = function() { gl.text.randomString = function() {
return Math.random().toString(36).substring(7); return Math.random().toString(36).substring(7);
}; };
......
...@@ -66,8 +66,7 @@ ...@@ -66,8 +66,7 @@
<div class="ci-widget media"> <div class="ci-widget media">
<template v-if="hasCIError"> <template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10"> <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
<icon <icon name="status_failed" />
name="status_failed"/>
</div> </div>
<div class="media-body"> <div class="media-body">
Could not connect to the CI server. Please check your settings and try again Could not connect to the CI server. Please check your settings and try again
...@@ -86,7 +85,9 @@ ...@@ -86,7 +85,9 @@
class="pipeline-id"> class="pipeline-id">
#{{pipeline.id}} #{{pipeline.id}}
</a> </a>
{{pipeline.details.status.label}} for {{pipeline.details.status.label}} for
<a <a
:href="pipeline.commit.commit_path" :href="pipeline.commit.commit_path"
class="commit-sha js-commit-link"> class="commit-sha js-commit-link">
......
<script>
import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
export default {
name: 'datePicker',
props: {
label: {
type: String,
required: false,
default: 'Date picker',
},
selectedDate: {
type: Date,
required: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
},
methods: {
selected(dateText) {
this.$emit('newDateSelected', this.calendar.toString(dateText));
},
toggled() {
this.$emit('hidePicker');
},
},
mounted() {
this.calendar = new Pikaday({
field: this.$el.querySelector('.dropdown-menu-toggle'),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: this.$el,
defaultDate: this.selectedDate,
setDefaultDate: !!this.selectedDate,
minDate: this.minDate,
maxDate: this.maxDate,
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: this.selected.bind(this),
onClose: this.toggled.bind(this),
});
this.$el.append(this.calendar.el);
this.calendar.show();
},
beforeDestroy() {
this.calendar.destroy();
},
};
</script>
<template>
<div class="pikaday-container">
<div class="dropdown open">
<button
type="button"
class="dropdown-menu-toggle"
data-toggle="dropdown"
@click="toggled"
>
<span class="dropdown-toggle-text">
{{label}}
</span>
<i
class="fa fa-chevron-down"
aria-hidden="true"
>
</i>
</button>
</div>
</div>
</template>
<script>
export default {
name: 'collapsedCalendarIcon',
props: {
containerClass: {
type: String,
required: false,
default: '',
},
text: {
type: String,
required: false,
default: '',
},
showIcon: {
type: Boolean,
required: false,
default: true,
},
},
methods: {
click() {
this.$emit('click');
},
},
};
</script>
<template>
<div
:class="containerClass"
@click="click"
>
<i
v-if="showIcon"
class="fa fa-calendar"
aria-hidden="true"
>
</i>
<slot>
<span>
{{ text }}
</span>
</slot>
</div>
</template>
<script>
import { dateInWords } from '../../../lib/utils/datetime_utility';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
export default {
name: 'sidebarCollapsedGroupedDatePicker',
props: {
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
disableClickableIcons: {
type: Boolean,
required: false,
default: false,
},
},
components: {
toggleSidebar,
collapsedCalendarIcon,
},
computed: {
hasMinAndMaxDates() {
return this.minDate && this.maxDate;
},
hasNoMinAndMaxDates() {
return !this.minDate && !this.maxDate;
},
showMinDateBlock() {
return this.minDate || this.hasNoMinAndMaxDates;
},
showFromText() {
return !this.maxDate && this.minDate;
},
iconClass() {
const disabledClass = this.disableClickableIcons ? 'disabled' : '';
return `block sidebar-collapsed-icon calendar-icon ${disabledClass}`;
},
},
methods: {
toggleSidebar() {
this.$emit('toggleCollapse');
},
dateText(dateType = 'min') {
const date = this[`${dateType}Date`];
const dateWords = dateInWords(date, true);
const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
return date ? parsedDateWords : 'None';
},
},
};
</script>
<template>
<div class="block sidebar-grouped-item">
<div
v-if="showToggleSidebar"
class="issuable-sidebar-header"
>
<toggle-sidebar
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
<collapsed-calendar-icon
v-if="showMinDateBlock"
:container-class="iconClass"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
<span v-if="showFromText">From</span>
<span>{{ dateText('min') }}</span>
</span>
</collapsed-calendar-icon>
<div
v-if="hasMinAndMaxDates"
class="text-center sidebar-collapsed-divider"
>
-
</div>
<collapsed-calendar-icon
v-if="maxDate"
:container-class="iconClass"
:show-icon="!minDate"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
<span v-if="!minDate">Until</span>
<span>{{ dateText('max') }}</span>
</span>
</collapsed-calendar-icon>
</div>
</template>
<script>
import datePicker from '../pikaday.vue';
import loadingIcon from '../loading_icon.vue';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
import { dateInWords } from '../../../lib/utils/datetime_utility';
export default {
name: 'sidebarDatePicker',
props: {
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
editable: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: 'Date picker',
},
selectedDate: {
type: Date,
required: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
},
data() {
return {
editing: false,
};
},
components: {
datePicker,
toggleSidebar,
loadingIcon,
collapsedCalendarIcon,
},
computed: {
selectedAndEditable() {
return this.selectedDate && this.editable;
},
selectedDateWords() {
return dateInWords(this.selectedDate, true);
},
collapsedText() {
return this.selectedDateWords ? this.selectedDateWords : 'None';
},
},
methods: {
stopEditing() {
this.editing = false;
},
toggleDatePicker() {
this.editing = !this.editing;
},
newDateSelected(date = null) {
this.date = date;
this.editing = false;
this.$emit('saveDate', date);
},
toggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
</script>
<template>
<div class="block">
<div class="issuable-sidebar-header">
<toggle-sidebar
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
<collapsed-calendar-icon
class="sidebar-collapsed-icon"
:text="collapsedText"
/>
<div class="title">
{{ label }}
<loading-icon
v-if="isLoading"
:inline="true"
/>
<div class="pull-right">
<button
v-if="editable && !editing"
type="button"
class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action"
@click="toggleDatePicker"
>
Edit
</button>
<toggle-sidebar
v-if="showToggleSidebar"
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
</div>
<div class="value">
<date-picker
v-if="editing"
:selected-date="selectedDate"
:min-date="minDate"
:max-date="maxDate"
:label="label"
@newDateSelected="newDateSelected"
@hidePicker="stopEditing"
/>
<span
v-else
class="value-content"
>
<template v-if="selectedDate">
<strong>{{ selectedDateWords }}</strong>
<span
v-if="selectedAndEditable"
class="no-value"
>
-
<button
type="button"
class="btn-blank btn-link btn-secondary-hover-link"
@click="newDateSelected(null)"
>
remove
</button>
</span>
</template>
<span
v-else
class="no-value"
>
None
</span>
</span>
</div>
</div>
</template>
<script>
export default {
name: 'toggleSidebar',
props: {
collapsed: {
type: Boolean,
required: true,
},
},
methods: {
toggle() {
this.$emit('toggle');
},
},
};
</script>
<template>
<button
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
>
<i
aria-label="toggle collapse"
class="fa"
:class="{ 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed }"
></i>
</button>
</template>
...@@ -412,6 +412,7 @@ ...@@ -412,6 +412,7 @@
padding: 0; padding: 0;
background: transparent; background: transparent;
border: 0; border: 0;
border-radius: 0;
&:hover, &:hover,
&:active, &:active,
...@@ -421,3 +422,25 @@ ...@@ -421,3 +422,25 @@
box-shadow: none; box-shadow: none;
} }
} }
.btn-link.btn-secondary-hover-link {
color: $gl-text-color-secondary;
&:hover,
&:active,
&:focus {
color: $gl-link-color;
text-decoration: none;
}
}
.btn-link.btn-primary-hover-link {
color: inherit;
&:hover,
&:active,
&:focus {
color: $gl-link-color;
text-decoration: none;
}
}
...@@ -43,11 +43,13 @@ ...@@ -43,11 +43,13 @@
} }
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
cursor: pointer;
.btn { .btn {
background-color: $gray-light; background-color: $gray-light;
} }
&:not(.disabled) {
cursor: pointer;
}
} }
} }
...@@ -55,6 +57,10 @@ ...@@ -55,6 +57,10 @@
padding-right: 0; padding-right: 0;
z-index: 300; z-index: 300;
.btn-sidebar-action {
display: inline-flex;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper { &:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width; padding-right: $gutter_collapsed_width;
...@@ -136,3 +142,18 @@ ...@@ -136,3 +142,18 @@
.issuable-sidebar { .issuable-sidebar {
@include new-style-dropdown; @include new-style-dropdown;
} }
.pikaday-container {
.pika-single {
margin-top: 2px;
width: 250px;
}
.dropdown-menu-toggle {
line-height: 20px;
}
}
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
...@@ -284,10 +284,15 @@ ...@@ -284,10 +284,15 @@
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
} }
.no-value { .no-value,
.btn-secondary-hover-link {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
.btn-secondary-hover-link:hover {
color: $gl-link-color;
}
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
display: none; display: none;
} }
...@@ -353,7 +358,8 @@ ...@@ -353,7 +358,8 @@
.gutter-toggle { .gutter-toggle {
width: 100%; width: 100%;
margin-left: 0; margin-left: 0;
padding-left: 25px; padding-left: 0;
text-align: center;
} }
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
...@@ -367,7 +373,7 @@ ...@@ -367,7 +373,7 @@
fill: $issuable-sidebar-color; fill: $issuable-sidebar-color;
} }
&:hover, &:hover:not(.disabled),
&:hover .todo-undone { &:hover .todo-undone {
color: $gl-text-color; color: $gl-text-color;
...@@ -954,3 +960,21 @@ ...@@ -954,3 +960,21 @@
.add-issuable-form-actions { .add-issuable-form-actions {
margin-top: $gl-padding; margin-top: $gl-padding;
} }
.right-sidebar-collapsed {
.sidebar-grouped-item {
.sidebar-collapsed-icon {
margin-bottom: 0;
}
.sidebar-collapsed-divider {
line-height: 5px;
font-size: 12px;
color: $theme-gray-700;
+ .sidebar-collapsed-icon {
padding-top: 0;
}
}
}
}
...@@ -781,10 +781,9 @@ ...@@ -781,10 +781,9 @@
.code-quality-container { .code-quality-container {
border-top: 1px solid $gray-darker; border-top: 1px solid $gray-darker;
border-bottom: 1px solid $gray-darker;
padding: $gl-padding-top; padding: $gl-padding-top;
background-color: $gray-light; background-color: $gray-light;
margin: 4px -16px 0; margin: $gl-padding -16px -16px;
.mr-widget-code-quality-list { .mr-widget-code-quality-list {
list-style: none; list-style: none;
......
...@@ -11,7 +11,8 @@ module NavHelper ...@@ -11,7 +11,8 @@ module NavHelper
if current_path?('merge_requests#show') || if current_path?('merge_requests#show') ||
current_path?('projects/merge_requests/conflicts#show') || current_path?('projects/merge_requests/conflicts#show') ||
current_path?('issues#show') || current_path?('issues#show') ||
current_path?('milestones#show') current_path?('milestones#show') ||
current_path?('epics#show')
if cookies[:collapsed_gutter] == 'true' if cookies[:collapsed_gutter] == 'true'
%w[page-gutter right-sidebar-collapsed] %w[page-gutter right-sidebar-collapsed]
else else
......
...@@ -19,11 +19,10 @@ module Geo ...@@ -19,11 +19,10 @@ module Geo
end end
def execute def execute
project = Project.find(project_id)
project.expire_caches_before_rename(old_disk_path) project.expire_caches_before_rename(old_disk_path)
if migrating_from_legacy_storage?(project) if migrating_from_legacy_storage? && !move_repository
Geo::MoveRepositoryService.new(project, old_disk_path, new_disk_path).execute raise RepositoryCannotBeRenamed, "Repository #{old_disk_path} could not be renamed to #{new_disk_path}"
end end
true true
...@@ -31,12 +30,20 @@ module Geo ...@@ -31,12 +30,20 @@ module Geo
private private
def migrating_from_legacy_storage?(project) def project
@project ||= Project.find(project_id)
end
def migrating_from_legacy_storage?
from_legacy_storage? && project.hashed_storage?(:repository) from_legacy_storage? && project.hashed_storage?(:repository)
end end
def from_legacy_storage? def from_legacy_storage?
old_storage_version.nil? || old_storage_version.zero? old_storage_version.nil? || old_storage_version.zero?
end end
def move_repository
Geo::MoveRepositoryService.new(project, old_disk_path, new_disk_path).execute
end
end end
end end
module Geo module Geo
RepositoryCannotBeRenamed = Class.new(StandardError)
class MoveRepositoryService class MoveRepositoryService
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include Gitlab::Geo::ProjectLogHelpers
attr_reader :project, :old_disk_path, :new_disk_path attr_reader :project, :old_disk_path, :new_disk_path
...@@ -11,29 +14,21 @@ module Geo ...@@ -11,29 +14,21 @@ module Geo
end end
def execute def execute
# Make sure target directory exists (used when transfering repositories)
project.ensure_storage_path_exists project.ensure_storage_path_exists
move_project_repository && move_wiki_repository
rescue
log_error('Repository cannot be renamed')
false
end
if gitlab_shell.mv_repository(project.repository_storage_path, private
old_disk_path, new_disk_path)
# If repository moved successfully we need to send update instructions to users. def move_project_repository
# However we cannot allow rollback since we moved repository gitlab_shell.mv_repository(project.repository_storage_path, old_disk_path, new_disk_path)
# So we basically we mute exceptions in next actions end
begin
gitlab_shell.mv_repository(project.repository_storage_path,
"#{old_disk_path}.wiki", "#{new_disk_path}.wiki")
rescue
# Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks
false
end
else
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
raise StandardError.new('Repository cannot be renamed')
end
true def move_wiki_repository
gitlab_shell.mv_repository(project.repository_storage_path, "#{old_disk_path}.wiki", "#{new_disk_path}.wiki")
end end
end end
end end
...@@ -13,11 +13,22 @@ module Geo ...@@ -13,11 +13,22 @@ module Geo
end end
def execute def execute
project = Project.find(project_id)
project.expire_caches_before_rename(old_disk_path) project.expire_caches_before_rename(old_disk_path)
return true if project.hashed_storage?(:repository) if project.legacy_storage? && !move_repository
raise RepositoryCannotBeRenamed, "Repository #{old_disk_path} could not be renamed to #{new_disk_path}"
end
true
end
private
def project
@project ||= Project.find(project_id)
end
def move_repository
Geo::MoveRepositoryService.new(project, old_disk_path, new_disk_path).execute Geo::MoveRepositoryService.new(project, old_disk_path, new_disk_path).execute
end end
end end
......
...@@ -6,6 +6,8 @@ module MergeRequests ...@@ -6,6 +6,8 @@ module MergeRequests
# Executed when you do merge via GitLab UI # Executed when you do merge via GitLab UI
# #
class MergeService < MergeRequests::BaseService class MergeService < MergeRequests::BaseService
prepend EE::MergeRequests::MergeService
MergeError = Class.new(StandardError) MergeError = Class.new(StandardError)
attr_reader :merge_request, :source attr_reader :merge_request, :source
...@@ -18,17 +20,7 @@ module MergeRequests ...@@ -18,17 +20,7 @@ module MergeRequests
@merge_request = merge_request @merge_request = merge_request
unless @merge_request.mergeable? error_check!
return handle_merge_error(log_message: 'Merge request is not mergeable', save_message_on_model: true)
end
check_size_limit
@source = find_merge_source
unless @source
return handle_merge_error(log_message: 'No source for merge', save_message_on_model: true)
end
merge_request.in_locked_state do merge_request.in_locked_state do
if commit if commit
...@@ -65,6 +57,19 @@ module MergeRequests ...@@ -65,6 +57,19 @@ module MergeRequests
private private
def error_check!
error =
if @merge_request.should_be_rebased?
'Only fast-forward merge is allowed for your project. Please update your source branch'
elsif !@merge_request.mergeable?
'Merge request is not mergeable'
elsif !source
'No source for merge'
end
raise MergeError, error if error
end
def commit def commit
message = params[:commit_message] || merge_request.merge_commit_message message = params[:commit_message] || merge_request.merge_commit_message
...@@ -115,25 +120,8 @@ module MergeRequests ...@@ -115,25 +120,8 @@ module MergeRequests
merge_request.to_reference(full: true) merge_request.to_reference(full: true)
end end
def check_size_limit def source
if @merge_request.target_project.above_size_limit? @source ||= @merge_request.diff_head_sha
message = Gitlab::RepositorySizeError.new(@merge_request.target_project).merge_error
raise MergeError, message
end
end
def find_merge_source
return merge_request.diff_head_sha unless merge_request.squash
squash_result = SquashService.new(project, current_user, params).execute(merge_request)
case squash_result[:status]
when :success
squash_result[:squash_sha]
when :error
raise MergeError, squash_result[:message]
end
end end
end end
end end
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
Geo nodes (#{@nodes.count}) Geo nodes (#{@nodes.count})
%ul.well-list.geo-nodes %ul.well-list.geo-nodes
- @nodes.each do |node| - @nodes.each do |node|
%li{ id: dom_id(node), class: node_class(node), data: { status_url: status_admin_geo_node_path(node) } } %li{ id: dom_id(node), class: node_class(node), data: { status_url: status_admin_geo_node_path(node, format: :json) } }
.node-block .node-block
= node_status_icon(node) = node_status_icon(node)
%strong= node.url %strong= node.url
......
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
= link_to "Help", help_path = link_to "Help", help_path
%li.divider %li.divider
%li %li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" = link_to "Sign out", destroy_user_session_path, class: "sign-out-link"
- if session[:impersonator_id] - if session[:impersonator_id]
%li.impersonation %li.impersonation
= link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.create_access_levels-container .create_access_levels-container
= dropdown_tag('Select', = dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-create wide', options: { toggle_class: 'js-allowed-to-create wide',
dropdown_class: 'dropdown-menu-selectable', dropdown_class: 'dropdown-menu-selectable capitalize-header',
data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }}) data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }})
= render 'projects/protected_tags/shared/create_protected_tag' = render 'projects/protected_tags/shared/create_protected_tag'
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
= s_('TagsPage|Optionally, add a message to the tag.') = s_('TagsPage|Optionally, add a message to the tag.')
%hr %hr
.form-group .form-group
= label_tag :release_description, 'Release notes', class: 'control-label' = label_tag :release_description, s_('TagsPage|Release notes'), class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here...'), current_text: @release_description = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here...'), current_text: @release_description
...@@ -41,6 +41,6 @@ ...@@ -41,6 +41,6 @@
.help-block .help-block
= s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.') = s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.')
.form-actions .form-actions
= button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_tags_path(@project), class: 'btn btn-cancel' = link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel'
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
...@@ -2,10 +2,6 @@ class UpdateMergeRequestsWorker ...@@ -2,10 +2,6 @@ class UpdateMergeRequestsWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
def metrics_tags
@metrics_tags || {}
end
def perform(project_id, user_id, oldrev, newrev, ref) def perform(project_id, user_id, oldrev, newrev, ref)
project = Project.find_by(id: project_id) project = Project.find_by(id: project_id)
return unless project return unless project
...@@ -13,11 +9,6 @@ class UpdateMergeRequestsWorker ...@@ -13,11 +9,6 @@ class UpdateMergeRequestsWorker
user = User.find_by(id: user_id) user = User.find_by(id: user_id)
return unless user return unless user
@metrics_tags = {
project_id: project_id,
user_id: user_id
}
MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref) MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
end end
end end
--- ---
title: Fix TRIGGER checks for MySQL title: Add sidebar for epic
merge_request: merge_request:
author: author:
type: fixed type: added
--- ---
title: 'Fix: Failed to rebase MR from forked repo' title: 'Geo: Fix handling of nil values on advanced section in admin screen'
merge_request: merge_request:
author: author:
type: fixed type: fixed
--- ---
title: Fix arguments Import/Export error importing project merge requests title: Geo - Allow Sidekiq to retry failed jobs to rename project repositories
merge_request: merge_request:
author: author:
type: fixed type: fixed
---
title: Remove update merge request worker tagging.
merge_request:
author:
type: removed
---
title: Fix GPG signature popup info in Safari and Firefox
merge_request: 15228
author:
type: fixed
---
title: Moves mini graph of pipeline to the end of sentence in MR widget. Cleans HTML
and tests
merge_request:
author:
type: fixed
---
title: Fix webhooks recent deliveries
merge_request: 15146
author: Alexander Randa (@randaalex)
type: fixed
---
title: Fix issues with forked projects of which the source was deleted
merge_request: 15150
author:
type: fixed
---
title: Make sure group and project creation is blocked for new users that are external
by default
merge_request:
author:
type: fixed
---
title: Change 'Sign Out' route from a DELETE to a GET
merge_request: 39708
author: Joe Marty
type: changed
--- ---
title: Remove Filesystem check metrics that use too much CPU to handle requests title: Speed up issues list APIs
merge_request: merge_request:
author: author:
type: performance type: performance
---
title: Fix diff parser so it tolerates to diff special markers in the content
merge_request:
author:
type: fixed
---
title: Fix a migration that adds merge_requests_ff_only_enabled column to MR table
merge_request:
author:
type: fixed
---
title: Render 404 when polling commit notes without having permissions
merge_request:
author:
type: fixed
---
title: Fix cancel button not working while uploading on the new issue page
merge_request: 15137
author:
type: fixed
---
title: Avoid regenerating the ref path for the environment
merge_request:
author:
type: fixed
---
title: Add /groups/:id/subgroups endpoint to API
merge_request: 15142
author: marbemac
type: added
...@@ -195,7 +195,7 @@ Devise.setup do |config| ...@@ -195,7 +195,7 @@ Devise.setup do |config|
config.navigational_formats = [:"*/*", "*/*", :html, :zip] config.navigational_formats = [:"*/*", "*/*", :html, :zip]
# The default HTTP method used to sign out a resource. Default is :delete. # The default HTTP method used to sign out a resource. Default is :delete.
config.sign_out_via = :delete config.sign_out_via = :get
# ==> OmniAuth # ==> OmniAuth
# To configure a new OmniAuth provider copy and edit omniauth.rb.sample # To configure a new OmniAuth provider copy and edit omniauth.rb.sample
......
...@@ -9,7 +9,7 @@ mapping structure from the projects URLs: ...@@ -9,7 +9,7 @@ mapping structure from the projects URLs:
* Project's repository: `#{namespace}/#{project_name}.git` * Project's repository: `#{namespace}/#{project_name}.git`
* Project's wiki: `#{namespace}/#{project_name}.wiki.git` * Project's wiki: `#{namespace}/#{project_name}.wiki.git`
This structure made simple to migrate from existing solutions to GitLab and easy for Administrators to find where the This structure made simple to migrate from existing solutions to GitLab and easy for Administrators to find where the
repository is stored. repository is stored.
...@@ -27,7 +27,7 @@ of load in big installations, and can be even worst if they are using any type o ...@@ -27,7 +27,7 @@ of load in big installations, and can be even worst if they are using any type o
Last, for GitLab Geo, this storage type means we have to synchronize the disk state, replicate renames in the correct Last, for GitLab Geo, this storage type means we have to synchronize the disk state, replicate renames in the correct
order or we may end-up with wrong repository or missing data temporarily. order or we may end-up with wrong repository or missing data temporarily.
This pattern also exists in other objects stored in GitLab, like issue Attachments, GitLab Pages artifacts, This pattern also exists in other objects stored in GitLab, like issue Attachments, GitLab Pages artifacts,
Docker Containers for the integrated Registry, etc. Docker Containers for the integrated Registry, etc.
## Hashed Storage ## Hashed Storage
...@@ -62,9 +62,9 @@ you will never mistakenly restore a repository in the wrong project (considering ...@@ -62,9 +62,9 @@ you will never mistakenly restore a repository in the wrong project (considering
### How to migrate to Hashed Storage ### How to migrate to Hashed Storage
In GitLab, go to **Admin > Settings**, find the **Repository Storage** section and select In GitLab, go to **Admin > Settings**, find the **Repository Storage** section and select
"_Create new projects using hashed storage paths_". "_Create new projects using hashed storage paths_".
To migrate your existing projects to the new storage type, check the specific [rake tasks]. To migrate your existing projects to the new storage type, check the specific [rake tasks].
[ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283 [ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283
...@@ -79,14 +79,14 @@ coverage status below. ...@@ -79,14 +79,14 @@ coverage status below.
Note that things stored in an S3 compatible endpoint will not have the downsides mentioned earlier, if they are not Note that things stored in an S3 compatible endpoint will not have the downsides mentioned earlier, if they are not
prefixed with `#{namespace}/#{project_name}`, which is true for CI Cache and LFS Objects. prefixed with `#{namespace}/#{project_name}`, which is true for CI Cache and LFS Objects.
| Storable Object | Legacy Storage | Hashed Storage | S3 Compatible | GitLab Version | | Storable Object | Legacy Storage | Hashed Storage | S3 Compatible | GitLab Version |
| ----------------| -------------- | -------------- | ------------- | -------------- | | --------------- | -------------- | -------------- | ------------- | -------------- |
| Repository | Yes | Yes | - | 10.0 | | Repository | Yes | Yes | - | 10.0 |
| Attachments | Yes | Yes | - | 10.2 | | Attachments | Yes | Yes | - | 10.2 |
| Avatars | Yes | No | - | - | | Avatars | Yes | No | - | - |
| Pages | Yes | No | - | - | | Pages | Yes | No | - | - |
| Docker Registry | Yes | No | - | - | | Docker Registry | Yes | No | - | - |
| CI Build Logs | No | No | - | - | | CI Build Logs | No | No | - | - |
| CI Artifacts | No | No | - | - | | CI Artifacts | No | No | Yes (EEP) | - |
| CI Cache | No | No | Yes | - | | CI Cache | No | No | Yes | - |
| LFS Objects | Yes | No | Yes (EEP) | - | | LFS Objects | Yes | No | Yes (EEP) | - |
...@@ -9,13 +9,13 @@ Parameters: ...@@ -9,13 +9,13 @@ Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `skip_groups` | array of integers | no | Skip the group IDs passes | | `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to | | `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
| `search` | string | no | Return list of authorized groups matching the search criteria | | `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` | | `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | | `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) | | `statistics` | boolean | no | Include group statistics (admins only) |
| `owned` | boolean | no | Limit by groups owned by the current user | | `owned` | boolean | no | Limit to groups owned by the current user |
``` ```
GET /groups GET /groups
...@@ -80,6 +80,47 @@ You can filter by [custom attributes](custom_attributes.md) with: ...@@ -80,6 +80,47 @@ You can filter by [custom attributes](custom_attributes.md) with:
GET /groups?custom_attributes[key]=value&custom_attributes[other_key]=other_value GET /groups?custom_attributes[key]=value&custom_attributes[other_key]=other_value
``` ```
## List a groups's subgroups
Get a list of visible direct subgroups in this group.
When accessed without authentication, only public groups are returned.
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group |
| `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
| `owned` | boolean | no | Limit to groups owned by the current user |
```
GET /groups/:id/subgroups
```
```json
[
{
"id": 1,
"name": "Foobar Group",
"path": "foo-bar",
"description": "An interesting group",
"visibility": "public",
"lfs_enabled": true,
"avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/foo.jpg",
"web_url": "http://gitlab.example.com/groups/foo-bar",
"request_access_enabled": false,
"full_name": "Foobar Group",
"full_path": "foo-bar",
"parent_id": 123
}
]
```
## List a group's projects ## List a group's projects
Get a list of projects in this group. When accessed without authentication, only Get a list of projects in this group. When accessed without authentication, only
......
...@@ -58,6 +58,8 @@ Parameters: ...@@ -58,6 +58,8 @@ Parameters:
"project_id": 3, "project_id": 3,
"title": "test1", "title": "test1",
"state": "opened", "state": "opened",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"upvotes": 0, "upvotes": 0,
"downvotes": 0, "downvotes": 0,
"author": { "author": {
...@@ -170,6 +172,8 @@ Parameters: ...@@ -170,6 +172,8 @@ Parameters:
"project_id": 3, "project_id": 3,
"title": "test1", "title": "test1",
"state": "opened", "state": "opened",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"upvotes": 0, "upvotes": 0,
"downvotes": 0, "downvotes": 0,
"author": { "author": {
...@@ -248,6 +252,8 @@ Parameters: ...@@ -248,6 +252,8 @@ Parameters:
"project_id": 3, "project_id": 3,
"title": "test1", "title": "test1",
"state": "merged", "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"upvotes": 0, "upvotes": 0,
"downvotes": 0, "downvotes": 0,
"author": { "author": {
......
# File Storage in GitLab
We use the [CarrierWave] gem to handle file upload, store and retrieval.
There are many places where file uploading is used, according to contexts:
* System
- Instance Logo (logo visible in sign in/sign up pages)
- Header Logo (one displayed in the navigation bar)
* Group
- Group avatars
* User
- User avatars
- User snippet attachments
* Project
- Project avatars
- Issues/MR Markdown attachments
- Issues/MR Legacy Markdown attachments
- CI Build Artifacts
- LFS Objects
## Disk storage
GitLab started saving everything on local disk. While directory location changed from previous versions,
they are still not 100% standardized. You can see them below:
| Description | In DB? | Relative path | Uploader class | model_type |
| ------------------------------------- | ------ | ----------------------------------------------------------- | ---------------------- | ---------- |
| Instance logo | yes | uploads/-/system/appearance/logo/:id/:filename | `AttachmentUploader` | Appearance |
| Header logo | yes | uploads/-/system/appearance/header_logo/:id/:filename | `AttachmentUploader` | Appearance |
| Group avatars | yes | uploads/-/system/group/avatar/:id/:filename | `AvatarUploader` | Group |
| User avatars | yes | uploads/-/system/user/avatar/:id/:filename | `AvatarUploader` | User |
| User snippet attachments | yes | uploads/-/system/personal_snippet/:id/:random_hex/:filename | `PersonalFileUploader` | Snippet |
| Project avatars | yes | uploads/-/system/project/avatar/:id/:filename | `AvatarUploader` | Project |
| Issues/MR Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project |
| Issues/MR Legacy Markdown attachments | no | uploads/-/system/note/attachment/:id/:filename | `AttachmentUploader` | Note |
| CI Artifacts (CE) | yes | shared/artifacts/:year_:month/:project_id/:id | `ArtifactUploader` | Ci::Build |
| LFS Objects (CE) | yes | shared/lfs-objects/:hex/:hex/:object_hash | `LfsObjectUploader` | LfsObject |
CI Artifacts and LFS Objects behave differently in CE and EE. In CE they inherit the `GitlabUploader`
while in EE they inherit the `ObjectStoreUploader` and store files in and S3 API compatible object store.
In the case of Issues/MR Markdown attachments, there is a different approach using the [Hashed Storage] layout,
instead of basing the path into a mutable variable `:project_path_with_namespace`, it's possible to use the
hash of the project ID instead, if project migrates to the new approach (introduced in 10.2).
[CarrierWave]: https://github.com/carrierwaveuploader/carrierwave
[Hashed Storage]: ../administration/repository_storage_types.md
...@@ -110,7 +110,7 @@ You can mark that content for translation with: ...@@ -110,7 +110,7 @@ You can mark that content for translation with:
In JavaScript we added the `__()` (double underscore parenthesis) function In JavaScript we added the `__()` (double underscore parenthesis) function
for translations. for translations.
### Updating the PO files with the new content ## Updating the PO files with the new content
Now that the new content is marked for translation, we need to update the PO Now that the new content is marked for translation, we need to update the PO
files with the following command: files with the following command:
...@@ -119,23 +119,20 @@ files with the following command: ...@@ -119,23 +119,20 @@ files with the following command:
bundle exec rake gettext:find bundle exec rake gettext:find
``` ```
This command will update the `locale/**/gitlab.edit.po` file with the This command will update the `locale/gitlab.pot` file with the newly externalized
new content that the parser has found. strings and remove any strings that aren't used anymore. You should check this
file in. Once the changes are on master, they will be picked up by
[Crowdin](http://translate.gitlab.com) and be presented for translation.
New translations will be added with their default content and will be marked The command also updates the translation files for each language: `locale/*/gitlab.po`
fuzzy. To use the translation, look for the `#, fuzzy` mention in `gitlab.edit.po` These changes can be discarded, the languange files will be updated by Crowdin
and remove it. automatically.
We need to make sure we remove the `fuzzy` translations before generating the Discard all of them at once like this:
`locale/**/gitlab.po` file. When they aren't removed, the resulting `.po` will
be treated as a binary file which could overwrite translations that were merged
before the new translations.
When we are just preparing a page to be translated, but not actually adding any ```sh
translations. There's no need to generate `.po` files. git checkout locale/*/gitlab.po
```
Translations that aren't used in the source code anymore will be marked with
`~#`; these can be removed to keep our translation files clutter-free.
### Validating PO files ### Validating PO files
......
...@@ -137,7 +137,17 @@ Using hashed storage significantly improves Geo replication - project and group ...@@ -137,7 +137,17 @@ Using hashed storage significantly improves Geo replication - project and group
renames no longer require synchronization between nodes - so we recommend it is renames no longer require synchronization between nodes - so we recommend it is
used for all GitLab Geo installations. used for all GitLab Geo installations.
### Step 4. Managing the secondary GitLab node ### Step 4. (Optional) Configuring the secondary to trust the primary
You can safely skip this step if your primary uses a CA-issued HTTPS certificate.
If your primary is using a self-signed certificate for *HTTPS* support, you will
need to add that certificate to the secondary's trust store. Retrieve the
certificate from the primary and follow
[these instructions](https://docs.gitlab.com/omnibus/settings/ssl.html)
on the secondary.
### Step 5. Managing the secondary GitLab node
You can monitor the status of the syncing process on a secondary node You can monitor the status of the syncing process on a secondary node
by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`) by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`)
...@@ -231,7 +241,7 @@ to add a new secondary in the short term, you can follow these instructions: ...@@ -231,7 +241,7 @@ to add a new secondary in the short term, you can follow these instructions:
``` ```
Follow the steps above to set up the new Geo node. When you reach Follow the steps above to set up the new Geo node. When you reach
[Step 4: Enabling the secondary GitLab node](#step-4-enabling-the-secondary-gitlab-node) [Step 5: Enabling the secondary GitLab node](#step-5-managing-the-secondary-gitlab-node)
select "SSH (deprecated)" instead of "HTTP/HTTPS", and populate the "Public Key" select "SSH (deprecated)" instead of "HTTP/HTTPS", and populate the "Public Key"
with the output of the previous command (beginning `ssh-rsa AAAA...`). with the output of the previous command (beginning `ssh-rsa AAAA...`).
......
...@@ -128,8 +128,23 @@ Using hashed storage significantly improves Geo replication - project and group ...@@ -128,8 +128,23 @@ Using hashed storage significantly improves Geo replication - project and group
renames no longer require synchronization between nodes - so we recommend it is renames no longer require synchronization between nodes - so we recommend it is
used for all GitLab Geo installations. used for all GitLab Geo installations.
### Step 4. (Optional) Configuring the secondary to trust the primary
### Step 4. Managing the secondary GitLab node You can safely skip this step if your primary uses a CA-issued HTTPS certificate.
If your primary is using a self-signed certificate for *HTTPS* support, you will
need to add that certificate to the secondary's trust store. Retrieve the
certificate from the primary and follow your distribution's instructions for
adding it to the secondary's trust store. In Debian/Ubuntu, for example, with a
certificate file of `primary.geo.example.com.crt`, you would follow these steps:
```
sudo -i
cp primary.geo.example.com.crt /usr/local/share/ca-certificates
update-ca-certificates
```
### Step 5. Managing the secondary GitLab node
You can monitor the status of the syncing process on a secondary node You can monitor the status of the syncing process on a secondary node
by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`) by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`)
......
This diff is collapsed.
# From Community Edition 10.2 to Enterprise Edition 10.2
This guide assumes you have a correctly configured and tested installation of
GitLab Community Edition 10.2. If you run into any trouble or if you have any
questions please contact us at [support@gitlab.com].
### 0. Backup
Make a backup just in case something goes wrong:
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
For installations using MySQL, this may require granting "LOCK TABLES"
privileges to the GitLab user on the database version.
### 1. Stop server
```bash
sudo service gitlab stop
```
### 2. Get the EE code
```bash
cd /home/git/gitlab
sudo -u git -H git remote add -f ee https://gitlab.com/gitlab-org/gitlab-ee.git
sudo -u git -H git checkout 10-2-stable-ee
```
### 3. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
# MySQL installations (note: the line below states '--without postgres')
sudo -u git -H bundle install --without postgres development test --deployment
# PostgreSQL installations (note: the line below states '--without mysql')
sudo -u git -H bundle install --without mysql development test --deployment
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
### 4. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
### 5. Check application status
Check if GitLab and its environment are configured correctly:
```bash
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```
To make sure you didn't miss anything run a more thorough check with:
```bash
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
```
If all items are green, then congratulations upgrade complete!
## Things went south? Revert to previous version (Community Edition 10.2)
### 1. Revert the code to the previous version
```bash
cd /home/git/gitlab
sudo -u git -H git checkout 10-2-stable
```
### 2. Restore from the backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
[support@gitlab.com]: mailto:support@gitlab.com
...@@ -57,59 +57,6 @@ Here's what you'll need to have installed: ...@@ -57,59 +57,6 @@ Here's what you'll need to have installed:
sudo gitlab-rake db:create db:migrate sudo gitlab-rake db:create db:migrate
``` ```
1. Stop Unicorn in case it's interfering the next step:
``` bash
sudo gitlab-ctl stop unicorn
```
After these steps, you'll have a fresh PostgreSQL database with up-to-date schema.
## Migrate data from MySQL to PostgreSQL
Now, you can use pgloader to migrate the data from MySQL to PostgreSQL:
1. Save the following snippet in a `commands.load` file, and edit with your
database `username`, `password` and `host`:
```
LOAD DATABASE
FROM mysql://username:password@host/gitlabhq_production
INTO postgresql://gitlab-psql@unix://var/opt/gitlab/postgresql:/gitlabhq_production
WITH include no drop, truncate, disable triggers, create no tables,
create no indexes, preserve index names, no foreign keys,
data only
ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public'
;
```
1. Start the migration:
``` bash
sudo -u gitlab-psql pgloader commands.load
```
1. Once the migration finishes, start GitLab:
``` bash
sudo gitlab-ctl start
```
Now, you can verify that everything worked by visiting GitLab.
## Troubleshooting
### Experiencing 500 errors after the migration
If you experience 500 errors after the migration, try to clear the cache:
``` bash
sudo gitlab-rake cache:clear
```
=======
1. Stop Unicorn to prevent other database access from interfering with the loading of data: 1. Stop Unicorn to prevent other database access from interfering with the loading of data:
``` bash ``` bash
......
...@@ -18,7 +18,8 @@ Into a single commit on merge: ...@@ -18,7 +18,8 @@ Into a single commit on merge:
![A squashed commit followed by a merge commit][squashed-commit] ![A squashed commit followed by a merge commit][squashed-commit]
Note that the squashed commit is still followed by a merge commit, as the merge The squashed commit's commit message is the merge request title. And note that
the squashed commit is still followed by a merge commit, as the merge
method for this example repository uses a merge commit. Squashing also works method for this example repository uses a merge commit. Squashing also works
with the fast-forward merge strategy, see with the fast-forward merge strategy, see
[squashing and fast-forward merge](#squashing-and-fast-forward-merge) for more [squashing and fast-forward merge](#squashing-and-fast-forward-merge) for more
......
...@@ -28,20 +28,22 @@ ...@@ -28,20 +28,22 @@
<template> <template>
<div class="detail-page-header"> <div class="detail-page-header">
Opened <div class="issuable-meta">
<timeagoTooltip Opened
:time="created" <timeagoTooltip
/> :time="created"
by
<strong>
<user-avatar-link
:link-href="author.url"
:img-src="author.src"
:img-size="24"
:tooltipText="author.username"
:username="author.name"
imgCssClasses="avatar-inline"
/> />
</strong> by
<strong>
<user-avatar-link
:link-href="author.url"
:img-src="author.src"
:img-size="24"
:tooltipText="author.username"
:username="author.name"
imgCssClasses="avatar-inline"
/>
</strong>
</div>
</div> </div>
</template> </template>
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import issuableApp from '~/issue_show/components/app.vue'; import issuableApp from '~/issue_show/components/app.vue';
import relatedIssuesRoot from '~/issuable/related_issues/components/related_issues_root.vue'; import relatedIssuesRoot from '~/issuable/related_issues/components/related_issues_root.vue';
import epicHeader from './epic_header.vue'; import epicHeader from './epic_header.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue';
export default { export default {
name: 'epicShowApp', name: 'epicShowApp',
...@@ -64,9 +65,18 @@ ...@@ -64,9 +65,18 @@
type: String, type: String,
required: true, required: true,
}, },
startDate: {
type: String,
required: false,
},
endDate: {
type: String,
required: false,
},
}, },
components: { components: {
epicHeader, epicHeader,
epicSidebar,
issuableApp, issuableApp,
relatedIssuesRoot, relatedIssuesRoot,
}, },
...@@ -85,21 +95,29 @@ ...@@ -85,21 +95,29 @@
:author="author" :author="author"
:created="created" :created="created"
/> />
<div class="issuable-details detail-page-description content-block"> <div class="issuable-details content-block">
<issuable-app <div class="detail-page-description">
:can-update="canUpdate" <issuable-app
:can-destroy="canDestroy" :can-update="canUpdate"
:can-destroy="canDestroy"
:endpoint="endpoint"
:issuable-ref="issuableRef"
:initial-title-html="initialTitleHtml"
:initial-title-text="initialTitleText"
:initial-description-html="initialDescriptionHtml"
:initial-description-text="initialDescriptionText"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-inline-edit-button="true"
/>
</div>
<epic-sidebar
:endpoint="endpoint" :endpoint="endpoint"
:issuable-ref="issuableRef" :editable="canUpdate"
:initial-title-html="initialTitleHtml" :initialStartDate="startDate"
:initial-title-text="initialTitleText" :initialEndDate="endDate"
:initial-description-html="initialDescriptionHtml"
:initial-description-text="initialDescriptionText"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-inline-edit-button="true"
/> />
<related-issues-root <related-issues-root
:endpoint="issueLinksEndpoint" :endpoint="issueLinksEndpoint"
......
...@@ -12,6 +12,10 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -12,6 +12,10 @@ document.addEventListener('DOMContentLoaded', () => {
canDestroy: false, canDestroy: false,
}); });
// Convert backend casing to match frontend style guide
props.startDate = props.start_date;
props.endDate = props.end_date;
return new Vue({ return new Vue({
el, el,
components: { components: {
......
<script>
import Cookies from 'js-cookie';
import Flash from '~/flash';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
import sidebarCollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import SidebarService from '../services/sidebar_service';
import Store from '../stores/sidebar_store';
export default {
name: 'epicSidebar',
props: {
endpoint: {
type: String,
required: true,
},
editable: {
type: Boolean,
required: false,
default: false,
},
initialStartDate: {
type: String,
required: false,
},
initialEndDate: {
type: String,
required: false,
},
},
data() {
const store = new Store({
startDate: this.initialStartDate,
endDate: this.initialEndDate,
});
return {
store,
// Backend will pass the appropriate css class for the contentContainer
collapsed: Cookies.get('collapsed_gutter') === 'true',
savingStartDate: false,
savingEndDate: false,
service: new SidebarService(this.endpoint),
};
},
components: {
sidebarDatePicker,
sidebarCollapsedGroupedDatePicker,
},
methods: {
toggleSidebar() {
this.collapsed = !this.collapsed;
const contentContainer = this.$el.closest('.page-with-sidebar');
contentContainer.classList.toggle('right-sidebar-expanded');
contentContainer.classList.toggle('right-sidebar-collapsed');
Cookies.set('collapsed_gutter', this.collapsed);
},
saveDate(dateType = 'start', newDate) {
const type = dateType === 'start' ? dateType : 'end';
const capitalizedType = capitalizeFirstCharacter(type);
const serviceMethod = `update${capitalizedType}Date`;
const savingBoolean = `saving${capitalizedType}Date`;
this[savingBoolean] = true;
return this.service[serviceMethod](newDate)
.then(() => {
this[savingBoolean] = false;
this.store[`${type}Date`] = newDate;
})
.catch(() => {
this[savingBoolean] = false;
Flash(`An error occurred while saving ${type} date`);
});
},
saveStartDate(date) {
return this.saveDate('start', date);
},
saveEndDate(date) {
return this.saveDate('end', date);
},
},
};
</script>
<template>
<aside
class="right-sidebar"
:class="{ 'right-sidebar-expanded' : !collapsed, 'right-sidebar-collapsed': collapsed }"
>
<div class="issuable-sidebar">
<sidebar-date-picker
v-if="!collapsed"
:collapsed="collapsed"
:is-loading="savingStartDate"
:editable="editable"
label="Planned start date"
:selected-date="store.startDateTime"
:max-date="store.endDateTime"
:show-toggle-sidebar="true"
@saveDate="saveStartDate"
@toggleCollapse="toggleSidebar"
/>
<sidebar-date-picker
v-if="!collapsed"
:collapsed="collapsed"
:is-loading="savingEndDate"
:editable="editable"
label="Planned finish date"
:selected-date="store.endDateTime"
:min-date="store.startDateTime"
@saveDate="saveEndDate"
@toggleCollapse="toggleSidebar"
/>
<sidebar-collapsed-grouped-date-picker
v-if="collapsed"
:collapsed="collapsed"
:min-date="store.startDateTime"
:max-date="store.endDateTime"
:show-toggle-sidebar="true"
@toggleCollapse="toggleSidebar"
/>
</div>
</aside>
</template>
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class SidebarService {
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}.json`, {});
}
updateStartDate(startDate) {
return this.resource.update({
start_date: startDate,
});
}
updateEndDate(endDate) {
return this.resource.update({
end_date: endDate,
});
}
}
import { parsePikadayDate } from '~/lib/utils/datefix';
export default class SidebarStore {
constructor({ startDate, endDate }) {
this.startDate = startDate;
this.endDate = endDate;
}
get startDateTime() {
return this.startDate ? parsePikadayDate(this.startDate) : null;
}
get endDateTime() {
return this.endDate ? parsePikadayDate(this.endDate) : null;
}
}
...@@ -9,7 +9,9 @@ module EpicsHelper ...@@ -9,7 +9,9 @@ module EpicsHelper
url: user_path(author), url: user_path(author),
username: "@#{author.username}", username: "@#{author.username}",
src: avatar_icon(@epic.author) src: avatar_icon(@epic.author)
} },
start_date: @epic.start_date,
end_date: @epic.end_date
} }
data.to_json data.to_json
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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