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:
USE_BUNDLE_INSTALL: "false"
NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json
stage: post-test
allow_failure: yes
allow_failure: true
retry: 0
only:
- branches
......@@ -416,7 +416,6 @@ ee_compat_check:
- /^[\d-]+-stable(-ee)?/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
allow_failure: no
retry: 0
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
......
......@@ -398,7 +398,7 @@ group :ed25519 do
end
# 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
......
......@@ -275,7 +275,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.52.0)
gitaly-proto (0.54.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -355,10 +355,10 @@ GEM
rake
grape_logging (1.7.0)
grape
grpc (1.6.6)
grpc (1.7.2)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (~> 0.5.1)
googleauth (>= 0.5.1, < 0.7)
haml (4.0.7)
tilt
haml_lint (0.26.0)
......@@ -1034,7 +1034,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.52.0)
gitaly-proto (~> 0.54.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
......
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Flash from '../flash';
import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
import sidebarEventHub from '../sidebar/event_hub';
import './models/issue';
import './models/label';
import './models/list';
......@@ -14,7 +15,7 @@ import './models/milestone';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
import './services/board_service';
import BoardService from './services/board_service';
import './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters';
......@@ -77,11 +78,16 @@ $(() => {
});
Store.rootPath = this.boardsEndpoint;
// Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
eventHub.$on('newDetailIssue', this.updateDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
eventHub.$off('newDetailIssue', this.updateDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
......@@ -112,6 +118,46 @@ $(() => {
methods: {
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 eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
export default {
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: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
......@@ -56,12 +42,30 @@ export default {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
Store.detail.issue = {};
eventHub.$emit('clearDetailIssue');
} else {
Store.detail.issue = this.issue;
eventHub.$emit('newDetailIssue', this.issue);
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 */
import boardNewIssue from './board_new_issue';
import boardCard from './board_card';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
......@@ -5,12 +5,13 @@
import Vue from 'vue';
import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
import assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
const Store = gl.issueBoards.BoardsStore;
......@@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({
new DueDateSelectors();
new LabelsSelect();
new Sidebar();
gl.Subscription.bindAll('.subscription');
},
components: {
assigneeTitle,
assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn,
'assignee-title': AssigneeTitle,
assignees: Assignees,
subscriptions,
},
});
......@@ -17,6 +17,11 @@ class ListIssue {
this.assignees = [];
this.selected = false;
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) {
this.milestone = new ListMilestone(obj.milestone);
......@@ -73,6 +78,14 @@ class ListIssue {
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) {
const data = {
issue: {
......
......@@ -2,7 +2,7 @@
import Vue from 'vue';
class BoardService {
export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
......@@ -88,6 +88,14 @@ class BoardService {
return this.issues.bulkUpdate(data);
}
static getIssueInfo(endpoint) {
return Vue.http.get(endpoint);
}
static toggleIssueSubscription(endpoint) {
return Vue.http.post(endpoint);
}
}
window.BoardService = BoardService;
......@@ -14,7 +14,6 @@ export default () => {
});
new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
gl.Subscription.bindAll('.subscription');
new DueDateSelectors();
window.sidebar = new Sidebar();
};
......@@ -29,7 +29,6 @@ import './commit/image_file';
// lib/utils
import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
import './lib/utils/pretty_time';
import './lib/utils/url_utility';
// behaviors
......@@ -80,7 +79,6 @@ import './right_sidebar';
import './search';
import './search_autocomplete';
import './smart_interval';
import './subscription';
import './subscription_select';
import initBreadcrumbs from './breadcrumb';
......
......@@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue';
export default {
......@@ -21,7 +22,7 @@ export default {
onToggleSubscription() {
this.mediator.toggleSubscription()
.catch(() => {
Flash('Error occurred when toggling the notification subscription');
Flash(__('Error occurred when toggling the notification subscription'));
});
},
},
......
......@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: false,
},
id: {
type: Number,
required: false,
},
},
components: {
loadingButton,
......@@ -32,7 +36,7 @@ export default {
},
methods: {
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 @@
Sample configuration:
<icon
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
name="retry"
:size="32"
css-classes="top"
/>
*/
......
......@@ -50,7 +50,9 @@
<template>
<div class="md-header">
<ul class="nav-links clearfix">
<li :class="{ active: !previewMarkdown }">
<li
class="md-header-tab"
:class="{ active: !previewMarkdown }">
<a
class="js-write-link"
href="#md-write-holder"
......@@ -59,7 +61,9 @@
Write
</a>
</li>
<li :class="{ active: previewMarkdown }">
<li
class="md-header-tab"
:class="{ active: previewMarkdown }">
<a
class="js-preview-link"
href="#md-preview-holder"
......@@ -68,56 +72,52 @@
Preview
</a>
</li>
<li class="pull-right">
<div class="toolbar-group">
<toolbar-button
tag="**"
button-title="Add bold text"
icon="bold" />
<toolbar-button
tag="*"
button-title="Add italic text"
icon="italic" />
<toolbar-button
tag="> "
:prepend="true"
button-title="Insert a quote"
icon="quote" />
<toolbar-button
tag="`"
tag-block="```"
button-title="Insert code"
icon="code" />
<toolbar-button
tag="* "
:prepend="true"
button-title="Add a bullet list"
icon="list-bulleted" />
<toolbar-button
tag="1. "
:prepend="true"
button-title="Add a numbered list"
icon="list-numbered" />
<toolbar-button
tag="* [ ] "
:prepend="true"
button-title="Add a task list"
icon="task-done" />
</div>
<div class="toolbar-group">
<button
v-tooltip
aria-label="Go full screen"
class="toolbar-btn js-zen-enter"
data-container="body"
tabindex="-1"
title="Go full screen"
type="button">
<icon
name="screen-full">
</icon>
</button>
</div>
<li class="md-header-toolbar">
<toolbar-button
tag="**"
button-title="Add bold text"
icon="bold" />
<toolbar-button
tag="*"
button-title="Add italic text"
icon="italic" />
<toolbar-button
tag="> "
:prepend="true"
button-title="Insert a quote"
icon="quote" />
<toolbar-button
tag="`"
tag-block="```"
button-title="Insert code"
icon="code" />
<toolbar-button
tag="* "
:prepend="true"
button-title="Add a bullet list"
icon="list-bulleted" />
<toolbar-button
tag="1. "
:prepend="true"
button-title="Add a numbered list"
icon="list-numbered" />
<toolbar-button
tag="* [ ] "
:prepend="true"
button-title="Add a task list"
icon="task-done" />
<button
v-tooltip
aria-label="Go full screen"
class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
data-container="body"
tabindex="-1"
title="Go full screen"
type="button">
<icon
name="screen-full">
</icon>
</button>
</li>
</ul>
</div>
......
......@@ -40,7 +40,7 @@
<button
v-tooltip
type="button"
class="toolbar-btn js-md hidden-xs"
class="toolbar-btn js-md"
tabindex="-1"
data-container="body"
:data-md-tag="tag"
......
......@@ -430,6 +430,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
.prepend-top-15 { margin-top: 15px; }
.prepend-top-default { margin-top: $gl-padding !important; }
......
......@@ -57,6 +57,7 @@
.md-header {
.nav-links {
a {
width: 100%;
padding-top: 0;
line-height: 19px;
......@@ -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 {
color: $gl-text-color;
padding-top: 10px;
......@@ -126,16 +149,6 @@
}
}
.toolbar-group {
float: left;
margin-right: -5px;
margin-left: $gl-padding;
&:first-child {
margin-left: 0;
}
}
.toolbar-btn {
float: left;
padding: 0 7px;
......@@ -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 {
overflow-y: auto;
overflow-x: hidden;
......
......@@ -196,7 +196,11 @@ class ApplicationController < ActionController::Base
end
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
end
end
......
......@@ -84,6 +84,7 @@ module Boards
resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true,
sidebar_endpoints: true,
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
......
......@@ -222,7 +222,7 @@ module MarkupHelper
data = options[:data].merge({ container: 'body' })
content_tag :button,
type: 'button',
class: 'toolbar-btn js-md has-tooltip hidden-xs',
class: 'toolbar-btn js-md has-tooltip',
tabindex: -1,
data: data,
title: options[:title],
......
......@@ -246,7 +246,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
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)
json[:labels] = labels.as_json(
......
......@@ -10,6 +10,8 @@ module MergeRequests
attr_reader :merge_request, :source
delegate :merge_jid, :state, to: :@merge_request
def execute(merge_request)
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
FfMergeService.new(project, current_user, params).execute(merge_request)
......@@ -27,6 +29,7 @@ module MergeRequests
success
end
end
log_info("Merge process finished on JID #{merge_jid} with state #{state}")
rescue MergeError => e
handle_merge_error(log_message: e.message, save_message_on_model: true)
end
......@@ -49,7 +52,9 @@ module MergeRequests
def commit
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)
log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
raise MergeError, 'Conflicts detected during merge' unless commit_id
......@@ -63,7 +68,9 @@ module MergeRequests
end
def after_merge
log_info("Post merge started on JID #{merge_jid} with state #{state}")
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?
DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
......@@ -92,6 +99,11 @@ module MergeRequests
@merge_request.update(merge_error: log_message) if save_message_on_model
end
def log_info(message)
@logger ||= Rails.logger
@logger.info("#{merge_request_info} - #{message}")
end
def merge_request_info
merge_request.to_reference(full: true)
end
......
......@@ -10,25 +10,23 @@
.md-area
.md-header
%ul.nav-links.clearfix
%li.active
%li.md-header-tab.active
%a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 }
Write
%li
%li.md-header-tab
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview
%li.pull-right
.toolbar-group
= 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: "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: "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: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
.toolbar-group
%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")
%li.md-header-toolbar
= 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: "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: "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: "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" } }
= sprite_icon("screen-full")
.md-write-holder
= yield
......
......@@ -40,7 +40,7 @@
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- 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
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
......
......@@ -4,7 +4,7 @@
.sidebar-container
.blocks-container
.block
%strong.prepend-top-10
%strong.inline.prepend-top-8
= @build.name
- 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
......
- if current_user
.block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" }
%span.issuable-header-text.hide-collapsed.pull-left
Notifications
%button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
%span
{{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
.block.subscriptions
%subscriptions{ ":loading" => "issue.isFetching && issue.isFetching.subscriptions",
":subscribed" => "issue.subscribed",
":id" => "issue.id" }
---
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 @@
#
# 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
enable_extension "plpgsql"
......@@ -1040,7 +1040,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
create_table "merge_requests", force: :cascade do |t|
t.string "target_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 "assignee_id"
t.string "title"
......@@ -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", ["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", ["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", ["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
......@@ -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", ["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", ["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|
t.integer "merge_request_id", null: false
......@@ -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_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", "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", "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", "merge_requests", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
......
......@@ -79,7 +79,7 @@ PUT /application/settings
| `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 |
| `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_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` |
......
......@@ -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
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: '')`
**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)
**css_class (optional)** If you want to add additional css classes
**Example**
**Example**
`= 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 :
`<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
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)`
## 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.
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
......
......@@ -80,13 +80,13 @@ errors during usage.
- 256GB RAM supports up to 32,000 users
- 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
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
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
......@@ -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.
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
......
......@@ -58,9 +58,9 @@ module Gitlab
def protection_options
{
"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,
"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,
"Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL
"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: 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, but masters can. No-one can force push or delete the branch." => PROTECTION_FULL
}
end
......
......@@ -61,9 +61,9 @@ module Gitlab
def import_wiki
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")
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
errors << { type: :wiki, errors: e.message }
end
......
......@@ -68,6 +68,11 @@ module Gitlab
has_one :route, as: :source
self.table_name = 'projects'
HASHED_STORAGE_FEATURES = {
repository: 1,
attachments: 2
}.freeze
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage]['path']
end
......@@ -76,6 +81,13 @@ module Gitlab
def self.name
'Project'
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
......
......@@ -22,9 +22,11 @@ module Gitlab
end
def move_project_folders(project, old_full_path, new_full_path)
move_repository(project, old_full_path, new_full_path)
move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
move_uploads(old_full_path, new_full_path)
unless project.hashed_storage?(:repository)
move_repository(project, 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)
end
......
......@@ -304,7 +304,13 @@ module Gitlab
end
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
# Returns an Array of all ref names, except when it's matching pattern
......
......@@ -126,6 +126,15 @@ module Gitlab
GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request)
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
def consume_refs_response(response)
......
module Gitlab
module ImportExport
class MergeRequestParser
FORKED_PROJECT_ID = -1
FORKED_PROJECT_ID = nil
def initialize(project, diff_head_sha, merge_request, relation_hash)
@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
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
##
# 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
#
......
......@@ -9,5 +9,16 @@ module QA
expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/)
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
......@@ -6,6 +6,10 @@ describe ApplicationController do
describe '#check_password_expiration' do
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
user.password_expires_at = Time.new(2002)
......
......@@ -167,19 +167,36 @@ describe "Admin::Users" do
it 'sees impersonation log out icon' do
icon = first('.fa.fa-user-secret')
expect(icon).not_to eql nil
expect(icon).not_to be nil
end
it 'logs out of impersonated user back to original user' do
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
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 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
......
......@@ -331,11 +331,29 @@ describe 'Issue Boards', :js do
context 'subscription' do
it 'changes issue subscription' do
click_card(card)
wait_for_requests
page.within('.subscription') do
page.within('.subscriptions') do
click_button 'Subscribe'
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
......
......@@ -13,6 +13,8 @@
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
"issue_sidebar_endpoint": { "type": "string" },
"toggle_subscription_endpoint": { "type": "string" },
"project": {
"id": { "type": "integer" },
"path": { "type": "string" }
......
......@@ -9,10 +9,11 @@
import Vue from 'vue';
import '~/boards/models/assignee';
import eventHub from '~/boards/eventhub';
import '~/boards/models/list';
import '~/boards/models/label';
import '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card';
import boardCard from '~/boards/components/board_card.vue';
import './mock_data';
describe('Board card', () => {
......@@ -157,33 +158,35 @@ describe('Board card', () => {
});
it('sets detail issue to card issue on mouse up', () => {
spyOn(eventHub, '$emit');
triggerEvent('mousedown');
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);
});
it('adds active class if detail issue is set', (done) => {
triggerEvent('mousedown');
triggerEvent('mouseup');
setTimeout(() => {
expect(vm.$el.classList.contains('is-active')).toBe(true);
done();
}, 0);
vm.detailIssue.issue = vm.issue;
Vue.nextTick()
.then(() => {
expect(vm.$el.classList.contains('is-active')).toBe(true);
})
.then(done)
.catch(done.fail);
});
it('resets detail issue to empty if already set', () => {
triggerEvent('mousedown');
triggerEvent('mouseup');
spyOn(eventHub, '$emit');
expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
gl.issueBoards.BoardsStore.detail.issue = vm.issue;
triggerEvent('mousedown');
triggerEvent('mouseup');
expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue');
});
});
});
......@@ -133,6 +133,19 @@ describe('Issue model', () => {
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', () => {
it('passes assignee ids when there are assignees', (done) => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
......
......@@ -98,7 +98,6 @@ describe('LoadingButton', function () {
it('does not call given callback when disabled because of loading', () => {
vm = mountComponent(LoadingButton, {
loading: true,
indeterminate: true,
});
spyOn(vm, '$emit');
......
......@@ -54,11 +54,13 @@ describe Gitlab::BitbucketImport::Importer do
create(
:project,
import_source: project_identifier,
import_url: "https://bitbucket.org/#{project_identifier}.git",
import_data_attributes: { credentials: data }
)
end
let(:importer) { described_class.new(project) }
let(:gitlab_shell) { double }
let(:issues_statuses_sample_data) do
{
......@@ -67,6 +69,10 @@ describe Gitlab::BitbucketImport::Importer do
}
end
before do
allow(importer).to receive(:gitlab_shell) { gitlab_shell }
end
context 'issues statuses' do
before do
# HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this
......@@ -110,15 +116,36 @@ describe Gitlab::BitbucketImport::Importer do
end
it 'maps statuses to open or closed' do
allow(importer).to receive(:import_wiki)
importer.execute
expect(project.issues.where(state: "closed").size).to eq(5)
expect(project.issues.where(state: "opened").size).to eq(2)
end
it 'calls import_wiki' do
expect(importer).to receive(:import_wiki)
importer.execute
describe 'wiki import' do
it 'is skipped when the wiki exists' do
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
......@@ -87,6 +87,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
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
expect(subject).to receive(:move_uploads)
.with('known-parent/the-path', 'known-parent/the-path0')
......@@ -94,6 +102,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
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
expect(subject).to receive(:move_pages)
.with('known-parent/the-path', 'known-parent/the-path0')
......
......@@ -1783,6 +1783,32 @@ describe Gitlab::Git::Repository, seed_helper: true do
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)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged
......
......@@ -104,4 +104,17 @@ describe Gitlab::GitalyClient::RefService do
expect { client.ref_exists?('reXXXXX') }.to raise_error(ArgumentError)
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
......@@ -155,7 +155,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
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
......
......@@ -5,7 +5,7 @@ describe Milestones::DestroyService do
let(:project) { create(:project) }
let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) }
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
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