Commit 25c8d7ce authored by Stan Hu's avatar Stan Hu

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

CE upstream - 2018-10-01 15:21 UTC

See merge request gitlab-org/gitlab-ee!7732
parents 59ced21c e0028387
......@@ -125,7 +125,7 @@ stages:
# Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes.
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
# https://docs.gitlab.com/ce/development/documentation/#testing
.except-docs: &except-docs
except:
- /(^docs[\/-].*|.*-docs$)/
......
......@@ -435,7 +435,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.117.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
......
......@@ -298,7 +298,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gitaly-proto (0.117.0)
gitaly-proto (0.118.1)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
......@@ -1062,7 +1062,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.117.0)
gitaly-proto (~> 0.118.1)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
......
......@@ -301,7 +301,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gitaly-proto (0.117.0)
gitaly-proto (0.118.1)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
......@@ -1071,7 +1071,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.117.0)
gitaly-proto (~> 0.118.1)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
......
......@@ -151,7 +151,8 @@
<a
:href="issue.path"
:title="issue.title"
class="js-no-trigger">{{ issue.title }}</a>
class="js-no-trigger"
@mousemove.stop>{{ issue.title }}</a>
<span
v-if="issueId"
class="board-card-number append-right-5"
......
......@@ -50,7 +50,9 @@ export default {
this.stopPipelinePolling();
},
methods: {
...mapActions(['setRightPane']),
...mapActions('rightPane', {
openRightPane: 'open',
}),
...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
startTimer() {
this.intervalId = setInterval(() => {
......@@ -88,7 +90,7 @@ export default {
<button
type="button"
class="p-0 border-0 h-50"
@click="setRightPane($options.rightSidebarViews.pipelines)"
@click="openRightPane($options.rightSidebarViews.pipelines)"
>
<ci-icon
v-tooltip
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import _ from 'underscore';
import { __ } from '~/locale';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
......@@ -30,14 +31,10 @@ export default {
},
},
computed: {
...mapState(['rightPane', 'currentMergeRequestId', 'clientsidePreviewEnabled']),
...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']),
...mapState('rightPane', ['isOpen', 'currentView']),
...mapGetters(['packageJson']),
pipelinesActive() {
return (
this.rightPane === rightSidebarViews.pipelines ||
this.rightPane === rightSidebarViews.jobsDetail
);
},
...mapGetters('rightPane', ['isActiveView', 'isAliveView']),
showLivePreview() {
return this.packageJson && this.clientsidePreviewEnabled;
},
......@@ -46,22 +43,26 @@ export default {
{
show: this.currentMergeRequestId,
title: __('Merge Request'),
isActive: this.rightPane === rightSidebarViews.mergeRequestInfo,
view: rightSidebarViews.mergeRequestInfo,
views: [
rightSidebarViews.mergeRequestInfo,
],
icon: 'text-description',
},
{
show: true,
title: __('Pipelines'),
isActive: this.pipelinesActive,
view: rightSidebarViews.pipelines,
views: [
rightSidebarViews.pipelines,
rightSidebarViews.jobsDetail,
],
icon: 'rocket',
},
{
show: this.showLivePreview,
title: __('Live preview'),
isActive: this.rightPane === rightSidebarViews.clientSidePreview,
view: rightSidebarViews.clientSidePreview,
views: [
rightSidebarViews.clientSidePreview,
],
icon: 'live-preview',
},
];
......@@ -71,13 +72,26 @@ export default {
.concat(this.extensionTabs)
.filter(tab => tab.show);
},
tabViews() {
return _.flatten(this.tabs.map(tab => tab.views));
},
aliveTabViews() {
return this.tabViews.filter(view => this.isAliveView(view.name));
},
},
methods: {
...mapActions(['setRightPane']),
clickTab(e, view) {
...mapActions('rightPane', ['toggleOpen', 'open']),
clickTab(e, tab) {
e.target.blur();
this.setRightPane(view);
if (this.isActiveTab(tab)) {
this.toggleOpen();
} else {
this.open(tab.views[0]);
}
},
isActiveTab(tab) {
return tab.views.some(view => this.isActiveView(view.name));
},
},
};
......@@ -88,15 +102,22 @@ export default {
class="multi-file-commit-panel ide-right-sidebar"
>
<resizable-panel
v-if="rightPane"
v-show="isOpen"
:collapsible="false"
:initial-width="350"
:min-size="350"
:class="`ide-right-sidebar-${rightPane}`"
:class="`ide-right-sidebar-${currentView}`"
side="right"
class="multi-file-commit-panel-inner"
>
<component :is="rightPane" />
<div
v-for="tabView in aliveTabViews"
v-show="isActiveView(tabView.name)"
:key="tabView.name"
class="h-100"
>
<component :is="tabView.name" />
</div>
</resizable-panel>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
......@@ -109,13 +130,13 @@ export default {
:title="tab.title"
:aria-label="tab.title"
:class="{
active: tab.isActive
active: isActiveTab(tab) && isOpen
}"
data-container="body"
data-placement="left"
class="ide-sidebar-link is-right"
type="button"
@click="clickTab($event, tab.view)"
@click="clickTab($event, tab)"
>
<icon
:size="16"
......
......@@ -22,12 +22,14 @@ export default {
},
},
computed: {
...mapState('rightPane', {
rightPaneIsOpen: 'isOpen',
}),
...mapState([
'rightPanelCollapsed',
'viewer',
'panelResizing',
'currentActivityView',
'rightPane',
]),
...mapGetters([
'currentMergeRequest',
......@@ -99,7 +101,7 @@ export default {
this.editor.updateDimensions();
}
},
rightPane() {
rightPaneIsOpen() {
this.editor.updateDimensions();
},
},
......
......@@ -29,10 +29,10 @@ export const diffModes = {
};
export const rightSidebarViews = {
pipelines: 'pipelines-list',
jobsDetail: 'jobs-detail',
mergeRequestInfo: 'merge-request-info',
clientSidePreview: 'clientside',
pipelines: { name: 'pipelines-list', keepAlive: true },
jobsDetail: { name: 'jobs-detail', keepAlive: false },
mergeRequestInfo: { name: 'merge-request-info', keepAlive: true },
clientSidePreview: { name: 'clientside', keepAlive: false },
};
export const stageKeys = {
......
......@@ -184,10 +184,6 @@ export const burstUnusedSeal = ({ state, commit }) => {
}
};
export const setRightPane = ({ commit }, view) => {
commit(types.SET_RIGHT_PANE, view);
};
export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
export const setErrorMessage = ({ commit }, errorMessage) =>
......
......@@ -9,6 +9,7 @@ import pipelines from './modules/pipelines';
import mergeRequests from './modules/merge_requests';
import branches from './modules/branches';
import fileTemplates from './modules/file_templates';
import paneModule from './modules/pane';
Vue.use(Vuex);
......@@ -24,6 +25,7 @@ export const createStore = () =>
mergeRequests,
branches,
fileTemplates: fileTemplates(),
rightPane: paneModule(),
},
});
......
import * as types from './mutation_types';
export const toggleOpen = ({ dispatch, state }, view) => {
if (state.isOpen) {
dispatch('close');
} else {
dispatch('open', view);
}
};
export const open = ({ commit }, view) => {
commit(types.SET_OPEN, true);
if (view) {
const { name, keepAlive } = view;
commit(types.SET_CURRENT_VIEW, name);
if (keepAlive) {
commit(types.KEEP_ALIVE_VIEW, name);
}
}
};
export const close = ({ commit }) => {
commit(types.SET_OPEN, false);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
export const isActiveView = state => view => state.currentView === view;
export const isAliveView = (state, getters) => view =>
state.keepAliveViews[view] || (state.isOpen && getters.isActiveView(view));
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
export default () => ({
namespaced: true,
state: state(),
actions,
getters,
mutations,
});
export const SET_OPEN = 'SET_OPEN';
export const SET_CURRENT_VIEW = 'SET_CURRENT_VIEW';
export const KEEP_ALIVE_VIEW = 'KEEP_ALIVE_VIEW';
import * as types from './mutation_types';
export default {
[types.SET_OPEN](state, isOpen) {
Object.assign(state, {
isOpen,
});
},
[types.SET_CURRENT_VIEW](state, currentView) {
Object.assign(state, {
currentView,
});
},
[types.KEEP_ALIVE_VIEW](state, viewName) {
Object.assign(state.keepAliveViews, {
[viewName]: true,
});
},
};
export default () => ({
isOpen: false,
currentView: null,
keepAliveViews: {},
});
......@@ -113,7 +113,7 @@ export const toggleStageCollapsed = ({ commit }, stageId) =>
export const setDetailJob = ({ commit, dispatch }, job) => {
commit(types.SET_DETAIL_JOB, job);
dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, {
dispatch('rightPane/open', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, {
root: true,
});
};
......
......@@ -68,8 +68,6 @@ export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
export const CLEAR_PROJECTS = 'CLEAR_PROJECTS';
export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
......
......@@ -166,11 +166,6 @@ export default {
unusedSeal: false,
});
},
[types.SET_RIGHT_PANE](state, view) {
Object.assign(state, {
rightPane: state.rightPane === view ? null : view,
});
},
[types.SET_LINKS](state, links) {
Object.assign(state, { links });
},
......
......@@ -23,7 +23,6 @@ export default () => ({
currentActivityView: activityBarViews.edit,
unusedSeal: true,
fileFindVisible: false,
rightPane: null,
links: {},
errorMessage: null,
entryModal: {
......
......@@ -324,6 +324,16 @@ img.emoji {
word-wrap: break-word;
}
.checkbox-icon-inline-wrapper {
.checkbox {
display: inline;
label {
display: inline;
}
}
}
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
......
......@@ -673,6 +673,14 @@ class Repository
end
end
def list_last_commits_for_tree(sha, path, offset: 0, limit: 25)
commits = raw_repository.list_last_commits_for_tree(sha, path, offset: offset, limit: limit)
commits.each do |path, commit|
commits[path] = ::Commit.new(commit, @project)
end
end
def last_commit_for_path(sha, path)
commit = raw_repository.last_commit_for_path(sha, path)
::Commit.new(commit, @project) if commit
......
......@@ -57,7 +57,6 @@ module Users
:force_random_password,
:hide_no_password,
:hide_no_ssh_key,
:key_id,
:linkedin,
:name,
:password,
......@@ -71,7 +70,10 @@ module Users
:twitter,
:username,
:website_url,
:private_profile
:private_profile,
:organization,
:location,
:public_email
]
end
......
......@@ -109,10 +109,11 @@
= f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters.")
%hr
%h5= ("Private profile")
- private_profile_label = capture do
= s_("Profiles|Don't display activity-related personal information on your profiles")
.checkbox-icon-inline-wrapper
- private_profile_label = capture do
= s_("Profiles|Don't display activity-related personal information on your profiles")
= f.check_box :private_profile, label: private_profile_label
= link_to icon('question-circle'), help_page_path('user/profile/index.md', anchor: 'private-profile')
= f.check_box :private_profile, label: private_profile_label
%h5= s_("Profiles|Private contributions")
= f.check_box :include_private_contributions, label: 'Include private contributions on my profile'
.help-block
......
---
title: Fix link handling for issue cards to avoid too sensitive drag events.
merge_request: 21910
author: Johann Hubert Sonntagbauer
type: fixed
---
title: Adds support for Gitaly ListLastCommitsForTree RPC in order to make bulk-fetch
of commits more performant
merge_request: 21921
author:
type: performance
---
title: Prevents private profile help link from toggling checkbox
merge_request: 21757
author:
type: other
---
title: Add support for setting the public email through the api
merge_request: 21938
author: Alexis Reigel
type: added
---
title: Allow setting user's organization and location attributes through the API by adding them to the list of allowed parameters
merge_request: 21938
author: Alexis Reigel
type: fixed
---
title: Prevent Error 500s with invalid relative links
merge_request: 22001
author:
type: fixed
......@@ -291,6 +291,7 @@ Parameters:
- `provider` (optional) - External provider name
- `bio` (optional) - User's biography
- `location` (optional) - User's location
- `public_email` (optional) - The public email of the user
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
- `skip_confirmation` (optional) - Skip confirmation - true or false (default)
......@@ -309,27 +310,28 @@ PUT /users/:id
Parameters:
- `email` - Email
- `username` - Username
- `name` - Name
- `password` - Password
- `skype` - Skype ID
- `linkedin` - LinkedIn
- `twitter` - Twitter account
- `website_url` - Website URL
- `organization` - Organization name
- `projects_limit` - Limit projects each user can create
- `extern_uid` - External UID
- `provider` - External provider name
- `bio` - User's biography
- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
- `email` - Email
- `username` - Username
- `name` - Name
- `password` - Password
- `skype` - Skype ID
- `linkedin` - LinkedIn
- `twitter` - Twitter account
- `website_url` - Website URL
- `organization` - Organization name
- `projects_limit` - Limit projects each user can create
- `extern_uid` - External UID
- `provider` - External provider name
- `bio` - User's biography
- `location` (optional) - User's location
- `public_email` (optional) - The public email of the user
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
- `skip_reconfirmation` (optional) - Skip reconfirmation - true or false (default)
- `external` (optional) - Flags the user as external - true or false(default)
- `external` (optional) - Flags the user as external - true or false(default)
- `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user
- `avatar` (optional) - Image file for user's avatar
- `private_profile` (optional) - User's profile is private - true or false
- `avatar` (optional) - Image file for user's avatar
- `private_profile` (optional) - User's profile is private - true or false
On password update, user will be forced to change it upon next login.
Note, at the moment this method does only return a `404` error,
......
......@@ -17,7 +17,7 @@ Two things need to be configured for the interactive web terminal to work:
- The Runner needs to have [`[session_server]` configured
properly][session-server]
- Web terminals need to be
- If you are using a reverse proxy with your GitLab instance, web terminals need to be
[enabled](../../administration/integration/terminal.md#enabling-and-disabling-terminal-support)
## Debugging a running job
......
......@@ -42,12 +42,12 @@ module API
optional :provider, type: String, desc: 'The external provider'
optional :bio, type: String, desc: 'The biography of the user'
optional :location, type: String, desc: 'The location of the user'
optional :public_email, type: String, desc: 'The public email of the user'
optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
optional :avatar, type: File, desc: 'Avatar image for user'
optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile'
optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user'
all_or_none_of :extern_uid, :provider
# EE
......
......@@ -60,7 +60,11 @@ module Banzai
path_parts.unshift(relative_url_root, project.full_path)
end
path = Addressable::URI.escape(File.join(*path_parts))
begin
path = Addressable::URI.escape(File.join(*path_parts))
rescue Addressable::URI::InvalidURIError
return
end
html_attr.value =
if context[:only_path]
......
......@@ -953,6 +953,12 @@ module Gitlab
end
end
def list_last_commits_for_tree(sha, path, offset: 0, limit: 25)
wrapped_gitaly_errors do
gitaly_commit_client.list_last_commits_for_tree(sha, path, offset: offset, limit: limit)
end
end
def last_commit_for_path(sha, path)
wrapped_gitaly_errors do
gitaly_commit_client.last_commit_for_path(sha, path)
......
......@@ -148,6 +148,24 @@ module Gitlab
GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count
end
def list_last_commits_for_tree(revision, path, offset: 0, limit: 25)
request = Gitaly::ListLastCommitsForTreeRequest.new(
repository: @gitaly_repo,
revision: encode_binary(revision),
path: encode_binary(path.to_s),
offset: offset,
limit: limit
)
response = GitalyClient.call(@repository.storage, :commit_service, :list_last_commits_for_tree, request, timeout: GitalyClient.medium_timeout)
response.each_with_object({}) do |gitaly_response, hsh|
gitaly_response.commits.each do |commit_for_tree|
hsh[commit_for_tree.path] = Gitlab::Git::Commit.new(@repository, commit_for_tree.commit)
end
end
end
def last_commit_for_path(revision, path)
request = Gitaly::LastCommitForPathRequest.new(
repository: @gitaly_repo,
......
......@@ -75,25 +75,29 @@ module Gitlab
end
def fill_last_commits!(entries)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433
Gitlab::GitalyClient.allow_n_plus_1_calls do
entries.each do |entry|
raw_commit = repository.last_commit_for_path(commit.id, entry_path(entry))
# Ensure the path is in "path/" format
ensured_path =
if path
File.join(*[path, ""])
end
commits_hsh = repository.list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit)
if raw_commit
commit = resolve_commit(raw_commit)
entries.each do |entry|
path_key = entry_path(entry)
commit = cache_commit(commits_hsh[path_key])
entry[:commit] = commit
entry[:commit_path] = commit_path(commit)
end
if commit
entry[:commit] = commit
entry[:commit_path] = commit_path(commit)
end
end
end
def resolve_commit(raw_commit)
return nil unless raw_commit.present?
def cache_commit(commit)
return nil unless commit.present?
resolved_commits[raw_commit.id] ||= ::Commit.new(raw_commit, project)
resolved_commits[commit.id] ||= commit
end
def commit_path(commit)
......
......@@ -97,13 +97,13 @@ end
automated_cleanup = AutomatedCleanup.new
timed('Review apps cleanup') do
automated_cleanup.perform_gitlab_environment_cleanup!(days_for_stop: 5, days_for_delete: 6)
automated_cleanup.perform_gitlab_environment_cleanup!(days_for_stop: 2, days_for_delete: 3)
end
puts
timed('Helm releases cleanup') do
automated_cleanup.perform_helm_releases_cleanup!(days: 7)
automated_cleanup.perform_helm_releases_cleanup!(days: 3)
end
exit(0)
import Vue from 'vue';
import store from '~/ide/stores';
import ideStatusBar from '~/ide/components/ide_status_bar.vue';
import { rightSidebarViews } from '~/ide/constants';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
import { projectData } from '../mock_data';
......@@ -64,7 +65,7 @@ describe('ideStatusBar', () => {
describe('pipeline status', () => {
it('opens right sidebar on clicking icon', done => {
spyOn(vm, 'setRightPane');
spyOn(vm, 'openRightPane');
Vue.set(vm.$store.state.pipelines, 'latestPipeline', {
details: {
status: {
......@@ -80,7 +81,7 @@ describe('ideStatusBar', () => {
.then(() => {
vm.$el.querySelector('.ide-status-pipeline button').click();
expect(vm.setRightPane).toHaveBeenCalledWith('pipelines-list');
expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
})
.then(done)
.catch(done.fail);
......
......@@ -25,7 +25,8 @@ describe('IDE right pane', () => {
describe('active', () => {
it('renders merge request button as active', done => {
vm.$store.state.rightPane = rightSidebarViews.mergeRequestInfo;
vm.$store.state.rightPane.isOpen = true;
vm.$store.state.rightPane.currentView = rightSidebarViews.mergeRequestInfo.name;
vm.$store.state.currentMergeRequestId = '123';
vm.$store.state.currentProjectId = 'gitlab-ce';
vm.$store.state.currentMergeRequestId = 1;
......@@ -41,20 +42,21 @@ describe('IDE right pane', () => {
},
};
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-sidebar-link.active')).not.toBe(null);
expect(
vm.$el.querySelector('.ide-sidebar-link.active').getAttribute('data-original-title'),
).toBe('Merge Request');
done();
});
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.ide-sidebar-link.active')).not.toBe(null);
expect(
vm.$el.querySelector('.ide-sidebar-link.active').getAttribute('data-original-title'),
).toBe('Merge Request');
})
.then(done)
.catch(done.fail);
});
});
describe('click', () => {
beforeEach(() => {
spyOn(vm, 'setRightPane');
spyOn(vm, 'open');
});
it('sets view to merge request', done => {
......@@ -63,7 +65,7 @@ describe('IDE right pane', () => {
vm.$nextTick(() => {
vm.$el.querySelector('.ide-sidebar-link').click();
expect(vm.setRightPane).toHaveBeenCalledWith(rightSidebarViews.mergeRequestInfo);
expect(vm.open).toHaveBeenCalledWith(rightSidebarViews.mergeRequestInfo);
done();
});
......
......@@ -319,8 +319,8 @@ describe('RepoEditor', () => {
});
});
it('calls updateDimensions when rightPane is updated', done => {
vm.$store.state.rightPane = 'testing';
it('calls updateDimensions when rightPane is opened', done => {
vm.$store.state.rightPane.isOpen = true;
vm.$nextTick(() => {
expect(vm.editor.updateDimensions).toHaveBeenCalled();
......
......@@ -6,6 +6,7 @@ import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
import pipelinesState from '~/ide/stores/modules/pipelines/state';
import branchesState from '~/ide/stores/modules/branches/state';
import fileTemplatesState from '~/ide/stores/modules/file_templates/state';
import paneState from '~/ide/stores/modules/pane/state';
export const resetStore = store => {
const newState = {
......@@ -15,6 +16,7 @@ export const resetStore = store => {
pipelines: pipelinesState(),
branches: branchesState(),
fileTemplates: fileTemplatesState(),
rightPane: paneState(),
};
store.replaceState(newState);
};
......
import * as actions from '~/ide/stores/modules/pane/actions';
import * as types from '~/ide/stores/modules/pane/mutation_types';
import testAction from 'spec/helpers/vuex_action_helper';
describe('IDE pane module actions', () => {
const TEST_VIEW = { name: 'test' };
const TEST_VIEW_KEEP_ALIVE = { name: 'test-keep-alive', keepAlive: true };
describe('toggleOpen', () => {
it('dispatches open if closed', done => {
testAction(
actions.toggleOpen,
TEST_VIEW,
{ isOpen: false },
[],
[{ type: 'open', payload: TEST_VIEW }],
done,
);
});
it('dispatches close if opened', done => {
testAction(
actions.toggleOpen,
TEST_VIEW,
{ isOpen: true },
[],
[{ type: 'close' }],
done,
);
});
});
describe('open', () => {
it('commits SET_OPEN', done => {
testAction(
actions.open,
null,
{},
[{ type: types.SET_OPEN, payload: true }],
[],
done,
);
});
it('commits SET_CURRENT_VIEW if view is given', done => {
testAction(
actions.open,
TEST_VIEW,
{},
[
{ type: types.SET_OPEN, payload: true },
{ type: types.SET_CURRENT_VIEW, payload: TEST_VIEW.name },
],
[],
done,
);
});
it('commits KEEP_ALIVE_VIEW if keepAlive is true', done => {
testAction(
actions.open,
TEST_VIEW_KEEP_ALIVE,
{},
[
{ type: types.SET_OPEN, payload: true },
{ type: types.SET_CURRENT_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name },
{ type: types.KEEP_ALIVE_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name },
],
[],
done,
);
});
});
describe('close', () => {
it('commits SET_OPEN', done => {
testAction(
actions.close,
null,
{},
[{ type: types.SET_OPEN, payload: false }],
[],
done,
);
});
});
});
import * as getters from '~/ide/stores/modules/pane/getters';
import state from '~/ide/stores/modules/pane/state';
describe('IDE pane module getters', () => {
const TEST_VIEW = 'test-view';
const TEST_KEEP_ALIVE_VIEWS = {
[TEST_VIEW]: true,
};
describe('isActiveView', () => {
it('returns true if given view matches currentView', () => {
const result = getters.isActiveView({ currentView: 'A' })('A');
expect(result).toBe(true);
});
it('returns false if given view does not match currentView', () => {
const result = getters.isActiveView({ currentView: 'A' })('B');
expect(result).toBe(false);
});
});
describe('isAliveView', () => {
it('returns true if given view is in keepAliveViews', () => {
const result = getters.isAliveView(
{ keepAliveViews: TEST_KEEP_ALIVE_VIEWS },
{},
)(TEST_VIEW);
expect(result).toBe(true);
});
it('returns true if given view is active view and open', () => {
const result = getters.isAliveView(
{ ...state(), isOpen: true },
{ isActiveView: () => true },
)(TEST_VIEW);
expect(result).toBe(true);
});
it('returns false if given view is active view and closed', () => {
const result = getters.isAliveView(
state(),
{ isActiveView: () => true },
)(TEST_VIEW);
expect(result).toBe(false);
});
it('returns false if given view is not activeView', () => {
const result = getters.isAliveView(
{ ...state(), isOpen: true },
{ isActiveView: () => false },
)(TEST_VIEW);
expect(result).toBe(false);
});
});
});
import state from '~/ide/stores/modules/pane/state';
import mutations from '~/ide/stores/modules/pane/mutations';
import * as types from '~/ide/stores/modules/pane/mutation_types';
describe('IDE pane module mutations', () => {
const TEST_VIEW = 'test-view';
let mockedState;
beforeEach(() => {
mockedState = state();
});
describe('SET_OPEN', () => {
it('sets isOpen', () => {
mockedState.isOpen = false;
mutations[types.SET_OPEN](mockedState, true);
expect(mockedState.isOpen).toBe(true);
});
});
describe('SET_CURRENT_VIEW', () => {
it('sets currentView', () => {
mockedState.currentView = null;
mutations[types.SET_CURRENT_VIEW](mockedState, TEST_VIEW);
expect(mockedState.currentView).toEqual(TEST_VIEW);
});
});
describe('KEEP_ALIVE_VIEW', () => {
it('adds entry to keepAliveViews', () => {
mutations[types.KEEP_ALIVE_VIEW](mockedState, TEST_VIEW);
expect(mockedState.keepAliveViews).toEqual({
[TEST_VIEW]: true,
});
});
});
});
......@@ -315,29 +315,29 @@ describe('IDE pipelines actions', () => {
'job',
mockedState,
[{ type: types.SET_DETAIL_JOB, payload: 'job' }],
[{ type: 'setRightPane', payload: 'jobs-detail' }],
[{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }],
done,
);
});
it('dispatches setRightPane as pipeline when job is null', done => {
it('dispatches rightPane/open as pipeline when job is null', done => {
testAction(
setDetailJob,
null,
mockedState,
[{ type: types.SET_DETAIL_JOB, payload: null }],
[{ type: 'setRightPane', payload: rightSidebarViews.pipelines }],
[{ type: 'rightPane/open', payload: rightSidebarViews.pipelines }],
done,
);
});
it('dispatches setRightPane as job', done => {
it('dispatches rightPane/open as job', done => {
testAction(
setDetailJob,
'job',
mockedState,
[{ type: types.SET_DETAIL_JOB, payload: 'job' }],
[{ type: 'setRightPane', payload: rightSidebarViews.jobsDetail }],
[{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }],
done,
);
});
......
......@@ -83,6 +83,11 @@ describe Banzai::Filter::RelativeLinkFilter do
expect { filter(act) }.not_to raise_error
end
it 'does not raise an exception with a space in the path' do
act = link("/uploads/d18213acd3732630991986120e167e3d/Landscape_8.jpg \nBut here's some more unexpected text :smile:)")
expect { filter(act) }.not_to raise_error
end
it 'ignores ref if commit is passed' do
doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') )
expect(doc.at_css('a')['href'])
......
......@@ -188,6 +188,57 @@ describe Repository do
end
end
describe '#list_last_commits_for_tree' do
let(:path_to_commit) do
{
"encoding" => "913c66a37b4a45b9769037c55c2d238bd0942d2e",
"files" => "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
".gitignore" => "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
".gitmodules" => "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"CHANGELOG" => "913c66a37b4a45b9769037c55c2d238bd0942d2e",
"CONTRIBUTING.md" => "6d394385cf567f80a8fd85055db1ab4c5295806f",
"Gemfile.zip" => "ae73cb07c9eeaf35924a10f713b364d32b2dd34f",
"LICENSE" => "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863",
"MAINTENANCE.md" => "913c66a37b4a45b9769037c55c2d238bd0942d2e",
"PROCESS.md" => "913c66a37b4a45b9769037c55c2d238bd0942d2e",
"README.md" => "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863",
"VERSION" => "913c66a37b4a45b9769037c55c2d238bd0942d2e",
"gitlab-shell" => "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"six" => "cfe32cf61b73a0d5e9f13e774abde7ff789b1660"
}
end
subject { repository.list_last_commits_for_tree(sample_commit.id, '.').id }
it 'returns the last commits for every entry in the current path' do
result = repository.list_last_commits_for_tree(sample_commit.id, '.')
result.each do |key, value|
result[key] = value.id
end
expect(result).to include(path_to_commit)
end
it 'returns the last commits for every entry in the current path starting from the offset' do
result = repository.list_last_commits_for_tree(sample_commit.id, '.', offset: path_to_commit.size - 1)
expect(result.size).to eq(1)
end
it 'returns a limited number of last commits for every entry in the current path starting from the offset' do
result = repository.list_last_commits_for_tree(sample_commit.id, '.', limit: 1)
expect(result.size).to eq(1)
end
it 'returns an empty hash when offset is out of bounds' do
result = repository.list_last_commits_for_tree(sample_commit.id, '.', offset: path_to_commit.size)
expect(result.size).to eq(0)
end
end
describe '#last_commit_for_path' do
shared_examples 'getting last commit for path' do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
......
......@@ -14,6 +14,49 @@ describe Users::BuildService do
expect(service.execute).to be_valid
end
context 'allowed params' do
let(:params) do
{
access_level: 1,
admin: 1,
avatar: anything,
bio: 1,
can_create_group: 1,
color_scheme_id: 1,
email: 1,
external: 1,
force_random_password: 1,
hide_no_password: 1,
hide_no_ssh_key: 1,
linkedin: 1,
name: 1,
password: 1,
password_automatically_set: 1,
password_expires_at: 1,
projects_limit: 1,
remember_me: 1,
skip_confirmation: 1,
skype: 1,
theme_id: 1,
twitter: 1,
username: 1,
website_url: 1,
private_profile: 1,
organization: 1,
location: 1,
public_email: 1
}
end
it 'sets all allowed attributes' do
admin_user # call first so the admin gets created before setting `expect`
expect(User).to receive(:new).with(hash_including(params)).and_call_original
service.execute
end
end
context 'with "user_default_external" application setting' do
using RSpec::Parameterized::TableSyntax
......
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