Commit e7278b9e authored by Marin Jankovski's avatar Marin Jankovski

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

parents 564b32e3 acae8ddb
...@@ -256,7 +256,7 @@ flaky-examples-check: ...@@ -256,7 +256,7 @@ flaky-examples-check:
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json
stage: post-test stage: post-test
allow_failure: yes allow_failure: true
retry: 0 retry: 0
only: only:
- branches - branches
...@@ -416,7 +416,6 @@ ee_compat_check: ...@@ -416,7 +416,6 @@ ee_compat_check:
- /^[\d-]+-stable(-ee)?/ - /^[\d-]+-stable(-ee)?/
- branches@gitlab-org/gitlab-ee - branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee - branches@gitlab/gitlab-ee
allow_failure: no
retry: 0 retry: 0
artifacts: artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}" name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
......
...@@ -398,7 +398,7 @@ group :ed25519 do ...@@ -398,7 +398,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.52.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.54.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -275,7 +275,7 @@ GEM ...@@ -275,7 +275,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.52.0) gitaly-proto (0.54.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -355,10 +355,10 @@ GEM ...@@ -355,10 +355,10 @@ GEM
rake rake
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
grpc (1.6.6) grpc (1.7.2)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
googleauth (~> 0.5.1) googleauth (>= 0.5.1, < 0.7)
haml (4.0.7) haml (4.0.7)
tilt tilt
haml_lint (0.26.0) haml_lint (0.26.0)
...@@ -1034,7 +1034,7 @@ DEPENDENCIES ...@@ -1034,7 +1034,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.52.0) gitaly-proto (~> 0.54.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
......
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ /* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */
import _ from 'underscore'; import _ from 'underscore';
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import VueResource from 'vue-resource';
import Flash from '../flash'; import Flash from '../flash';
import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards'; import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub'; import eventHub from './eventhub';
import sidebarEventHub from '../sidebar/event_hub';
import './models/issue'; import './models/issue';
import './models/label'; import './models/label';
import './models/list'; import './models/list';
...@@ -14,7 +15,7 @@ import './models/milestone'; ...@@ -14,7 +15,7 @@ import './models/milestone';
import './models/assignee'; import './models/assignee';
import './stores/boards_store'; import './stores/boards_store';
import './stores/modal_store'; import './stores/modal_store';
import './services/board_service'; import BoardService from './services/board_service';
import './mixins/modal_mixins'; import './mixins/modal_mixins';
import './mixins/sortable_default_options'; import './mixins/sortable_default_options';
import './filters/due_date_filters'; import './filters/due_date_filters';
...@@ -77,11 +78,16 @@ $(() => { ...@@ -77,11 +78,16 @@ $(() => {
}); });
Store.rootPath = this.boardsEndpoint; Store.rootPath = this.boardsEndpoint;
// Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens); eventHub.$on('updateTokens', this.updateTokens);
eventHub.$on('newDetailIssue', this.updateDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens); eventHub.$off('updateTokens', this.updateTokens);
eventHub.$off('newDetailIssue', this.updateDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
}, },
mounted () { mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true); this.filterManager = new FilteredSearchBoards(Store.filter, true);
...@@ -112,6 +118,46 @@ $(() => { ...@@ -112,6 +118,46 @@ $(() => {
methods: { methods: {
updateTokens() { updateTokens() {
this.filterManager.updateTokens(); this.filterManager.updateTokens();
},
updateDetailIssue(newIssue) {
const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
.then(res => res.json())
.then((data) => {
newIssue.setFetchingState('subscriptions', false);
newIssue.updateData({
subscribed: data.subscribed,
});
})
.catch(() => {
newIssue.setFetchingState('subscriptions', false);
Flash(__('An error occurred while fetching sidebar data'));
});
}
Store.detail.issue = newIssue;
},
clearDetailIssue() {
Store.detail.issue = {};
},
toggleSubscription(id) {
const issue = Store.detail.issue;
if (issue.id === id && issue.toggleSubscriptionEndpoint) {
issue.setFetchingState('subscriptions', true);
BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
.then(() => {
issue.setFetchingState('subscriptions', false);
issue.updateData({
subscribed: !issue.subscribed,
});
})
.catch(() => {
issue.setFetchingState('subscriptions', false);
Flash(__('An error occurred when toggling the notification subscription'));
});
}
} }
}, },
}); });
......
<script>
import './issue_card_inner'; import './issue_card_inner';
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
export default { export default {
name: 'BoardsIssueCard', name: 'BoardsIssueCard',
template: `
<li class="card"
:class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
:index="index"
:data-issue-id="issue.id"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)">
<issue-card-inner
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:update-filters="true" />
</li>
`,
components: { components: {
'issue-card-inner': gl.issueBoards.IssueCardInner, 'issue-card-inner': gl.issueBoards.IssueCardInner,
}, },
...@@ -56,12 +42,30 @@ export default { ...@@ -56,12 +42,30 @@ export default {
this.showDetail = false; this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
Store.detail.issue = {}; eventHub.$emit('clearDetailIssue');
} else { } else {
Store.detail.issue = this.issue; eventHub.$emit('newDetailIssue', this.issue);
Store.detail.list = this.list; Store.detail.list = this.list;
} }
} }
}, },
}, },
}; };
</script>
<template>
<li class="card"
:class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
:index="index"
:data-issue-id="issue.id"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)">
<issue-card-inner
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:update-filters="true" />
</li>
</template>
/* global Sortable */ /* global Sortable */
import boardNewIssue from './board_new_issue'; import boardNewIssue from './board_new_issue';
import boardCard from './board_card'; import boardCard from './board_card.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
...@@ -5,12 +5,13 @@ ...@@ -5,12 +5,13 @@
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../flash'; import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub'; import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title'; import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees'; import assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select'; import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue'; import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context'; import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select'; import LabelsSelect from '../../labels_select';
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({
new DueDateSelectors(); new DueDateSelectors();
new LabelsSelect(); new LabelsSelect();
new Sidebar(); new Sidebar();
gl.Subscription.bindAll('.subscription');
}, },
components: { components: {
assigneeTitle,
assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn, removeBtn: gl.issueBoards.RemoveIssueBtn,
'assignee-title': AssigneeTitle, subscriptions,
assignees: Assignees,
}, },
}); });
...@@ -17,6 +17,11 @@ class ListIssue { ...@@ -17,6 +17,11 @@ class ListIssue {
this.assignees = []; this.assignees = [];
this.selected = false; this.selected = false;
this.position = obj.relative_position || Infinity; this.position = obj.relative_position || Infinity;
this.isFetching = {
subscriptions: true,
};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
if (obj.milestone) { if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone); this.milestone = new ListMilestone(obj.milestone);
...@@ -73,6 +78,14 @@ class ListIssue { ...@@ -73,6 +78,14 @@ class ListIssue {
return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id)); return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
} }
updateData(newData) {
Object.assign(this, newData);
}
setFetchingState(key, value) {
this.isFetching[key] = value;
}
update (url) { update (url) {
const data = { const data = {
issue: { issue: {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import Vue from 'vue'; import Vue from 'vue';
class BoardService { export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) { constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, { this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: { issues: {
...@@ -88,6 +88,14 @@ class BoardService { ...@@ -88,6 +88,14 @@ class BoardService {
return this.issues.bulkUpdate(data); return this.issues.bulkUpdate(data);
} }
static getIssueInfo(endpoint) {
return Vue.http.get(endpoint);
}
static toggleIssueSubscription(endpoint) {
return Vue.http.post(endpoint);
}
} }
window.BoardService = BoardService; window.BoardService = BoardService;
...@@ -14,7 +14,6 @@ export default () => { ...@@ -14,7 +14,6 @@ export default () => {
}); });
new LabelsSelect(); new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser); new IssuableContext(sidebarOptions.currentUser);
gl.Subscription.bindAll('.subscription');
new DueDateSelectors(); new DueDateSelectors();
window.sidebar = new Sidebar(); window.sidebar = new Sidebar();
}; };
...@@ -29,7 +29,6 @@ import './commit/image_file'; ...@@ -29,7 +29,6 @@ import './commit/image_file';
// lib/utils // lib/utils
import { handleLocationHash } from './lib/utils/common_utils'; import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility'; import './lib/utils/datetime_utility';
import './lib/utils/pretty_time';
import './lib/utils/url_utility'; import './lib/utils/url_utility';
// behaviors // behaviors
...@@ -80,7 +79,6 @@ import './right_sidebar'; ...@@ -80,7 +79,6 @@ import './right_sidebar';
import './search'; import './search';
import './search_autocomplete'; import './search_autocomplete';
import './smart_interval'; import './smart_interval';
import './subscription';
import './subscription_select'; import './subscription_select';
import initBreadcrumbs from './breadcrumb'; import initBreadcrumbs from './breadcrumb';
......
...@@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store'; ...@@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator'; import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue'; import subscriptions from './subscriptions.vue';
export default { export default {
...@@ -21,7 +22,7 @@ export default { ...@@ -21,7 +22,7 @@ export default {
onToggleSubscription() { onToggleSubscription() {
this.mediator.toggleSubscription() this.mediator.toggleSubscription()
.catch(() => { .catch(() => {
Flash('Error occurred when toggling the notification subscription'); Flash(__('Error occurred when toggling the notification subscription'));
}); });
}, },
}, },
......
...@@ -14,6 +14,10 @@ export default { ...@@ -14,6 +14,10 @@ export default {
type: Boolean, type: Boolean,
required: false, required: false,
}, },
id: {
type: Number,
required: false,
},
}, },
components: { components: {
loadingButton, loadingButton,
...@@ -32,7 +36,7 @@ export default { ...@@ -32,7 +36,7 @@ export default {
}, },
methods: { methods: {
toggleSubscription() { toggleSubscription() {
eventHub.$emit('toggleSubscription'); eventHub.$emit('toggleSubscription', this.id);
}, },
}, },
}; };
......
class Subscription {
constructor(containerElm) {
this.containerElm = containerElm;
const subscribeButton = containerElm.querySelector('.js-subscribe-button');
if (subscribeButton) {
// remove class so we don't bind twice
subscribeButton.classList.remove('js-subscribe-button');
subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
}
}
toggleSubscription(event) {
const button = event.currentTarget;
const buttonSpan = button.querySelector('span');
if (!buttonSpan || button.classList.contains('disabled')) {
return;
}
button.classList.add('disabled');
const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
const toggleActionUrl = this.containerElm.dataset.url;
$.post(toggleActionUrl, () => {
button.classList.remove('disabled');
// hack to allow this to work with the issue boards Vue object
if (document.querySelector('html').classList.contains('issue-boards-page')) {
gl.issueBoards.boardStoreIssueSet(
'subscribed',
!gl.issueBoards.BoardsStore.detail.issue.subscribed,
);
} else {
buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
}
});
}
static bindAll(selector) {
[].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
}
}
window.gl = window.gl || {};
window.gl.Subscription = Subscription;
...@@ -6,10 +6,9 @@ ...@@ -6,10 +6,9 @@
Sample configuration: Sample configuration:
<icon <icon
:img-src="userAvatarSrc" name="retry"
:img-alt="tooltipText" :size="32"
:tooltip-text="tooltipText" css-classes="top"
tooltip-placement="top"
/> />
*/ */
......
...@@ -50,7 +50,9 @@ ...@@ -50,7 +50,9 @@
<template> <template>
<div class="md-header"> <div class="md-header">
<ul class="nav-links clearfix"> <ul class="nav-links clearfix">
<li :class="{ active: !previewMarkdown }"> <li
class="md-header-tab"
:class="{ active: !previewMarkdown }">
<a <a
class="js-write-link" class="js-write-link"
href="#md-write-holder" href="#md-write-holder"
...@@ -59,7 +61,9 @@ ...@@ -59,7 +61,9 @@
Write Write
</a> </a>
</li> </li>
<li :class="{ active: previewMarkdown }"> <li
class="md-header-tab"
:class="{ active: previewMarkdown }">
<a <a
class="js-preview-link" class="js-preview-link"
href="#md-preview-holder" href="#md-preview-holder"
...@@ -68,56 +72,52 @@ ...@@ -68,56 +72,52 @@
Preview Preview
</a> </a>
</li> </li>
<li class="pull-right"> <li class="md-header-toolbar">
<div class="toolbar-group"> <toolbar-button
<toolbar-button tag="**"
tag="**" button-title="Add bold text"
button-title="Add bold text" icon="bold" />
icon="bold" /> <toolbar-button
<toolbar-button tag="*"
tag="*" button-title="Add italic text"
button-title="Add italic text" icon="italic" />
icon="italic" /> <toolbar-button
<toolbar-button tag="> "
tag="> " :prepend="true"
:prepend="true" button-title="Insert a quote"
button-title="Insert a quote" icon="quote" />
icon="quote" /> <toolbar-button
<toolbar-button tag="`"
tag="`" tag-block="```"
tag-block="```" button-title="Insert code"
button-title="Insert code" icon="code" />
icon="code" /> <toolbar-button
<toolbar-button tag="* "
tag="* " :prepend="true"
:prepend="true" button-title="Add a bullet list"
button-title="Add a bullet list" icon="list-bulleted" />
icon="list-bulleted" /> <toolbar-button
<toolbar-button tag="1. "
tag="1. " :prepend="true"
:prepend="true" button-title="Add a numbered list"
button-title="Add a numbered list" icon="list-numbered" />
icon="list-numbered" /> <toolbar-button
<toolbar-button tag="* [ ] "
tag="* [ ] " :prepend="true"
:prepend="true" button-title="Add a task list"
button-title="Add a task list" icon="task-done" />
icon="task-done" /> <button
</div> v-tooltip
<div class="toolbar-group"> aria-label="Go full screen"
<button class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
v-tooltip data-container="body"
aria-label="Go full screen" tabindex="-1"
class="toolbar-btn js-zen-enter" title="Go full screen"
data-container="body" type="button">
tabindex="-1" <icon
title="Go full screen" name="screen-full">
type="button"> </icon>
<icon </button>
name="screen-full">
</icon>
</button>
</div>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
<button <button
v-tooltip v-tooltip
type="button" type="button"
class="toolbar-btn js-md hidden-xs" class="toolbar-btn js-md"
tabindex="-1" tabindex="-1"
data-container="body" data-container="body"
:data-md-tag="tag" :data-md-tag="tag"
......
...@@ -430,6 +430,7 @@ img.emoji { ...@@ -430,6 +430,7 @@ img.emoji {
/** COMMON CLASSES **/ /** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; } .prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; } .prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; } .prepend-top-10 { margin-top: 10px; }
.prepend-top-15 { margin-top: 15px; } .prepend-top-15 { margin-top: 15px; }
.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-default { margin-top: $gl-padding !important; }
......
...@@ -57,6 +57,7 @@ ...@@ -57,6 +57,7 @@
.md-header { .md-header {
.nav-links { .nav-links {
a { a {
width: 100%;
padding-top: 0; padding-top: 0;
line-height: 19px; line-height: 19px;
...@@ -72,6 +73,28 @@ ...@@ -72,6 +73,28 @@
} }
} }
.md-header-tab {
@media(max-width: $screen-xs-max) {
flex: 1;
width: 100%;
border-bottom: 1px solid $border-color;
text-align: center;
}
}
.md-header-toolbar {
margin-left: auto;
@media(max-width: $screen-xs-max) {
flex: none;
display: flex;
justify-content: center;
width: 100%;
padding-top: $gl-padding-top;
padding-bottom: $gl-padding-top;
}
}
.referenced-users { .referenced-users {
color: $gl-text-color; color: $gl-text-color;
padding-top: 10px; padding-top: 10px;
...@@ -126,16 +149,6 @@ ...@@ -126,16 +149,6 @@
} }
} }
.toolbar-group {
float: left;
margin-right: -5px;
margin-left: $gl-padding;
&:first-child {
margin-left: 0;
}
}
.toolbar-btn { .toolbar-btn {
float: left; float: left;
padding: 0 7px; padding: 0 7px;
...@@ -158,6 +171,16 @@ ...@@ -158,6 +171,16 @@
} }
} }
.toolbar-fullscreen-btn {
margin-left: $gl-padding;
margin-right: -5px;
@media(max-width: $screen-xs-max) {
margin-left: 0;
margin-right: 0;
}
}
.atwho-view { .atwho-view {
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
......
...@@ -196,7 +196,11 @@ class ApplicationController < ActionController::Base ...@@ -196,7 +196,11 @@ class ApplicationController < ActionController::Base
end end
def check_password_expiration def check_password_expiration
if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? return if session[:impersonator_id] || current_user&.ldap_user?
password_expires_at = current_user&.password_expires_at
if password_expires_at && password_expires_at < Time.now
return redirect_to new_profile_password_path return redirect_to new_profile_password_path
end end
end end
......
...@@ -84,6 +84,7 @@ module Boards ...@@ -84,6 +84,7 @@ module Boards
resource.as_json( resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true, labels: true,
sidebar_endpoints: true,
include: { include: {
project: { only: [:id, :path] }, project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
......
...@@ -222,7 +222,7 @@ module MarkupHelper ...@@ -222,7 +222,7 @@ module MarkupHelper
data = options[:data].merge({ container: 'body' }) data = options[:data].merge({ container: 'body' })
content_tag :button, content_tag :button,
type: 'button', type: 'button',
class: 'toolbar-btn js-md has-tooltip hidden-xs', class: 'toolbar-btn js-md has-tooltip',
tabindex: -1, tabindex: -1,
data: data, data: data,
title: options[:title], title: options[:title],
......
...@@ -246,7 +246,12 @@ class Issue < ActiveRecord::Base ...@@ -246,7 +246,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {}) def as_json(options = {})
super(options).tap do |json| super(options).tap do |json|
json[:subscribed] = subscribed?(options[:user], project) if options.key?(:user) && options[:user] if options.key?(:sidebar_endpoints) && project
url_helper = Gitlab::Routing.url_helpers
json.merge!(issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self))
end
if options.key?(:labels) if options.key?(:labels)
json[:labels] = labels.as_json( json[:labels] = labels.as_json(
......
...@@ -10,6 +10,8 @@ module MergeRequests ...@@ -10,6 +10,8 @@ module MergeRequests
attr_reader :merge_request, :source attr_reader :merge_request, :source
delegate :merge_jid, :state, to: :@merge_request
def execute(merge_request) def execute(merge_request)
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService) if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
FfMergeService.new(project, current_user, params).execute(merge_request) FfMergeService.new(project, current_user, params).execute(merge_request)
...@@ -27,6 +29,7 @@ module MergeRequests ...@@ -27,6 +29,7 @@ module MergeRequests
success success
end end
end end
log_info("Merge process finished on JID #{merge_jid} with state #{state}")
rescue MergeError => e rescue MergeError => e
handle_merge_error(log_message: e.message, save_message_on_model: true) handle_merge_error(log_message: e.message, save_message_on_model: true)
end end
...@@ -49,7 +52,9 @@ module MergeRequests ...@@ -49,7 +52,9 @@ module MergeRequests
def commit def commit
message = params[:commit_message] || merge_request.merge_commit_message message = params[:commit_message] || merge_request.merge_commit_message
log_info("Git merge started on JID #{merge_jid}")
commit_id = repository.merge(current_user, source, merge_request, message) commit_id = repository.merge(current_user, source, merge_request, message)
log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
raise MergeError, 'Conflicts detected during merge' unless commit_id raise MergeError, 'Conflicts detected during merge' unless commit_id
...@@ -63,7 +68,9 @@ module MergeRequests ...@@ -63,7 +68,9 @@ module MergeRequests
end end
def after_merge def after_merge
log_info("Post merge started on JID #{merge_jid} with state #{state}")
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
log_info("Post merge finished on JID #{merge_jid} with state #{state}")
if delete_source_branch? if delete_source_branch?
DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
...@@ -92,6 +99,11 @@ module MergeRequests ...@@ -92,6 +99,11 @@ module MergeRequests
@merge_request.update(merge_error: log_message) if save_message_on_model @merge_request.update(merge_error: log_message) if save_message_on_model
end end
def log_info(message)
@logger ||= Rails.logger
@logger.info("#{merge_request_info} - #{message}")
end
def merge_request_info def merge_request_info
merge_request.to_reference(full: true) merge_request.to_reference(full: true)
end end
......
...@@ -10,25 +10,23 @@ ...@@ -10,25 +10,23 @@
.md-area .md-area
.md-header .md-header
%ul.nav-links.clearfix %ul.nav-links.clearfix
%li.active %li.md-header-tab.active
%a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 } %a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 }
Write Write
%li %li.md-header-tab
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview Preview
%li.pull-right %li.md-header-toolbar
.toolbar-group = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" })
= markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" }) = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" })
= markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" }) = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
= markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
= markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
= markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
.toolbar-group = sprite_icon("screen-full")
%button.toolbar-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
= sprite_icon("screen-full")
.md-write-holder .md-write-holder
= yield = yield
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
.dropdown-menu.dropdown-menu-align-right.hidden-lg .dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul %ul
- if can_update_issue - if can_update_issue
%li= link_to 'Edit', edit_project_issue_path(@project, @issue) %li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit'
- unless current_user == @issue.author - unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue - if can_update_issue
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.sidebar-container .sidebar-container
.blocks-container .blocks-container
.block .block
%strong.prepend-top-10 %strong.inline.prepend-top-8
= @build.name = @build.name
- if can?(current_user, :update_build, @build) && @build.retryable? - if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post = link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post
......
- if current_user - if current_user
.block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" } .block.subscriptions
%span.issuable-header-text.hide-collapsed.pull-left %subscriptions{ ":loading" => "issue.isFetching && issue.isFetching.subscriptions",
Notifications ":subscribed" => "issue.subscribed",
%button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } ":id" => "issue.id" }
%span
{{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
---
title: Impersonation no longer gets stuck on password change.
merge_request: 15497
author:
type: fixed
---
title: Update Issue Boards to fetch the notification subscription status asynchronously
merge_request:
author:
type: performance
---
title: Add inline editing to issues on mobile
merge_request: 15438
author:
type: changed
---
title: Removed unused rake task, 'rake gitlab:sidekiq:drop_post_receive'
merge_request: 15493
author:
type: fixed
---
title: Fix bitbucket wiki import with hashed storage enabled
merge_request: 15490
author:
type: fixed
---
title: Align retry button with job title with new grid size
merge_request:
author:
type: fixed
---
title: Don't move repositories and attachments for projects using hashed storage
merge_request: 15479
author:
type: other
---
title: Clarify wording of protected branch settings for the default branch
merge_request:
author:
type: other
---
title: Clean up schema of the "merge_requests" table
merge_request:
author:
type: other
---
title: Add logs for monitoring the merge process
merge_request:
author:
type: other
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsAuthorIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_authors
where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.author_id = users.id)')
.where('author_id IS NOT NULL')
end
end
def up
# Replacing the ghost user ID logic would be too complex, hence we don't
# redefine the User model here.
ghost_id = User.select(:id).ghost.id
MergeRequest.with_orphaned_authors.each_batch(of: 100) do |batch|
batch.update_all(author_id: ghost_id)
end
add_concurrent_foreign_key(
:merge_requests,
:users,
column: :author_id,
on_delete: :nullify
)
end
def down
remove_foreign_key(:merge_requests, column: :author_id)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsAssigneeIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_assignees
where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.assignee_id = users.id)')
.where('assignee_id IS NOT NULL')
end
end
def up
MergeRequest.with_orphaned_assignees.each_batch(of: 100) do |batch|
batch.update_all(assignee_id: nil)
end
add_concurrent_foreign_key(
:merge_requests,
:users,
column: :assignee_id,
on_delete: :nullify
)
end
def down
remove_foreign_key(:merge_requests, column: :assignee_id)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsUpdatedByIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_updaters
where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.updated_by_id = users.id)')
.where('updated_by_id IS NOT NULL')
end
end
def up
MergeRequest.with_orphaned_updaters.each_batch(of: 100) do |batch|
batch.update_all(updated_by_id: nil)
end
add_concurrent_index(
:merge_requests,
:updated_by_id,
where: 'updated_by_id IS NOT NULL'
)
add_concurrent_foreign_key(
:merge_requests,
:users,
column: :updated_by_id,
on_delete: :nullify
)
end
def down
remove_foreign_key_without_error(:merge_requests, column: :updated_by_id)
remove_concurrent_index(:merge_requests, :updated_by_id)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsMergeUserIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_mergers
where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.merge_user_id = users.id)')
.where('merge_user_id IS NOT NULL')
end
end
def up
MergeRequest.with_orphaned_mergers.each_batch(of: 100) do |batch|
batch.update_all(merge_user_id: nil)
end
add_concurrent_index(
:merge_requests,
:merge_user_id,
where: 'merge_user_id IS NOT NULL'
)
add_concurrent_foreign_key(
:merge_requests,
:users,
column: :merge_user_id,
on_delete: :nullify
)
end
def down
remove_foreign_key_without_error(:merge_requests, column: :merge_user_id)
remove_concurrent_index(:merge_requests, :merge_user_id)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsSourceProjectIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_source_projects
where('NOT EXISTS (SELECT true FROM projects WHERE merge_requests.source_project_id = projects.id)')
.where('source_project_id IS NOT NULL')
end
end
def up
# We need to allow NULL values so we can nullify the column when the source
# project is removed. We _don't_ want to remove the merge request, instead
# the application will keep them but close them.
change_column_null(:merge_requests, :source_project_id, true)
MergeRequest.with_orphaned_source_projects.each_batch(of: 100) do |batch|
batch.update_all(source_project_id: nil)
end
add_concurrent_foreign_key(
:merge_requests,
:projects,
column: :source_project_id,
on_delete: :nullify
)
end
def down
remove_foreign_key_without_error(:merge_requests, column: :source_project_id)
change_column_null(:merge_requests, :source_project_id, false)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsMilestoneIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_milestones
where('NOT EXISTS (SELECT true FROM milestones WHERE merge_requests.milestone_id = milestones.id)')
.where('milestone_id IS NOT NULL')
end
end
def up
MergeRequest.with_orphaned_milestones.each_batch(of: 100) do |batch|
batch.update_all(milestone_id: nil)
end
add_concurrent_foreign_key(
:merge_requests,
:milestones,
column: :milestone_id,
on_delete: :nullify
)
end
def down
remove_foreign_key_without_error(:merge_requests, column: :milestone_id)
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171114104051) do ActiveRecord::Schema.define(version: 20171114162227) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1040,7 +1040,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do ...@@ -1040,7 +1040,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
create_table "merge_requests", force: :cascade do |t| create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false t.string "target_branch", null: false
t.string "source_branch", null: false t.string "source_branch", null: false
t.integer "source_project_id", null: false t.integer "source_project_id"
t.integer "author_id" t.integer "author_id"
t.integer "assignee_id" t.integer "assignee_id"
t.string "title" t.string "title"
...@@ -1080,6 +1080,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do ...@@ -1080,6 +1080,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree
add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree
add_index "merge_requests", ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)", using: :btree
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
...@@ -1088,6 +1089,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do ...@@ -1088,6 +1089,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
add_index "merge_requests", ["updated_by_id"], name: "index_merge_requests_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
create_table "merge_requests_closing_issues", force: :cascade do |t| create_table "merge_requests_closing_issues", force: :cascade do |t|
t.integer "merge_request_id", null: false t.integer "merge_request_id", null: false
...@@ -1965,7 +1967,13 @@ ActiveRecord::Schema.define(version: 20171114104051) do ...@@ -1965,7 +1967,13 @@ ActiveRecord::Schema.define(version: 20171114104051) do
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify
add_foreign_key "merge_requests", "milestones", name: "fk_6a5165a692", on_delete: :nullify
add_foreign_key "merge_requests", "projects", column: "source_project_id", name: "fk_3308fe130c", on_delete: :nullify
add_foreign_key "merge_requests", "projects", column: "target_project_id", name: "fk_a6963e8447", on_delete: :cascade add_foreign_key "merge_requests", "projects", column: "target_project_id", name: "fk_a6963e8447", on_delete: :cascade
add_foreign_key "merge_requests", "users", column: "assignee_id", name: "fk_6149611a04", on_delete: :nullify
add_foreign_key "merge_requests", "users", column: "author_id", name: "fk_e719a85f8a", on_delete: :nullify
add_foreign_key "merge_requests", "users", column: "merge_user_id", name: "fk_ad525e1f87", on_delete: :nullify
add_foreign_key "merge_requests", "users", column: "updated_by_id", name: "fk_641731faff", on_delete: :nullify
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
......
...@@ -79,7 +79,7 @@ PUT /application/settings ...@@ -79,7 +79,7 @@ PUT /application/settings
| `clientside_sentry_enabled` | boolean | no | Enable Sentry error reporting for the client side | | `clientside_sentry_enabled` | boolean | no | Enable Sentry error reporting for the client side |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
| `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts | | `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts |
| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `2`. | | `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and masters can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but masters can; no-one can force push or delete the branch)_ as a parameter. Default is `2`. |
| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `default_projects_limit` | integer | no | Project limit per user. Default is `100000` | | `default_projects_limit` | integer | no | Project limit per user. Default is `100000` |
......
...@@ -4,15 +4,17 @@ We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are on ...@@ -4,15 +4,17 @@ We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are on
### Usage in HAML/Rails ### Usage in HAML/Rails
To use a sprite Icon in HAML or Rails we use a specific helper function : To use a sprite Icon in HAML or Rails we use a specific helper function :
`sprite_icon(icon_name, size: nil, css_class: '')` `sprite_icon(icon_name, size: nil, css_class: '')`
**icon_name** Use the icon_name that you can find in the SVG Sprite (Overview is available under `/assets/sprite.symbol.html`). **icon_name** Use the icon_name that you can find in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
**size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class) **size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class)
**css_class (optional)** If you want to add additional css classes **css_class (optional)** If you want to add additional css classes
**Example** **Example**
`= sprite_icon('issues', size: 72, css_class: 'icon-danger')` `= sprite_icon('issues', size: 72, css_class: 'icon-danger')`
...@@ -20,16 +22,34 @@ To use a sprite Icon in HAML or Rails we use a specific helper function : ...@@ -20,16 +22,34 @@ To use a sprite Icon in HAML or Rails we use a specific helper function :
`<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>` `<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>`
### Usage in Vue
We have a special Vue component for our sprite icons in `\vue_shared\components\icon.vue`.
Sample usage :
`<icon
name="retry"
:size="32"
css-classes="top"
/>`
**name** Name of the Icon in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
**size (optional)** Number value for the size which is then mapped to a specific CSS class (Available Sizes: 8,12,16,18,24,32,48,72 are mapped to `sXX` css classes)
**css-classes (optional)** Additional CSS Classes to add to the svg tag.
### Usage in HTML/JS ### Usage in HTML/JS
Please use the following function inside JS to render an icon : Please use the following function inside JS to render an icon :
`gl.utils.spriteIcon(iconName)` `gl.utils.spriteIcon(iconName)`
## Adding a new icon to the sprite ## Adding a new icon to the sprite
All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency. All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced.
# SVG Illustrations # SVG Illustrations
......
...@@ -80,13 +80,13 @@ errors during usage. ...@@ -80,13 +80,13 @@ errors during usage.
- 256GB RAM supports up to 32,000 users - 256GB RAM supports up to 32,000 users
- More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/) - More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/)
We recommend having at least 2GB of swap on your server, even if you currently have We recommend having at least [2GB of swap on your server](https://askubuntu.com/a/505344/310789), even if you currently have
enough available RAM. Having swap will help reduce the chance of errors occurring enough available RAM. Having swap will help reduce the chance of errors occurring
if your available memory changes. We also recommend [configuring the kernel's swappiness setting](https://askubuntu.com/a/103916) if your available memory changes. We also recommend [configuring the kernel's swappiness setting](https://askubuntu.com/a/103916)
to a low value like `10` to make the most of your RAM while still having the swap to a low value like `10` to make the most of your RAM while still having the swap
available when needed. available when needed.
Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those. Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
## Database ## Database
...@@ -146,7 +146,7 @@ So for a machine with 2 cores, 3 unicorn workers is ideal. ...@@ -146,7 +146,7 @@ So for a machine with 2 cores, 3 unicorn workers is ideal.
For all machines that have 2GB and up we recommend a minimum of three unicorn workers. For all machines that have 2GB and up we recommend a minimum of three unicorn workers.
If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping. If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping.
To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). To change the Unicorn workers when you have the Omnibus package (which defaults to the recommendation above) please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings).
## Redis and Sidekiq ## Redis and Sidekiq
......
...@@ -58,9 +58,9 @@ module Gitlab ...@@ -58,9 +58,9 @@ module Gitlab
def protection_options def protection_options
{ {
"Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE, "Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE,
"Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch." => PROTECTION_DEV_CAN_MERGE, "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch. Masters can push to the branch." => PROTECTION_DEV_CAN_MERGE,
"Partially protected: Developers can push new commits, but cannot force push or delete the branch. Masters can do all of those." => PROTECTION_DEV_CAN_PUSH, "Partially protected: Both developers and masters can push new commits, but cannot force push or delete the branch." => PROTECTION_DEV_CAN_PUSH,
"Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL "Fully protected: Developers cannot push new commits, but masters can. No-one can force push or delete the branch." => PROTECTION_FULL
} }
end end
......
...@@ -61,9 +61,9 @@ module Gitlab ...@@ -61,9 +61,9 @@ module Gitlab
def import_wiki def import_wiki
return if project.wiki.repository_exists? return if project.wiki.repository_exists?
path_with_namespace = "#{project.full_path}.wiki" disk_path = project.wiki.disk_path
import_url = project.import_url.sub(/\.git\z/, ".git/wiki") import_url = project.import_url.sub(/\.git\z/, ".git/wiki")
gitlab_shell.import_repository(project.repository_storage_path, path_with_namespace, import_url) gitlab_shell.import_repository(project.repository_storage_path, disk_path, import_url)
rescue StandardError => e rescue StandardError => e
errors << { type: :wiki, errors: e.message } errors << { type: :wiki, errors: e.message }
end end
......
...@@ -68,6 +68,11 @@ module Gitlab ...@@ -68,6 +68,11 @@ module Gitlab
has_one :route, as: :source has_one :route, as: :source
self.table_name = 'projects' self.table_name = 'projects'
HASHED_STORAGE_FEATURES = {
repository: 1,
attachments: 2
}.freeze
def repository_storage_path def repository_storage_path
Gitlab.config.repositories.storages[repository_storage]['path'] Gitlab.config.repositories.storages[repository_storage]['path']
end end
...@@ -76,6 +81,13 @@ module Gitlab ...@@ -76,6 +81,13 @@ module Gitlab
def self.name def self.name
'Project' 'Project'
end end
def hashed_storage?(feature)
raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)
return false unless respond_to?(:storage_version)
self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
end
end end
end end
end end
......
...@@ -22,9 +22,11 @@ module Gitlab ...@@ -22,9 +22,11 @@ module Gitlab
end end
def move_project_folders(project, old_full_path, new_full_path) def move_project_folders(project, old_full_path, new_full_path)
move_repository(project, old_full_path, new_full_path) unless project.hashed_storage?(:repository)
move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki") move_repository(project, old_full_path, new_full_path)
move_uploads(old_full_path, new_full_path) move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
end
move_uploads(old_full_path, new_full_path) unless project.hashed_storage?(:attachments)
move_pages(old_full_path, new_full_path) move_pages(old_full_path, new_full_path)
end end
......
...@@ -304,7 +304,13 @@ module Gitlab ...@@ -304,7 +304,13 @@ module Gitlab
end end
def delete_all_refs_except(prefixes) def delete_all_refs_except(prefixes)
delete_refs(*all_ref_names_except(prefixes)) gitaly_migrate(:ref_delete_refs) do |is_enabled|
if is_enabled
gitaly_ref_client.delete_refs(except_with_prefixes: prefixes)
else
delete_refs(*all_ref_names_except(prefixes))
end
end
end end
# Returns an Array of all ref names, except when it's matching pattern # Returns an Array of all ref names, except when it's matching pattern
......
...@@ -126,6 +126,15 @@ module Gitlab ...@@ -126,6 +126,15 @@ module Gitlab
GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request) GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request)
end end
def delete_refs(except_with_prefixes:)
request = Gitaly::DeleteRefsRequest.new(
repository: @gitaly_repo,
except_with_prefix: except_with_prefixes
)
GitalyClient.call(@repository.storage, :ref_service, :delete_refs, request)
end
private private
def consume_refs_response(response) def consume_refs_response(response)
......
module Gitlab module Gitlab
module ImportExport module ImportExport
class MergeRequestParser class MergeRequestParser
FORKED_PROJECT_ID = -1 FORKED_PROJECT_ID = nil
def initialize(project, diff_head_sha, merge_request, relation_hash) def initialize(project, diff_head_sha, merge_request, relation_hash)
@project = project @project = project
......
namespace :gitlab do
namespace :sidekiq do
QUEUE = 'queue:post_receive'.freeze
desc 'Drop all Sidekiq PostReceive jobs for a given project'
task :drop_post_receive, [:project] => :environment do |t, args|
unless args.project.present?
abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]"
end
project_path = Project.find_by_full_path(args.project).repository.path_to_repo
Sidekiq.redis do |redis|
unless redis.exists(QUEUE)
abort "Queue #{QUEUE} is empty"
end
temp_queue = "#{QUEUE}_#{Time.now.to_i}"
redis.rename(QUEUE, temp_queue)
# At this point, then post_receive queue is empty. It may be receiving
# new jobs already. We will repopulate it with the old jobs, skipping the
# ones we want to drop.
dropped = 0
while (job = redis.lpop(temp_queue))
if repo_path(job) == project_path
dropped += 1
else
redis.rpush(QUEUE, job)
end
end
# The temp_queue will delete itself after we have popped all elements
# from it
puts "Dropped #{dropped} jobs containing #{project_path} from #{QUEUE}"
end
end
def repo_path(job)
job_args = JSON.parse(job)['args']
if job_args
job_args.first
else
nil
end
end
end
end
...@@ -8,6 +8,13 @@ ENV DEBIAN_FRONTEND noninteractive ...@@ -8,6 +8,13 @@ ENV DEBIAN_FRONTEND noninteractive
RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list
RUN apt-get update && apt-get install -y wget git unzip xvfb RUN apt-get update && apt-get install -y wget git unzip xvfb
##
# Install Docker
#
RUN wget -q https://download.docker.com/linux/static/stable/x86_64/docker-17.09.0-ce.tgz && \
tar -zxf docker-17.09.0-ce.tgz && mv docker/docker /usr/local/bin/docker && \
rm docker-17.09.0-ce.tgz
## ##
# Install Google Chrome version with headless support # Install Google Chrome version with headless support
# #
......
...@@ -9,5 +9,16 @@ module QA ...@@ -9,5 +9,16 @@ module QA
expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/)
end end
end end
##
# TODO, temporary workaround for gitlab-org/gitlab-qa#102.
#
after do
visit Runtime::Scenario.mattermost_address
reset_session!
visit Runtime::Scenario.gitlab_address
reset_session!
end
end end
end end
...@@ -6,6 +6,10 @@ describe ApplicationController do ...@@ -6,6 +6,10 @@ describe ApplicationController do
describe '#check_password_expiration' do describe '#check_password_expiration' do
let(:controller) { described_class.new } let(:controller) { described_class.new }
before do
allow(controller).to receive(:session).and_return({})
end
it 'redirects if the user is over their password expiry' do it 'redirects if the user is over their password expiry' do
user.password_expires_at = Time.new(2002) user.password_expires_at = Time.new(2002)
......
...@@ -167,19 +167,36 @@ describe "Admin::Users" do ...@@ -167,19 +167,36 @@ describe "Admin::Users" do
it 'sees impersonation log out icon' do it 'sees impersonation log out icon' do
icon = first('.fa.fa-user-secret') icon = first('.fa.fa-user-secret')
expect(icon).not_to eql nil expect(icon).not_to be nil
end end
it 'logs out of impersonated user back to original user' do it 'logs out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click find(:css, 'li.impersonation a').click
expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username) expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username)
end end
it 'is redirected back to the impersonated users page in the admin after stopping' do it 'is redirected back to the impersonated users page in the admin after stopping' do
find(:css, 'li.impersonation a').click find(:css, 'li.impersonation a').click
expect(current_path).to eql "/admin/users/#{another_user.username}" expect(current_path).to eq("/admin/users/#{another_user.username}")
end
end
context 'when impersonating a user with an expired password' do
before do
another_user.update(password_expires_at: Time.now - 5.minutes)
click_link 'Impersonate'
end
it 'does not redirect to password change page' do
expect(current_path).to eq('/')
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
find(:css, 'li.impersonation a').click
expect(current_path).to eq("/admin/users/#{another_user.username}")
end end
end end
end end
......
...@@ -331,11 +331,29 @@ describe 'Issue Boards', :js do ...@@ -331,11 +331,29 @@ describe 'Issue Boards', :js do
context 'subscription' do context 'subscription' do
it 'changes issue subscription' do it 'changes issue subscription' do
click_card(card) click_card(card)
wait_for_requests
page.within('.subscription') do page.within('.subscriptions') do
click_button 'Subscribe' click_button 'Subscribe'
wait_for_requests wait_for_requests
expect(page).to have_content("Unsubscribe")
expect(page).to have_content('Unsubscribe')
end
end
it 'has "Unsubscribe" button when already subscribed' do
create(:subscription, user: user, project: project, subscribable: issue2, subscribed: true)
visit project_board_path(project, board)
wait_for_requests
click_card(card)
wait_for_requests
page.within('.subscriptions') do
click_button 'Unsubscribe'
wait_for_requests
expect(page).to have_content('Subscribe')
end end
end end
end end
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
"confidential": { "type": "boolean" }, "confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] }, "due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" }, "relative_position": { "type": "integer" },
"issue_sidebar_endpoint": { "type": "string" },
"toggle_subscription_endpoint": { "type": "string" },
"project": { "project": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"path": { "type": "string" } "path": { "type": "string" }
......
...@@ -9,10 +9,11 @@ ...@@ -9,10 +9,11 @@
import Vue from 'vue'; import Vue from 'vue';
import '~/boards/models/assignee'; import '~/boards/models/assignee';
import eventHub from '~/boards/eventhub';
import '~/boards/models/list'; import '~/boards/models/list';
import '~/boards/models/label'; import '~/boards/models/label';
import '~/boards/stores/boards_store'; import '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card'; import boardCard from '~/boards/components/board_card.vue';
import './mock_data'; import './mock_data';
describe('Board card', () => { describe('Board card', () => {
...@@ -157,33 +158,35 @@ describe('Board card', () => { ...@@ -157,33 +158,35 @@ describe('Board card', () => {
}); });
it('sets detail issue to card issue on mouse up', () => { it('sets detail issue to card issue on mouse up', () => {
spyOn(eventHub, '$emit');
triggerEvent('mousedown'); triggerEvent('mousedown');
triggerEvent('mouseup'); triggerEvent('mouseup');
expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue); expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue);
expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list); expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list);
}); });
it('adds active class if detail issue is set', (done) => { it('adds active class if detail issue is set', (done) => {
triggerEvent('mousedown'); vm.detailIssue.issue = vm.issue;
triggerEvent('mouseup');
Vue.nextTick()
setTimeout(() => { .then(() => {
expect(vm.$el.classList.contains('is-active')).toBe(true); expect(vm.$el.classList.contains('is-active')).toBe(true);
done(); })
}, 0); .then(done)
.catch(done.fail);
}); });
it('resets detail issue to empty if already set', () => { it('resets detail issue to empty if already set', () => {
triggerEvent('mousedown'); spyOn(eventHub, '$emit');
triggerEvent('mouseup');
expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue); gl.issueBoards.BoardsStore.detail.issue = vm.issue;
triggerEvent('mousedown'); triggerEvent('mousedown');
triggerEvent('mouseup'); triggerEvent('mouseup');
expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue');
}); });
}); });
}); });
...@@ -133,6 +133,19 @@ describe('Issue model', () => { ...@@ -133,6 +133,19 @@ describe('Issue model', () => {
expect(relativePositionIssue.position).toBe(1); expect(relativePositionIssue.position).toBe(1);
}); });
it('updates data', () => {
issue.updateData({ subscribed: true });
expect(issue.subscribed).toBe(true);
});
it('sets fetching state', () => {
expect(issue.isFetching.subscriptions).toBe(true);
issue.setFetchingState('subscriptions', false);
expect(issue.isFetching.subscriptions).toBe(false);
});
describe('update', () => { describe('update', () => {
it('passes assignee ids when there are assignees', (done) => { it('passes assignee ids when there are assignees', (done) => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => { spyOn(Vue.http, 'patch').and.callFake((url, data) => {
......
...@@ -98,7 +98,6 @@ describe('LoadingButton', function () { ...@@ -98,7 +98,6 @@ describe('LoadingButton', function () {
it('does not call given callback when disabled because of loading', () => { it('does not call given callback when disabled because of loading', () => {
vm = mountComponent(LoadingButton, { vm = mountComponent(LoadingButton, {
loading: true, loading: true,
indeterminate: true,
}); });
spyOn(vm, '$emit'); spyOn(vm, '$emit');
......
...@@ -54,11 +54,13 @@ describe Gitlab::BitbucketImport::Importer do ...@@ -54,11 +54,13 @@ describe Gitlab::BitbucketImport::Importer do
create( create(
:project, :project,
import_source: project_identifier, import_source: project_identifier,
import_url: "https://bitbucket.org/#{project_identifier}.git",
import_data_attributes: { credentials: data } import_data_attributes: { credentials: data }
) )
end end
let(:importer) { described_class.new(project) } let(:importer) { described_class.new(project) }
let(:gitlab_shell) { double }
let(:issues_statuses_sample_data) do let(:issues_statuses_sample_data) do
{ {
...@@ -67,6 +69,10 @@ describe Gitlab::BitbucketImport::Importer do ...@@ -67,6 +69,10 @@ describe Gitlab::BitbucketImport::Importer do
} }
end end
before do
allow(importer).to receive(:gitlab_shell) { gitlab_shell }
end
context 'issues statuses' do context 'issues statuses' do
before do before do
# HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this
...@@ -110,15 +116,36 @@ describe Gitlab::BitbucketImport::Importer do ...@@ -110,15 +116,36 @@ describe Gitlab::BitbucketImport::Importer do
end end
it 'maps statuses to open or closed' do it 'maps statuses to open or closed' do
allow(importer).to receive(:import_wiki)
importer.execute importer.execute
expect(project.issues.where(state: "closed").size).to eq(5) expect(project.issues.where(state: "closed").size).to eq(5)
expect(project.issues.where(state: "opened").size).to eq(2) expect(project.issues.where(state: "opened").size).to eq(2)
end end
it 'calls import_wiki' do describe 'wiki import' do
expect(importer).to receive(:import_wiki) it 'is skipped when the wiki exists' do
importer.execute expect(project.wiki).to receive(:repository_exists?) { true }
expect(importer.gitlab_shell).not_to receive(:import_repository)
importer.execute
expect(importer.errors).to be_empty
end
it 'imports to the project disk_path' do
expect(project.wiki).to receive(:repository_exists?) { false }
expect(importer.gitlab_shell).to receive(:import_repository).with(
project.repository_storage_path,
project.wiki.disk_path,
project.import_url + '/wiki'
)
importer.execute
expect(importer.errors).to be_empty
end
end end
end end
end end
...@@ -87,6 +87,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr ...@@ -87,6 +87,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end end
it 'does not move the repositories when hashed storage is enabled' do
project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:repository])
expect(subject).not_to receive(:move_repository)
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end
it 'moves uploads' do it 'moves uploads' do
expect(subject).to receive(:move_uploads) expect(subject).to receive(:move_uploads)
.with('known-parent/the-path', 'known-parent/the-path0') .with('known-parent/the-path', 'known-parent/the-path0')
...@@ -94,6 +102,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr ...@@ -94,6 +102,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end end
it 'does not move uploads when hashed storage is enabled for attachments' do
project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:attachments])
expect(subject).not_to receive(:move_uploads)
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end
it 'moves pages' do it 'moves pages' do
expect(subject).to receive(:move_pages) expect(subject).to receive(:move_pages)
.with('known-parent/the-path', 'known-parent/the-path0') .with('known-parent/the-path', 'known-parent/the-path0')
......
...@@ -1783,6 +1783,32 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -1783,6 +1783,32 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
end end
describe '#delete_all_refs_except' do
let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
before do
repository.write_ref("refs/delete/a", "0b4bc9a49b562e85de7cc9e834518ea6828729b9")
repository.write_ref("refs/also-delete/b", "12d65c8dd2b2676fa3ac47d955accc085a37a9c1")
repository.write_ref("refs/keep/c", "6473c90867124755509e100d0d35ebdc85a0b6ae")
repository.write_ref("refs/also-keep/d", "0b4bc9a49b562e85de7cc9e834518ea6828729b9")
end
after do
ensure_seeds
end
it 'deletes all refs except those with the specified prefixes' do
repository.delete_all_refs_except(%w(refs/keep refs/also-keep refs/heads))
expect(repository.ref_exists?("refs/delete/a")).to be(false)
expect(repository.ref_exists?("refs/also-delete/b")).to be(false)
expect(repository.ref_exists?("refs/keep/c")).to be(true)
expect(repository.ref_exists?("refs/also-keep/d")).to be(true)
expect(repository.ref_exists?("refs/heads/master")).to be(true)
end
end
def create_remote_branch(repository, remote_name, branch_name, source_branch_name) def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name } source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged rugged = repository.rugged
......
...@@ -104,4 +104,17 @@ describe Gitlab::GitalyClient::RefService do ...@@ -104,4 +104,17 @@ describe Gitlab::GitalyClient::RefService do
expect { client.ref_exists?('reXXXXX') }.to raise_error(ArgumentError) expect { client.ref_exists?('reXXXXX') }.to raise_error(ArgumentError)
end end
end end
describe '#delete_refs' do
let(:prefixes) { %w(refs/heads refs/keep-around) }
it 'sends a delete_refs message' do
expect_any_instance_of(Gitaly::RefService::Stub)
.to receive(:delete_refs)
.with(gitaly_request_with_params(except_with_prefix: prefixes), kind_of(Hash))
.and_return(double('delete_refs_response'))
client.delete_refs(except_with_prefixes: prefixes)
end
end
end end
...@@ -155,7 +155,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -155,7 +155,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end end
it 'has no source if source/target differ' do it 'has no source if source/target differ' do
expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1) expect(MergeRequest.find_by_title('MR2').source_project_id).to be_nil
end end
end end
......
...@@ -5,7 +5,7 @@ describe Milestones::DestroyService do ...@@ -5,7 +5,7 @@ describe Milestones::DestroyService do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) } let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) }
let!(:issue) { create(:issue, project: project, milestone: milestone) } let!(:issue) { create(:issue, project: project, milestone: milestone) }
let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
before do before do
project.team << [user, :master] project.team << [user, :master]
......
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