Commit 0bd3d6e2 authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'ce-to-ee-2018-01-05' into 'master'

CE upstream - Friday

Closes #3377, gitaly#854, and gitaly#825

See merge request gitlab-org/gitlab-ee!3929
parents 2aa91fd2 4590e84a
......@@ -456,6 +456,7 @@ ee_compat_check:
- master
- tags
- /^[\d-]+-stable(-ee)?/
- /^security-/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
retry: 0
......@@ -533,7 +534,7 @@ db:rollback-mysql:
<<: *db-rollback
<<: *use-mysql
.db-seed_fu: &db-seed_fu
.gitlab-setup: &gitlab-setup
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
......@@ -542,22 +543,24 @@ db:rollback-mysql:
SIZE: "1"
SETUP_DB: "false"
CREATE_DB_USER: "true"
FIXTURE_PATH: db/fixtures/development
script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
- bundle exec rake db:setup db:seed_fu
- scripts/gitaly-test-spawn
- force=yes bundle exec rake gitlab:setup
artifacts:
when: on_failure
expire_in: 1d
paths:
- log/development.log
db:seed_fu-pg:
<<: *db-seed_fu
gitlab:setup-pg:
<<: *gitlab-setup
<<: *use-pg
db:seed_fu-mysql:
<<: *db-seed_fu
gitlab:setup-mysql:
<<: *gitlab-setup
<<: *use-mysql
# Frontend-related jobs
......
......@@ -554,7 +554,7 @@ the feature you contribute through all of these steps.
1. Description explaining the relevancy (see following item)
1. Working and clean code that is commented where needed
1. [Unit and system tests][testing] that pass on the CI server
1. [Unit, integration, and system tests][testing] that pass on the CI server
1. Performance/scalability implications have been considered, addressed, and tested
1. [Documented][doc-styleguide] in the `/doc` directory
1. [Changelog entry added][changelog], if necessary
......
......@@ -418,7 +418,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.62.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.64.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -308,7 +308,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.62.0)
gitaly-proto (0.64.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -1081,7 +1081,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.62.0)
gitaly-proto (~> 0.64.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......
......@@ -120,7 +120,7 @@ export default {
},
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: document.querySelectorAll('.boards-list')[0],
scroll: true,
group: 'issues',
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
......
......@@ -77,7 +77,8 @@ export default {
class="group-row"
>
<div
class="group-row-contents">
class="group-row-contents"
:class="{ 'project-row-contents': !isGroup }">
<item-actions
v-if="isGroup"
:group="group"
......@@ -97,7 +98,7 @@ export default {
/>
</div>
<div
class="avatar-container s40 hidden-xs"
class="avatar-container prepend-top-8 prepend-left-5 s24 hidden-xs"
:class="{ 'content-loading': group.isChildrenLoading }"
>
<a
......@@ -106,11 +107,12 @@ export default {
>
<img
v-if="hasAvatar"
class="avatar s40"
class="avatar s24"
:src="group.avatarUrl"
/>
<identicon
v-else
size-class="s24"
:entity-id=group.id
:entity-name="group.name"
/>
......@@ -123,7 +125,7 @@ export default {
:href="group.relativePath"
:title="group.fullName"
class="no-expand"
data-placement="top"
data-placement="bottom"
>{{
// ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending
......
<script>
import { s__ } from '../../locale';
import tooltip from '../../vue_shared/directives/tooltip';
import modal from '../../vue_shared/components/modal.vue';
import { s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import modal from '~/vue_shared/components/modal.vue';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
import Icon from '../../vue_shared/components/icon.vue';
export default {
components: {
Icon,
icon,
modal,
},
directives: {
......@@ -64,10 +64,9 @@ export default {
:title="editBtnTitle"
:aria-label="editBtnTitle"
data-container="body"
data-placement="bottom"
class="edit-group btn no-expand">
<icon
name="settings">
</icon>
<icon name="settings"/>
</a>
<a
v-tooltip
......@@ -77,10 +76,9 @@ export default {
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
data-container="body"
data-placement="bottom"
class="leave-group btn no-expand">
<i
class="fa fa-sign-out"
aria-hidden="true"/>
<icon name="leave"/>
</a>
<modal
v-show="modalStatus"
......
<script>
import icon from '~/vue_shared/components/icon.vue';
export default {
props: {
isGroupOpen: {
......@@ -7,9 +9,12 @@ export default {
default: false,
},
},
components: {
icon,
},
computed: {
iconClass() {
return this.isGroupOpen ? 'fa-caret-down' : 'fa-caret-right';
return this.isGroupOpen ? 'angle-down' : 'angle-right';
},
},
};
......@@ -17,9 +22,9 @@ export default {
<template>
<span class="folder-caret">
<i
:class="iconClass"
class="fa"
aria-hidden="true"/>
<icon
:size="12"
:name="iconClass"
/>
</span>
</template>
<script>
import tooltip from '../../vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants';
import itemStatsValue from './item_stats_value.vue';
export default {
directives: {
tooltip,
components: {
icon,
timeAgoTooltip,
itemStatsValue,
},
props: {
item: {
......@@ -34,65 +38,47 @@ export default {
<template>
<div class="stats">
<span
v-tooltip
<item-stats-value
v-if="isGroup"
css-class="number-subgroups"
icon-name="folder"
:title="s__('Subgroups')"
class="number-subgroups"
data-placement="top"
data-container="body">
<i
class="fa fa-folder"
aria-hidden="true"
/>
{{item.subgroupCount}}
</span>
<span
v-tooltip
:value=item.subgroupCount
/>
<item-stats-value
v-if="isGroup"
css-class="number-projects"
icon-name="bookmark"
:title="s__('Projects')"
class="number-projects"
data-placement="top"
data-container="body">
<i
class="fa fa-bookmark"
aria-hidden="true"
/>
{{item.projectCount}}
</span>
<span
v-tooltip
:value=item.projectCount
/>
<item-stats-value
v-if="isGroup"
css-class="number-users"
icon-name="users"
:title="s__('Members')"
class="number-users"
data-placement="top"
data-container="body">
<i
class="fa fa-users"
aria-hidden="true"
/>
{{item.memberCount}}
</span>
<span
:value=item.memberCount
/>
<item-stats-value
v-if="isProject"
class="project-stars">
<i
class="fa fa-star"
aria-hidden="true"
/>
{{item.starCount}}
</span>
<span
v-tooltip
css-class="project-stars"
icon-name="star"
:value=item.starCount
/>
<item-stats-value
css-class="item-visibility"
tooltip-placement="left"
:icon-name="visibilityIcon"
:title="visibilityTooltip"
data-placement="left"
data-container="body"
class="item-visibility">
<i
:class="visibilityIcon"
class="fa"
aria-hidden="true"
/>
<div
class="last-updated"
v-if="isProject"
>
<time-ago-tooltip
tooltip-placement="bottom"
:time="item.updatedAt"
/>
</span>
</div>
</div>
</template>
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
export default {
props: {
title: {
type: String,
required: false,
default: '',
},
cssClass: {
type: String,
required: false,
default: '',
},
iconName: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
required: false,
default: 'bottom',
},
/**
* value could either be number or string
* as `memberCount` is always passed as string
* while `subgroupCount` & `projectCount`
* are always number
*/
value: {
type: [Number, String],
required: false,
default: '',
},
},
directives: {
tooltip,
},
components: {
icon,
},
computed: {
isValuePresent() {
return this.value !== '';
},
},
};
</script>
<template>
<span
v-tooltip
data-container="body"
:data-placement="tooltipPlacement"
:class="cssClass"
:title="title"
>
<icon :name="iconName"/>
<span
v-if="isValuePresent"
class="stat-value"
>
{{value}}
</span>
</span>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
import { ITEM_TYPE } from '../constants';
export default {
components: {
icon,
},
props: {
itemType: {
type: String,
......@@ -16,9 +20,9 @@ export default {
computed: {
iconClass() {
if (this.itemType === ITEM_TYPE.GROUP) {
return this.isGroupOpen ? 'fa-folder-open' : 'fa-folder';
return this.isGroupOpen ? 'folder-open' : 'folder';
}
return 'fa-bookmark';
return 'bookmark';
},
},
};
......@@ -26,9 +30,6 @@ export default {
<template>
<span class="item-type-icon">
<i
:class="iconClass"
class="fa"
aria-hidden="true"/>
<icon :name="iconClass"/>
</span>
</template>
......@@ -29,7 +29,7 @@ export const PROJECT_VISIBILITY_TYPE = {
};
export const VISIBILITY_TYPE_ICON = {
public: 'fa-globe',
internal: 'fa-shield',
private: 'fa-lock',
public: 'earth',
internal: 'shield',
private: 'lock',
};
......@@ -91,6 +91,7 @@ export default class GroupsStore {
subgroupCount: rawGroupItem.subgroup_count,
memberCount: rawGroupItem.number_users_with_delimiter,
starCount: rawGroupItem.star_count,
updatedAt: rawGroupItem.updated_at,
};
}
......
......@@ -2,11 +2,18 @@
import { mapGetters, mapState, mapActions } from 'vuex';
import repoCommitSection from './repo_commit_section.vue';
import icon from '../../vue_shared/components/icon.vue';
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
export default {
data() {
return {
width: 290,
};
},
components: {
repoCommitSection,
icon,
panelResizer,
},
computed: {
...mapState([
......@@ -18,10 +25,20 @@ export default {
currentIcon() {
return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
},
maxSize() {
return window.innerWidth / 2;
},
panelStyle() {
if (!this.rightPanelCollapsed) {
return { width: `${this.width}px` };
}
return {};
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
......@@ -29,6 +46,12 @@ export default {
collapsed: !this.rightPanelCollapsed,
});
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
},
};
</script>
......@@ -39,6 +62,7 @@ export default {
:class="{
'is-collapsed': rightPanelCollapsed,
}"
:style="panelStyle"
>
<div
class="multi-file-commit-panel-section">
......@@ -71,5 +95,14 @@ export default {
<repo-commit-section
class=""/>
</div>
<panel-resizer
:size.sync="width"
:enabled="!rightPanelCollapsed"
:start-size="290"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="left"/>
</div>
</template>
......@@ -2,11 +2,18 @@
import { mapState, mapActions } from 'vuex';
import projectTree from './ide_project_tree.vue';
import icon from '../../vue_shared/components/icon.vue';
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
export default {
data() {
return {
width: 290,
};
},
components: {
projectTree,
icon,
panelResizer,
},
computed: {
...mapState([
......@@ -16,10 +23,20 @@ export default {
currentIcon() {
return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
},
maxSize() {
return window.innerWidth / 2;
},
panelStyle() {
if (!this.leftPanelCollapsed) {
return { width: `${this.width}px` };
}
return {};
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
......@@ -27,6 +44,12 @@ export default {
collapsed: !this.leftPanelCollapsed,
});
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
},
};
</script>
......@@ -37,6 +60,7 @@ export default {
:class="{
'is-collapsed': leftPanelCollapsed,
}"
:style="panelStyle"
>
<div class="multi-file-commit-panel-inner">
<project-tree
......@@ -58,5 +82,14 @@ export default {
class="collapse-text"
>Collapse sidebar</span>
</button>
<panel-resizer
:size.sync="width"
:enabled="!leftPanelCollapsed"
:start-size="290"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="right"/>
</div>
</template>
......@@ -90,6 +90,11 @@ export default {
rightPanelCollapsed() {
this.editor.updateDimensions();
},
panelResizing(isResizing) {
if (isResizing === false) {
this.editor.updateDimensions();
}
},
},
computed: {
...mapGetters([
......@@ -99,6 +104,7 @@ export default {
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
'panelResizing',
]),
shouldHideEditor() {
return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
......
......@@ -104,9 +104,9 @@
>
</file-icon>
{{ file.name }}
<fileStatusIcon
<file-status-icon
:file="file">
</fileStatusIcon>
</file-status-icon>
</a>
<new-dropdown
v-if="isTree"
......
......@@ -10,12 +10,10 @@ export default {
required: true,
},
},
components: {
fileStatusIcon,
fileIcon,
},
computed: {
closeLabel() {
if (this.tab.changed || this.tab.tempFile) {
......
......@@ -63,6 +63,10 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
export const checkCommitStatus = ({ state }) =>
service
.getBranchData(state.currentProjectId, state.currentBranchId)
......
......@@ -5,6 +5,7 @@ export const SET_ROOT = 'SET_ROOT';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
......
......@@ -49,6 +49,11 @@ export default {
rightPanelCollapsed: collapsed,
});
},
[types.SET_RESIZING_STATUS](state, resizing) {
Object.assign(state, {
panelResizing: resizing,
});
},
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
Object.assign(entry.lastCommit, {
id: lastCommit.commit.id,
......
......@@ -19,4 +19,5 @@ export default () => ({
projects: {},
leftPanelCollapsed: false,
rightPanelCollapsed: true,
panelResizing: false,
});
......@@ -96,14 +96,15 @@ export default class Job {
// eslint-disable-next-line class-methods-use-this
canScroll() {
return this.$document.height() > this.$window.height();
return $(document).height() > $(window).height();
}
toggleScroll() {
const currentPosition = this.$document.scrollTop();
const scrollHeight = this.$document.height();
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = this.$window.height();
const windowHeight = $(window).height();
if (this.canScroll()) {
if (currentPosition > 0 &&
(scrollHeight - currentPosition !== windowHeight)) {
......@@ -127,18 +128,22 @@ export default class Job {
this.toggleDisableButton(this.$scrollBottomBtn, true);
}
}
// eslint-disable-next-line class-methods-use-this
isScrolledToBottom() {
const currentPosition = this.$document.scrollTop();
const scrollHeight = this.$document.height();
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
const windowHeight = this.$window.height();
return scrollHeight - currentPosition === windowHeight;
}
// eslint-disable-next-line class-methods-use-this
scrollDown() {
this.$document.scrollTop(this.$document.height());
const $document = $(document);
$document.scrollTop($document.height());
}
scrollToBottom() {
......@@ -148,7 +153,7 @@ export default class Job {
}
scrollToTop() {
this.$document.scrollTop(0);
$(document).scrollTop(0);
this.hasBeenScrolled = true;
this.toggleScroll();
}
......
import { timeFormat as time } from 'd3-time-format';
import { timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear } from 'd3-time';
import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from 'd3-time';
import { bisector } from 'd3-array';
const d3 = { time, bisector, timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear };
const d3 = {
time,
bisector,
timeSecond,
timeMinute,
timeHour,
timeDay,
timeWeek,
timeMonth,
timeYear,
};
export const dateFormat = d3.time('%b %-d, %Y');
export const timeFormat = d3.time('%-I:%M%p');
......
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
import Cookies from 'js-cookie';
import Flash from '../flash';
import { getPagePath } from '../lib/utils/common_utils';
......@@ -7,6 +8,8 @@ import { getPagePath } from '../lib/utils/common_utils';
constructor({ form } = {}) {
this.onSubmitForm = this.onSubmitForm.bind(this);
this.form = form || $('.edit-user');
this.newRepoActivated = Cookies.get('new_repo');
this.setRepoRadio();
this.bindEvents();
this.initAvatarGlCrop();
}
......@@ -25,6 +28,7 @@ import { getPagePath } from '../lib/utils/common_utils';
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
......@@ -82,6 +86,23 @@ import { getPagePath } from '../lib/utils/common_utils';
}
});
}
setNewRepoCookie() {
if (this.value === 'off') {
Cookies.remove('new_repo');
} else {
Cookies.set('new_repo', true, { expires_in: 365 });
}
}
setRepoRadio() {
const multiEditRadios = $('input[name="user[multi_file]"]');
if (this.newRepoActivated || this.newRepoActivated === 'true') {
multiEditRadios.filter('[value=on]').prop('checked', true);
} else {
multiEditRadios.filter('[value=off]').prop('checked', true);
}
}
}
$(function() {
......
<script>
export default {
props: {
startSize: {
type: Number,
required: true,
},
side: {
type: String,
required: true,
},
minSize: {
type: Number,
required: false,
default: 0,
},
maxSize: {
type: Number,
required: false,
default: Number.MAX_VALUE,
},
enabled: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
size: this.startSize,
};
},
computed: {
className() {
return `drag${this.side}`;
},
cursorStyle() {
if (this.enabled) {
return { cursor: 'ew-resize' };
}
return {};
},
},
methods: {
resetSize(e) {
e.preventDefault();
this.size = this.startSize;
this.$emit('update:size', this.size);
},
startDrag(e) {
if (this.enabled) {
e.preventDefault();
this.startPos = e.clientX;
this.currentStartSize = this.size;
document.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.endDrag, { once: true });
this.$emit('resize-start', this.size);
}
},
drag(e) {
e.preventDefault();
let moved = e.clientX - this.startPos;
if (this.side === 'left') moved = -moved;
let newSize = this.currentStartSize + moved;
if (newSize < this.minSize) {
newSize = this.minSize;
} else if (newSize > this.maxSize) {
newSize = this.maxSize;
}
this.size = newSize;
this.$emit('update:size', newSize);
},
endDrag(e) {
e.preventDefault();
document.removeEventListener('mousemove', this.drag);
this.$emit('resize-end', this.size);
},
},
};
</script>
<template>
<div
class="dragHandle"
:class="className"
:style="cursorStyle"
@mousedown="startDrag"
@dblclick="resetSize"
></div>
</template>
......@@ -75,7 +75,7 @@
vertical-align: top;
&.s16 { font-size: 12px; line-height: 1.33; }
&.s24 { font-size: 14px; line-height: 1.8; }
&.s24 { font-size: 13px; line-height: 1.8; }
&.s26 { font-size: 20px; line-height: 1.33; }
&.s32 { font-size: 20px; line-height: 30px; }
&.s40 { font-size: 16px; line-height: 38px; }
......
......@@ -23,6 +23,7 @@
.context-header {
position: relative;
margin-right: 2px;
width: $contextual-sidebar-width;
a {
transition: padding $sidebar-transition-duration;
......
......@@ -520,7 +520,7 @@
.header-user {
.dropdown-menu-nav {
width: auto;
min-width: 140px;
min-width: 160px;
margin-top: 4px;
color: $gl-text-color;
left: auto;
......
......@@ -126,10 +126,8 @@ ul.content-list {
}
.description {
p {
@include str-truncated;
margin-bottom: 0;
}
@include str-truncated;
color: $gl-text-color-secondary;
}
.controls {
......@@ -321,7 +319,7 @@ ul.indent-list {
border: 2px solid $white-normal;
&.identicon {
line-height: 30px;
line-height: 15px;
}
}
}
......@@ -355,14 +353,19 @@ ul.indent-list {
.folder-caret {
width: 15px;
svg {
margin-bottom: 2px;
}
}
.item-type-icon {
margin-top: 2px;
width: 20px;
}
> .group-row:not(.has-children) {
.folder-caret .fa {
.folder-caret {
opacity: 0;
}
}
......@@ -445,12 +448,61 @@ ul.indent-list {
.avatar-container > a {
width: 100%;
text-decoration: none;
}
&.has-more-items {
display: block;
padding: 20px 10px;
}
.stats {
position: relative;
line-height: 46px;
> span {
display: inline-flex;
align-items: center;
height: 16px;
min-width: 30px;
}
> span:last-child {
margin-right: 0;
}
.stat-value {
margin: 2px 0 0 5px;
}
}
.controls {
margin-left: 5px;
> .btn {
margin-right: $btn-xs-side-margin;
}
}
}
.project-row-contents .stats {
line-height: inherit;
> span:first-child {
margin-left: 25px;
}
.item-visibility {
margin-right: 0;
}
.last-updated {
position: absolute;
right: 12px;
min-width: 250px;
text-align: right;
color: $gl-text-color-secondary;
}
}
}
......@@ -462,12 +514,12 @@ ul.indent-list {
ul.group-list-tree {
li.group-row {
&.has-description .title {
line-height: inherit;
> .group-row-contents .title {
line-height: $list-text-height;
}
&:not(.has-description) .title {
line-height: $list-text-height;
&.has-description > .group-row-contents .title {
line-height: inherit;
}
}
}
......
......@@ -178,6 +178,10 @@
font-weight: inherit;
}
dd {
margin-left: $gl-padding;
}
ul,
ol {
padding: 0;
......
......@@ -760,3 +760,8 @@ Popup
$popup-triangle-size: 15px;
$popup-triangle-border-size: 1px;
$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
/*
Multi file editor
*/
$border-color-settings: #e1e1e1;
......@@ -20,6 +20,22 @@
}
}
.multi-file-editor-options {
label {
margin-right: 20px;
text-align: center;
}
.preview {
font-size: 0;
img {
border: 1px solid $border-color-settings;
border-radius: 4px;
}
}
}
.application-theme {
label {
margin-right: 20px;
......
......@@ -41,10 +41,6 @@
}
}
.with-performance-bar .ide-view {
height: calc(100vh - #{$header-height});
}
.ide-file-list {
flex: 1;
......@@ -247,12 +243,13 @@ table.table tr td.multi-file-table-name {
.multi-file-commit-panel {
display: flex;
position: relative;
flex-direction: column;
height: 100%;
width: 290px;
padding: 0;
background-color: $gray-light;
border-left: 1px solid $white-dark;
padding-right: 3px;
.projects-sidebar {
display: flex;
......@@ -508,3 +505,30 @@ table.table tr td.multi-file-table-name {
margin-top: $header-height;
margin-bottom: 0;
}
.with-performance-bar {
.ide-flash-container.flash-container {
margin-top: $header-height + $performance-bar-height;
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height});
}
}
.dragHandle {
position: absolute;
top: 0;
bottom: 0;
width: 3px;
background-color: $white-dark;
&.dragright {
right: 0;
}
&.dragleft {
left: 0;
}
}
......@@ -410,7 +410,7 @@ module ProjectsHelper
end
def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil)
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name.downcase }
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
project_new_blob_path(
project,
project.default_branch || 'master',
......
module DeploymentPlatform
def deployment_platform(environment: nil)
@deployment_platform ||=
find_cluster_platform_kubernetes ||
find_kubernetes_service_integration ||
build_cluster_and_deployment_platform
end
private
def find_cluster_platform_kubernetes
clusters.find_by(enabled: true)&.platform_kubernetes
end
def find_kubernetes_service_integration
services.deployment.reorder(nil).find_by(active: true)
end
def build_cluster_and_deployment_platform
return unless kubernetes_service_template
cluster = ::Clusters::Cluster.create(cluster_attributes_from_service_template)
cluster.platform_kubernetes if cluster.persisted?
end
def kubernetes_service_template
@kubernetes_service_template ||= KubernetesService.active.find_by_template
end
def cluster_attributes_from_service_template
{
name: 'kubernetes-template',
projects: [self],
provider_type: :user,
platform_type: :kubernetes,
platform_kubernetes_attributes: platform_kubernetes_attributes_from_service_template
}
end
def platform_kubernetes_attributes_from_service_template
{
api_url: kubernetes_service_template.api_url,
ca_pem: kubernetes_service_template.ca_pem,
token: kubernetes_service_template.token,
namespace: kubernetes_service_template.namespace
}
end
end
......@@ -48,7 +48,18 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :target, -> {
# If the association for "target" defines an "author" association we want to
# eager-load this so Banzai & friends don't end up performing N+1 queries to
# get the authors of notes, issues, etc.
if reflections['events'].active_record.reflect_on_association(:author)
includes(:author)
else
self
end
}, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :push_event_payload
# Callbacks
......
......@@ -19,6 +19,7 @@ class Project < ActiveRecord::Base
include Routable
include GroupDescendant
include Gitlab::SQL::Pattern
include DeploymentPlatform
# EE specific modules
prepend EE::Project
......@@ -649,7 +650,7 @@ class Project < ActiveRecord::Base
end
def import?
external_import? || forked? || gitlab_project_import?
external_import? || forked? || gitlab_project_import? || bare_repository_import?
end
def no_import?
......@@ -689,6 +690,10 @@ class Project < ActiveRecord::Base
Gitlab::UrlSanitizer.new(import_url).masked_url
end
def bare_repository_import?
import_type == 'bare_repository'
end
def gitlab_project_import?
import_type == 'gitlab_project'
end
......@@ -910,11 +915,6 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
def deployment_platform(environment: nil)
@deployment_platform ||= clusters.find_by(enabled: true)&.platform_kubernetes
@deployment_platform ||= services.where(category: :deployment).reorder(nil).find_by(active: true)
end
def monitoring_services
services.where(category: :monitoring)
end
......
......@@ -44,6 +44,7 @@ class Service < ActiveRecord::Base
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
scope :deployment, -> { where(category: 'deployment') }
default_value_for :category, 'common'
......@@ -277,6 +278,10 @@ class Service < ActiveRecord::Base
nil
end
def self.find_by_template
find_by(template: true)
end
private
def cache_project_has_external_issue_tracker
......
......@@ -58,11 +58,7 @@ module Projects
after_create_actions if @project.persisted?
if @project.errors.empty?
@project.import_schedule if @project.import?
else
fail(error: @project.errors.full_messages.join(', '))
end
import_schedule
@project
rescue ActiveRecord::RecordInvalid => e
......@@ -167,5 +163,15 @@ module Projects
@project.path = @project.name.dup.parameterize
end
end
private
def import_schedule
if @project.errors.empty?
@project.import_schedule if @project.import? && !@project.bare_repository_import?
else
fail(error: @project.errors.full_messages.join(', '))
end
end
end
end
......@@ -56,6 +56,8 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
%li
= link_to "Turn on multi edit", profile_preferences_path
- if current_user
%li
= link_to "Help", help_path
......
......@@ -17,10 +17,6 @@
Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'}
- if current_user.two_factor_enabled?
= link_to 'Manage two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-info'
= link_to 'Disable', profile_two_factor_auth_path,
method: :delete,
data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." },
class: 'btn btn-danger'
- else
.append-bottom-10
= link_to 'Enable two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-success'
......
......@@ -3,6 +3,23 @@
= render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-4
%h4.prepend-top-0
GitLab multi file editor
%p Unlock an additional editing experience which makes it possible to edit and commit multiple files
.col-lg-8.multi-file-editor-options
= label_tag do
.preview.append-bottom-10= image_tag "multi-editor-off.png"
= f.radio_button :multi_file, "off", checked: true
Off
= label_tag do
.preview.append-bottom-10= image_tag "multi-editor-on.png"
= f.radio_button :multi_file, "on", checked: false
On
.col-sm-12
%hr
.col-lg-4.application-theme
%h4.prepend-top-0
GitLab navigation theme
......
- page_title 'Two-Factor Authentication', 'Account'
- add_to_breadcrumbs("Account", profile_account_path)
- add_to_breadcrumbs("Two-Factor Authentication", profile_account_path)
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
......@@ -18,7 +18,12 @@
Use an app on your mobile device to enable two-factor authentication (2FA).
.col-lg-8
- if current_user.two_factor_otp_enabled?
= icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page."
%p
You've already enabled two-factor authentication using mobile authenticator applications. In order to register a different device, you must first disable two-factor authentication.
= link_to 'Disable two-factor authentication', profile_two_factor_auth_path,
method: :delete,
data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." },
class: 'btn btn-danger'
- else
%p
Download the Google Authenticator application from App Store or Google Play Store and scan this code.
......
......@@ -5,22 +5,24 @@
= link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions'
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
%span.input-group-addon Source
%span.input-group-addon
= s_("CompareBranches|Source")
= hidden_field_tag :to, params[:to]
= button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
.dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag'
.dropdown-toggle-text.str-truncated= params[:to] || _("Select branch/tag")
= render 'shared/ref_dropdown'
.compare-ellipsis.inline ...
.form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
.input-group.inline-input-group
%span.input-group-addon Target
%span.input-group-addon
= s_("CompareBranches|Target")
= hidden_field_tag :from, params[:from]
= button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
.dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag'
.dropdown-toggle-text.str-truncated= params[:from] || _("Select branch/tag")
= render 'shared/ref_dropdown'
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
= button_tag s_("CompareBranches|Compare"), class: "btn btn-create commits-compare-btn"
- if @merge_request.present?
= link_to "View open merge request", project_merge_request_path(@project, @merge_request), class: 'prepend-left-10 btn'
= link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'prepend-left-10 btn'
- elsif create_mr_button?
= link_to "Create merge request", create_mr_path, class: 'prepend-left-10 btn'
= link_to _("Create merge request"), create_mr_path, class: 'prepend-left-10 btn'
......@@ -3,22 +3,16 @@
- page_title "Compare"
%div{ class: container_class }
%h3.page-title
= _("Compare Git revisions")
.sub-header-block
Compare Git revisions.
%br
Choose a branch/tag (e.g.
= succeed ')' do
- example_master = capture do
%code.ref-name master
or enter a commit SHA (e.g.
= succeed ')' do
- example_sha = capture do
%code.ref-name 4eedf23
to see what's changed or to create a merge request.
= (_("Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request.") % { master: example_master, sha: example_sha }).html_safe
%br
Changes are shown as if the
%b source
revision was being merged into the
%b target
revision.
= (_("Changes are shown as if the <b>source</b> revision was being merged into the <b>target</b> revision.")).html_safe
.prepend-top-20
= render "form"
- @no_container = true
- add_to_breadcrumbs "Compare Revisions", project_compare_index_path(@project)
- add_to_breadcrumbs _("Compare Revisions"), project_compare_index_path(@project)
- page_title "#{params[:from]}...#{params[:to]}"
%div{ class: container_class }
......@@ -13,12 +13,13 @@
.light-well
.center
%h4
There isn't anything to compare.
= s_("CompareBranches|There isn't anything to compare.")
%p.slead
- if params[:to] == params[:from]
%span.ref-name= params[:from]
and
%span.ref-name= params[:to]
are the same.
- source_branch = capture do
%span.ref-name= params[:from]
- target_branch = capture do
%span.ref-name= params[:to]
= (s_("CompareBranches|%{source_branch} and %{target_branch} are the same.") % { source_branch: source_branch, target_branch: target_branch }).html_safe
- else
You'll need to use different branch names to get a valid comparison.
= _("You'll need to use different branch names to get a valid comparison.")
---
title: Fix ANSI 256 bold colors in pipelines job output
merge_request:
author:
type: fixed
---
title: Fix API endpoints to edit wiki pages where project belongs to a group
merge_request: 16170
author:
type: fixed
---
title: Update groups tree to use GitLab SVG icons, add last updated at information
for projects
merge_request: 15980
author:
type: changed
---
title: Allow automatic creation of Kubernetes Integration from template
merge_request: 16104
author:
type: added
---
title: Fix gitlab-rake gitlab:import:repos import schedule
merge_request: 16115
author:
type: fixed
---
title: Fix viewing merge request diffs where the underlying blobs are unavailable
merge_request:
author:
type: fixed
---
title: Use a background migration for issues.closed_at
merge_request:
author:
type: other
---
title: Eager load event target authors whenever possible
merge_request:
author:
type: performance
---
title: Move 2FA disable button
merge_request: 16177
author: George Tsiolis
type: fixed
---
title: Added option to user preferences to enable the multi file editor
merge_request: 16056
author:
type: added
---
title: Add i18n helpers to branch comparison view
merge_request: 16031
author: James Ramsay
type: added
---
title: Fix inconsistent downcase of filenames in prefilled `Add` commit messages
merge_request: 16232
author: James Ramsay
type: fixed
---
title: Prevent excessive DB load due to faulty DeleteConflictingRedirectRoutes background
migration
merge_request: 16205
author:
type: fixed
---
title: Speed up generation of commit stats by using Rugged native methods
merge_request:
author:
type: performance
---
title: Avoid leaving a push event empty if payload cannot be created
merge_request:
author:
type: fixed
......@@ -2,36 +2,12 @@
# for more information on how to write migrations for GitLab.
class DeleteConflictingRedirectRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'DeleteConflictingRedirectRoutesRange'.freeze
BATCH_SIZE = 200 # At 200, I expect under 20s per batch, which is under our query timeout of 60s.
DELAY_INTERVAL = 12.seconds
disable_ddl_transaction!
class Route < ActiveRecord::Base
include EachBatch
self.table_name = 'routes'
end
def up
say opening_message
queue_background_migration_jobs_by_range_at_intervals(Route, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
# No-op.
# See https://gitlab.com/gitlab-com/infrastructure/issues/3460#note_53223252
end
def down
# nothing
end
def opening_message
<<~MSG
Clean up redirect routes that conflict with regular routes.
See initial bug fix:
https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13357
MSG
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable Migration/Datetime
class ScheduleIssuesClosedAtTypeChange < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class Issue < ActiveRecord::Base
self.table_name = 'issues'
include EachBatch
def self.to_migrate
where('closed_at IS NOT NULL')
end
end
def up
return unless migrate_column_type?
change_column_type_using_background_migration(
Issue.to_migrate,
:closed_at,
:datetime_with_timezone
)
end
def down
return if migrate_column_type?
change_column_type_using_background_migration(
Issue.to_migrate,
:closed_at,
:datetime
)
end
def migrate_column_type?
# Some environments may have already executed the previous version of this
# migration, thus we don't need to migrate those environments again.
column_for('issues', 'closed_at').type == :datetime
end
end
......@@ -195,6 +195,63 @@ end
And that's it, we're done!
## Changing Column Types For Large Tables
While `change_column_type_concurrently` can be used for changing the type of a
column without downtime it doesn't work very well for large tables. Because all
of the work happens in sequence the migration can take a very long time to
complete, preventing a deployment from proceeding.
`change_column_type_concurrently` can also produce a lot of pressure on the
database due to it rapidly updating many rows in sequence.
To reduce database pressure you should instead use
`change_column_type_using_background_migration` when migrating a column in a
large table (e.g. `issues`). This method works similar to
`change_column_type_concurrently` but uses background migration to spread the
work / load over a longer time period, without slowing down deployments.
Usage of this method is fairly simple:
```ruby
class ExampleMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
class Issue < ActiveRecord::Base
self.table_name = 'issues'
include EachBatch
def self.to_migrate
where('closed_at IS NOT NULL')
end
end
def up
change_column_type_using_background_migration(
Issue.to_migrate,
:closed_at,
:datetime_with_timezone
)
end
def down
change_column_type_using_background_migration(
Issue.to_migrate,
:closed_at,
:datetime
)
end
end
```
This would change the type of `issues.closed_at` to `timestamp with time zone`.
Keep in mind that the relation passed to
`change_column_type_using_background_migration` _must_ include `EachBatch`,
otherwise it will raise a `TypeError`.
## Adding Indexes
Adding indexes is an expensive process that blocks INSERT and UPDATE queries for
......
......@@ -9,11 +9,11 @@ should be deployed, upgraded, and configured.
## Chart Overview
* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small to medium deployments. The chart is in beta and will be deprecated by the [cloud native GitLab chart](#cloud-native-gitlab-chart).
* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small deployments. The chart is in beta and will be deprecated by the [cloud native GitLab chart](#cloud-native-gitlab-chart).
* **[Cloud Native GitLab Chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)**: The next generation GitLab chart, currently in development. Will support large deployments with horizontal scaling of individual GitLab components.
* Other Charts
* [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner.
* [Advanced GitLab Installation](gitlab_chart.md): Deprecated, being replaced by the [cloud native GitLab chart](#cloud-native-gitlab-chart). Provides additional deployment options, but provides less functionality out-of-the-box.
* [Advanced GitLab Installation](gitlab_chart.md): Deprecated, being replaced by the [cloud native GitLab chart](#cloud-native-gitlab-chart). Provides additional deployment options, but provides less functionality out-of-the-box.
* [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart.
## GitLab-Omnibus Chart (Recommended)
......
......@@ -20,6 +20,7 @@ project in an easy and automatic way:
1. [Auto Test](#auto-test)
1. [Auto Code Quality](#auto-code-quality)
1. [Auto SAST (Static Application Security Testing)](#auto-sast)
1. [Auto Browser Performance Testing](#auto-browser-performance-testing)
1. [Auto Review Apps](#auto-review-apps)
1. [Auto Deploy](#auto-deploy)
1. [Auto Monitoring](#auto-monitoring)
......@@ -220,6 +221,20 @@ check out.
Any security warnings are also [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html).
### Auto Browser Performance Testing
> Introduced in [GitLab Enterprise Edition Premium][ee] 10.4.
Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) to measure the performance of a web page. A JSON report is created and uploaded as an artifact, which includes the overall performance score for each page. By default, the root page of Review and Production environments will be tested. If you would like to add additional URL's to test, simply add the paths to a file named `.gitlab-urls.txt` in the root directory, one per line. For example:
```
/
/features
/direction
```
In GitLab Enterprise Edition Premium, performance differences between the source and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
### Auto Review Apps
NOTE: **Note:**
......
......@@ -104,6 +104,7 @@ added directly to your configured cluster. Those applications are needed for
| ----------- | :------------: | ----------- |
| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications |
## Enabling or disabling the Cluster integration
......
---
last_updated: 2017-09-25
last_updated: 2017-12-28
---
CAUTION: **Warning:**
......
......@@ -72,7 +72,7 @@ module API
end
def wiki_page
page = user_project.wiki.find_page(params[:slug])
page = ProjectWiki.new(user_project, current_user).find_page(params[:slug])
page || not_found!('Wiki Page')
end
......
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Background migration for cleaning up a concurrent column rename.
class CleanupConcurrentTypeChange
include Database::MigrationHelpers
RESCHEDULE_DELAY = 10.minutes
# table - The name of the table the migration is performed for.
# old_column - The name of the old (to drop) column.
# new_column - The name of the new column.
def perform(table, old_column, new_column)
return unless column_exists?(:issues, new_column)
rows_to_migrate = define_model_for(table)
.where(new_column => nil)
.where
.not(old_column => nil)
if rows_to_migrate.any?
BackgroundMigrationWorker.perform_in(
RESCHEDULE_DELAY,
'CleanupConcurrentTypeChange',
[table, old_column, new_column]
)
else
cleanup_concurrent_column_type_change(table, old_column)
end
end
# These methods are necessary so we can re-use the migration helpers in
# this class.
def connection
ActiveRecord::Base.connection
end
def method_missing(name, *args, &block)
connection.__send__(name, *args, &block) # rubocop: disable GitlabSecurity/PublicSend
end
def respond_to_missing?(*args)
connection.respond_to?(*args) || super
end
def define_model_for(table)
Class.new(ActiveRecord::Base) do
self.table_name = table
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# CopyColumn is a simple (reusable) background migration that can be used to
# update the value of a column based on the value of another column in the
# same table.
#
# For this background migration to work the table that is migrated _has_ to
# have an `id` column as the primary key.
class CopyColumn
# table - The name of the table that contains the columns.
# copy_from - The column containing the data to copy.
# copy_to - The column to copy the data to.
# start_id - The start ID of the range of rows to update.
# end_id - The end ID of the range of rows to update.
def perform(table, copy_from, copy_to, start_id, end_id)
return unless connection.column_exists?(table, copy_to)
quoted_table = connection.quote_table_name(table)
quoted_copy_from = connection.quote_column_name(copy_from)
quoted_copy_to = connection.quote_column_name(copy_to)
# We're using raw SQL here since this job may be frequently executed. As
# a result dynamically defining models would lead to many unnecessary
# schema information queries.
connection.execute <<-SQL.strip_heredoc
UPDATE #{quoted_table}
SET #{quoted_copy_to} = #{quoted_copy_from}
WHERE id BETWEEN #{start_id} AND #{end_id}
SQL
end
def connection
ActiveRecord::Base.connection
end
end
end
end
# frozen_string_literal: true
# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class DeleteConflictingRedirectRoutesRange
class Route < ActiveRecord::Base
self.table_name = 'routes'
end
class RedirectRoute < ActiveRecord::Base
self.table_name = 'redirect_routes'
end
# start_id - The start ID of the range of events to process
# end_id - The end ID of the range to process.
def perform(start_id, end_id)
return unless migrate?
conflicts = RedirectRoute.where(routes_match_redirects_clause(start_id, end_id))
num_rows = conflicts.delete_all
Rails.logger.info("Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange [#{start_id}, #{end_id}] - Deleted #{num_rows} redirect routes that were conflicting with routes.")
end
def migrate?
Route.table_exists? && RedirectRoute.table_exists?
end
def routes_match_redirects_clause(start_id, end_id)
<<~ROUTES_MATCH_REDIRECTS
EXISTS (
SELECT 1 FROM routes
WHERE (
LOWER(redirect_routes.path) = LOWER(routes.path)
OR LOWER(redirect_routes.path) LIKE LOWER(CONCAT(routes.path, '/%'))
)
AND routes.id BETWEEN #{start_id} AND #{end_id}
)
ROUTES_MATCH_REDIRECTS
# No-op.
# See https://gitlab.com/gitlab-com/infrastructure/issues/3460#note_53223252
end
end
end
......
......@@ -128,8 +128,14 @@ module Gitlab
end
def process_event(event)
replicate_event(event)
create_push_event_payload(event) if event.push_event?
ActiveRecord::Base.transaction do
replicate_event(event)
create_push_event_payload(event) if event.push_event?
end
rescue ActiveRecord::InvalidForeignKey => e
# A foreign key error means the associated event was removed. In this
# case we'll just skip migrating the event.
Rails.logger.error("Unable to migrate event #{event.id}: #{e}")
end
def replicate_event(event)
......@@ -137,9 +143,6 @@ module Gitlab
.with_indifferent_access.except(:title, :data)
EventForMigration.create!(new_attributes)
rescue ActiveRecord::InvalidForeignKey
# A foreign key error means the associated event was removed. In this
# case we'll just skip migrating the event.
end
def create_push_event_payload(event)
......@@ -156,9 +159,6 @@ module Gitlab
ref: event.trimmed_ref_name,
commit_title: event.commit_title
)
rescue ActiveRecord::InvalidForeignKey
# A foreign key error means the associated event was removed. In this
# case we'll just skip migrating the event.
end
def find_events(start_id, end_id)
......
......@@ -55,7 +55,8 @@ module Gitlab
name: project_name,
path: project_name,
skip_disk_validation: true,
import_type: 'gitlab_project',
skip_wiki: bare_repo.wiki_exists?,
import_type: 'bare_repository',
namespace_id: group&.id).execute
if project.persisted? && mv_repo(project)
......
......@@ -234,7 +234,7 @@ module Gitlab
# Most terminals show bold colored text in the light color variant
# Let's mimic that here
if @style_mask & STYLE_SWITCHES[:bold] != 0
fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1')
fg_color.sub!(/fg-([a-z]{2,}+)/, 'fg-l-\1')
end
css_classes << fg_color
end
......
......@@ -385,10 +385,27 @@ module Gitlab
# necessary since we copy over old values further down.
change_column_default(table, new, old_col.default) if old_col.default
trigger_name = rename_trigger_name(table, old, new)
install_rename_triggers(table, old, new)
update_column_in_batches(table, new, Arel::Table.new(table)[old])
change_column_null(table, new, false) unless old_col.null
copy_indexes(table, old, new)
copy_foreign_keys(table, old, new)
end
# Installs triggers in a table that keep a new column in sync with an old
# one.
#
# table - The name of the table to install the trigger in.
# old_column - The name of the old column.
# new_column - The name of the new column.
def install_rename_triggers(table, old_column, new_column)
trigger_name = rename_trigger_name(table, old_column, new_column)
quoted_table = quote_table_name(table)
quoted_old = quote_column_name(old)
quoted_new = quote_column_name(new)
quoted_old = quote_column_name(old_column)
quoted_new = quote_column_name(new_column)
if Database.postgresql?
install_rename_triggers_for_postgresql(trigger_name, quoted_table,
......@@ -397,13 +414,6 @@ module Gitlab
install_rename_triggers_for_mysql(trigger_name, quoted_table,
quoted_old, quoted_new)
end
update_column_in_batches(table, new, Arel::Table.new(table)[old])
change_column_null(table, new, false) unless old_col.null
copy_indexes(table, old, new)
copy_foreign_keys(table, old, new)
end
# Changes the type of a column concurrently.
......@@ -455,6 +465,97 @@ module Gitlab
remove_column(table, old)
end
# Changes the column type of a table using a background migration.
#
# Because this method uses a background migration it's more suitable for
# large tables. For small tables it's better to use
# `change_column_type_concurrently` since it can complete its work in a
# much shorter amount of time and doesn't rely on Sidekiq.
#
# Example usage:
#
# class Issue < ActiveRecord::Base
# self.table_name = 'issues'
#
# include EachBatch
#
# def self.to_migrate
# where('closed_at IS NOT NULL')
# end
# end
#
# change_column_type_using_background_migration(
# Issue.to_migrate,
# :closed_at,
# :datetime_with_timezone
# )
#
# Reverting a migration like this is done exactly the same way, just with
# a different type to migrate to (e.g. `:datetime` in the above example).
#
# relation - An ActiveRecord relation to use for scheduling jobs and
# figuring out what table we're modifying. This relation _must_
# have the EachBatch module included.
#
# column - The name of the column for which the type will be changed.
#
# new_type - The new type of the column.
#
# batch_size - The number of rows to schedule in a single background
# migration.
#
# interval - The time interval between every background migration.
def change_column_type_using_background_migration(
relation,
column,
new_type,
batch_size: 10_000,
interval: 10.minutes
)
unless relation.model < EachBatch
raise TypeError, 'The relation must include the EachBatch module'
end
temp_column = "#{column}_for_type_change"
table = relation.table_name
max_index = 0
add_column(table, temp_column, new_type)
install_rename_triggers(table, column, temp_column)
# Schedule the jobs that will copy the data from the old column to the
# new one.
relation.each_batch(of: batch_size) do |batch, index|
start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
max_index = index
BackgroundMigrationWorker.perform_in(
index * interval,
'CopyColumn',
[table, column, temp_column, start_id, end_id]
)
end
# Schedule the renaming of the column to happen (initially) 1 hour after
# the last batch finished.
BackgroundMigrationWorker.perform_in(
(max_index * interval) + 1.hour,
'CleanupConcurrentTypeChange',
[table, column, temp_column]
)
if perform_background_migration_inline?
# To ensure the schema is up to date immediately we perform the
# migration inline in dev / test environments.
Gitlab::BackgroundMigration.steal('CopyColumn')
Gitlab::BackgroundMigration.steal('CleanupConcurrentTypeChange')
end
end
def perform_background_migration_inline?
Rails.env.test? || Rails.env.development?
end
# Performs a concurrent column rename when using PostgreSQL.
def install_rename_triggers_for_postgresql(trigger, table, old, new)
execute <<-EOF.strip_heredoc
......
......@@ -116,8 +116,10 @@ module Gitlab
new_content_sha || old_content_sha
end
# Use #itself to check the value wrapped by a BatchLoader instance, rather
# than if the BatchLoader instance itself is falsey.
def blob
new_blob || old_blob
new_blob&.itself || old_blob&.itself
end
attr_writer :highlighted_diff_lines
......@@ -173,7 +175,7 @@ module Gitlab
end
def binary?
has_binary_notice? || old_blob&.binary? || new_blob&.binary?
has_binary_notice? || try_blobs(:binary?)
end
def text?
......@@ -181,15 +183,15 @@ module Gitlab
end
def external_storage_error?
old_blob&.external_storage_error? || new_blob&.external_storage_error?
try_blobs(:external_storage_error?)
end
def stored_externally?
old_blob&.stored_externally? || new_blob&.stored_externally?
try_blobs(:stored_externally?)
end
def external_storage
old_blob&.external_storage || new_blob&.external_storage
try_blobs(:external_storage)
end
def content_changed?
......@@ -204,15 +206,15 @@ module Gitlab
end
def size
[old_blob&.size, new_blob&.size].compact.sum
valid_blobs.map(&:size).sum
end
def raw_size
[old_blob&.raw_size, new_blob&.raw_size].compact.sum
valid_blobs.map(&:raw_size).sum
end
def raw_binary?
old_blob&.raw_binary? || new_blob&.raw_binary?
try_blobs(:raw_binary?)
end
def raw_text?
......@@ -235,6 +237,19 @@ module Gitlab
private
# The blob instances are instances of BatchLoader, which means calling
# &. directly on them won't work. Object#try also won't work, because Blob
# doesn't inherit from Object, but from BasicObject (via SimpleDelegator).
def try_blobs(meth)
old_blob&.itself&.public_send(meth) || new_blob&.itself&.public_send(meth)
end
# We can't use #compact for the same reason we can't use &., but calling
# #nil? explicitly does work because it is proxied to the blob itself.
def valid_blobs
[old_blob, new_blob].reject(&:nil?)
end
def text_position_properties(line)
{ old_line: line.old_line, new_line: line.new_line }
end
......
......@@ -34,13 +34,8 @@ module Gitlab
def rugged_stats(commit)
diff = commit.rugged_diff_from_parent
diff.each_patch do |p|
# TODO: Use the new Rugged convenience methods when they're released
@additions += p.stat[0]
@deletions += p.stat[1]
@total += p.changes
end
_files_changed, @additions, @deletions = diff.stat
@total = @additions + @deletions
end
end
end
......
......@@ -2,6 +2,9 @@ module Gitlab
module Git
class GitlabProjects
include Gitlab::Git::Popen
include Gitlab::Utils::StrongMemoize
ShardNameNotFoundError = Class.new(StandardError)
# Absolute path to directory where repositories are stored.
# Example: /home/git/repositories
......@@ -97,22 +100,13 @@ module Gitlab
end
def fork_repository(new_shard_path, new_repository_relative_path)
from_path = repository_absolute_path
to_path = File.join(new_shard_path, new_repository_relative_path)
# The repository cannot already exist
if File.exist?(to_path)
logger.error "fork-repository failed: destination repository <#{to_path}> already exists."
return false
Gitlab::GitalyClient.migrate(:fork_repository) do |is_enabled|
if is_enabled
gitaly_fork_repository(new_shard_path, new_repository_relative_path)
else
git_fork_repository(new_shard_path, new_repository_relative_path)
end
end
# Ensure the namepsace / hashed storage directory exists
FileUtils.mkdir_p(File.dirname(to_path), mode: 0770)
logger.info "Forking repository from <#{from_path}> to <#{to_path}>."
cmd = %W(git clone --bare --no-local -- #{from_path} #{to_path})
run(cmd, nil) && Gitlab::Git::Repository.create_hooks(to_path, global_hooks_path)
end
def fetch_remote(name, timeout, force:, tags:, ssh_key: nil, known_hosts: nil)
......@@ -253,6 +247,48 @@ module Gitlab
known_hosts_file&.close!
script&.close!
end
private
def shard_name
strong_memoize(:shard_name) do
shard_name_from_shard_path(shard_path)
end
end
def shard_name_from_shard_path(shard_path)
Gitlab.config.repositories.storages.find { |_, info| info['path'] == shard_path }&.first ||
raise(ShardNameNotFoundError, "no shard found for path '#{shard_path}'")
end
def git_fork_repository(new_shard_path, new_repository_relative_path)
from_path = repository_absolute_path
to_path = File.join(new_shard_path, new_repository_relative_path)
# The repository cannot already exist
if File.exist?(to_path)
logger.error "fork-repository failed: destination repository <#{to_path}> already exists."
return false
end
# Ensure the namepsace / hashed storage directory exists
FileUtils.mkdir_p(File.dirname(to_path), mode: 0770)
logger.info "Forking repository from <#{from_path}> to <#{to_path}>."
cmd = %W(git clone --bare --no-local -- #{from_path} #{to_path})
run(cmd, nil) && Gitlab::Git::Repository.create_hooks(to_path, global_hooks_path)
end
def gitaly_fork_repository(new_shard_path, new_repository_relative_path)
target_repository = Gitlab::Git::Repository.new(shard_name_from_shard_path(new_shard_path), new_repository_relative_path, nil)
raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil)
Gitlab::GitalyClient::RepositoryService.new(target_repository).fork_repository(raw_repository)
rescue GRPC::BadStatus => e
logger.error "fork-repository failed: #{e.message}"
false
end
end
end
end
......@@ -1691,6 +1691,7 @@ module Gitlab
cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list]
cmd << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << "--max-count=#{options[:max_count]}" if options[:max_count]
cmd += %W[--count #{options[:ref]}]
cmd += %W[-- #{options[:path]}] if options[:path].present?
......
......@@ -130,6 +130,7 @@ module Gitlab
request.after = Google::Protobuf::Timestamp.new(seconds: options[:after].to_i) if options[:after].present?
request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present?
request.path = options[:path] if options[:path].present?
request.max_count = options[:max_count] if options[:max_count].present?
GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count
end
......
......@@ -101,6 +101,7 @@ module Gitlab
request_enum.push(Gitaly::UserMergeBranchRequest.new(apply: true))
branch_update = response_enum.next.branch_update
return if branch_update.nil?
raise Gitlab::Git::CommitError.new('failed to apply merge to branch') unless branch_update.commit_id.present?
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update)
......
......@@ -81,6 +81,22 @@ module Gitlab
response.base.presence
end
def fork_repository(source_repository)
request = Gitaly::CreateForkRequest.new(
repository: @gitaly_repo,
source_repository: source_repository.gitaly_repository
)
GitalyClient.call(
@storage,
:repository_service,
:create_fork,
request,
remote_storage: source_repository.storage,
timeout: GitalyClient.default_timeout
)
end
def fetch_source_branch(source_repository, source_branch, local_ref)
request = Gitaly::FetchSourceBranchRequest.new(
repository: @gitaly_repo,
......
......@@ -12,7 +12,7 @@ module Gitlab
Gitaly::Repository.new(
storage_name: repository_storage,
relative_path: relative_path,
gl_repository: gl_repository,
gl_repository: gl_repository.to_s,
git_object_directory: git_object_directory.to_s,
git_alternate_object_directories: git_alternate_object_directories
)
......
......@@ -7,6 +7,7 @@ module Gitlab
module ImportSources
ImportSource = Struct.new(:name, :title, :importer)
# We exclude `bare_repository` here as it has no import class associated
ImportTable = [
ImportSource.new('github', 'GitHub', Gitlab::GithubImport::ParallelImporter),
ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer),
......
......@@ -8,7 +8,7 @@ end
module Gitlab
class Seeder
def self.quiet
mute_mailer unless Rails.env.test?
mute_mailer
SeedFu.quiet = true
......
......@@ -12,13 +12,13 @@ module QA
end
def ssh_key
<<~KEY.tr("\n", '')
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9
6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5
/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7
M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC
rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0
5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com
<<~KEY.delete("\n")
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9
6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5
/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7
M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC
rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0
5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com
KEY
end
end
......
#!/bin/sh
# Check if file exists with -f. Check if in in the gdk rook directory.
if [ ! -f ../GDK_ROOT ]; then
echo "Please run script from gitlab (e.g. gitlab-development-kit/gitlab) root directory."
exit 1
fi
PRECOMMIT=$(git rev-parse --git-dir)/hooks/pre-commit
# Check if symlink exists with -L. Check if script was already installed.
if [ -L $PRECOMMIT ]; then
echo "Pre-commit script already installed."
exit 1
fi
ln -s ./pre-commit $PRECOMMIT
echo "Pre-commit script installed successfully"
#!/bin/bash
mysql --user=root --host=mysql <<EOF
CREATE DATABASE IF NOT EXISTS gitlabhq_test;
CREATE DATABASE IF NOT EXISTS gitlabhq_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'gitlab'@'%';
GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
FLUSH PRIVILEGES;
......
#!/bin/sh
# Check if file exists with -f. Check if in in the gdk rook directory.
if [ ! -f ../GDK_ROOT ]; then
echo "Please run pre-commit from gitlab (e.g. gitlab-development-kit/gitlab) root directory."
exit 1
fi
jsfiles=$(git diff --cached --name-only --diff-filter=ACM "*.js" | tr '\n' ' ')
[ -z "$jsfiles" ] && exit 0
# Prettify all staged .js files
echo "$jsfiles" | xargs ./node_modules/.bin/prettier --write
# Add back the modified/prettified files to staging
echo "$jsfiles" | xargs git add
exit 0
......@@ -21,12 +21,14 @@ FactoryBot.define do
factory :rsa_key_2048 do
key do
'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9' \
'6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5' \
'/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7' \
'M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC' \
'rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0' \
'5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com'
<<~KEY.delete("\n")
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9
6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5
/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7
M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC
rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0
5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com
KEY
end
factory :rsa_deploy_key_2048, class: 'DeployKey'
......@@ -34,37 +36,44 @@ FactoryBot.define do
factory :dsa_key_2048 do
key do
'ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G' \
'Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp' \
'YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ' \
'/pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz' \
'OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv' \
'5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB' \
'AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t' \
'poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1' \
'M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH' \
'MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H' \
'nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A' \
'1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb' \
'aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI' \
'zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex' \
'PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z' \
'wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS' \
'Taja+Cf9kMo== dummy@gitlab.com'
<<~KEY.delete("\n")
ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G
Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp
YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ
/pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz
OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv
5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB
AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t
poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1
M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH
MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H
nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A
1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb
aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI
zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex
PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z
wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS
Taja+Cf9kMo== dummy@gitlab.com
KEY
end
end
factory :ecdsa_key_256 do
key do
'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYA' \
'AABBBJZmkzTgY0fiCQ+DVReyH/fFwTFz0XoR3RUO0u+199H19KFw7mNPxRSMOVS7tEtO' \
'Nj3Q7FcZXfqthHvgAzDiHsc= dummy@gitlab.com'
<<~KEY.delete("\n")
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYA
AABBBJZmkzTgY0fiCQ+DVReyH/fFwTFz0XoR3RUO0u+199H19KFw7mNPxRSMOVS7tEtO
Nj3Q7FcZXfqthHvgAzDiHsc= dummy@gitlab.com
KEY
end
end
factory :ed25519_key_256 do
key do
'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETnVTgzqC1gatgSlC4zH6aYt2CAQzgJOhDRvf59ohL6 dummy@gitlab.com'
<<~KEY.delete("\n")
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETnVTgzqC1gatgSlC4zH6aYt2CAQzgJ
OhDRvf59ohL6 dummy@gitlab.com
KEY
end
end
end
......
......@@ -24,6 +24,7 @@ feature 'Dashboard > Activity' do
end
let(:note) { create(:note, project: project, noteable: merge_request) }
let(:milestone) { create(:milestone, :active, project: project, title: '1.0') }
let!(:push_event) do
event = create(:push_event, project: project, author: user)
......@@ -54,6 +55,10 @@ feature 'Dashboard > Activity' do
create(:event, :commented, project: project, target: note, author: user)
end
let!(:milestone_event) do
create(:event, :closed, project: project, target: milestone, author: user)
end
before do
project.add_master(user)
......@@ -68,6 +73,7 @@ feature 'Dashboard > Activity' do
expect(page).to have_content('accepted')
expect(page).to have_content('closed')
expect(page).to have_content('commented on')
expect(page).to have_content('closed milestone')
end
end
......@@ -107,6 +113,7 @@ feature 'Dashboard > Activity' do
expect(page).not_to have_content('accepted')
expect(page).to have_content('closed')
expect(page).not_to have_content('commented on')
expect(page).to have_content('closed milestone')
end
end
......
......@@ -94,22 +94,14 @@ feature 'Dashboard Groups page', :js do
end
it 'can toggle parent group' do
# Collapsed by default
expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
expect(page).to have_selector("#group-#{group.id} .fa-caret-right")
# expand
click_group_caret(group)
expect(page).to have_selector("#group-#{group.id} .fa-caret-down")
expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right", count: 1)
expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}")
# collapse
click_group_caret(group)
expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
expect(page).to have_selector("#group-#{group.id} .fa-caret-right")
expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}")
end
end
......
......@@ -32,6 +32,18 @@ describe 'User visits the profile preferences page' do
end
end
describe 'User changes their multi file editor preferences', :js do
it 'set the new_repo cookie when the option is ON' do
choose 'user_multi_file_on'
expect(get_cookie('new_repo')).not_to be_nil
end
it 'deletes the new_repo cookie when the option is OFF' do
choose 'user_multi_file_off'
expect(get_cookie('new_repo')).to be_nil
end
end
describe 'User changes their default dashboard', :js do
it 'creates a flash message' do
select 'Starred Projects', from: 'user_dashboard'
......
......@@ -2,7 +2,7 @@ require 'spec_helper'
# Integration test that exports a file using the Import/Export feature
# It looks up for any sensitive word inside the JSON, so if a sensitive word is found
# we''l have to either include it adding the model that includes it to the +safe_list+
# we'll have to either include it adding the model that includes it to the +safe_list+
# or make sure the attribute is blacklisted in the +import_export.yml+ configuration
feature 'Import/Export - project export integration test', :js do
include Select2Helper
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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