Commit 782095da authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-10-01

# Conflicts:
#	doc/api/users.md

[ci skip]
parents 7bbe8949 70f4a26b
...@@ -125,7 +125,7 @@ stages: ...@@ -125,7 +125,7 @@ stages:
# Skip all jobs except the ones that begin with 'docs/'. # Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes. # 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: &except-docs
except: except:
- /(^docs[\/-].*|.*-docs$)/ - /(^docs[\/-].*|.*-docs$)/
......
...@@ -435,7 +435,7 @@ group :ed25519 do ...@@ -435,7 +435,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.117.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly'
gem 'grpc', '~> 1.11.0' gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
......
...@@ -298,7 +298,7 @@ GEM ...@@ -298,7 +298,7 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.117.0) gitaly-proto (0.118.1)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -1062,7 +1062,7 @@ DEPENDENCIES ...@@ -1062,7 +1062,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.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.117.0) gitaly-proto (~> 0.118.1)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
......
...@@ -301,7 +301,7 @@ GEM ...@@ -301,7 +301,7 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.117.0) gitaly-proto (0.118.1)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -1071,7 +1071,7 @@ DEPENDENCIES ...@@ -1071,7 +1071,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.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.117.0) gitaly-proto (~> 0.118.1)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
......
...@@ -151,7 +151,8 @@ ...@@ -151,7 +151,8 @@
<a <a
:href="issue.path" :href="issue.path"
:title="issue.title" :title="issue.title"
class="js-no-trigger">{{ issue.title }}</a> class="js-no-trigger"
@mousemove.stop>{{ issue.title }}</a>
<span <span
v-if="issueId" v-if="issueId"
class="board-card-number append-right-5" class="board-card-number append-right-5"
......
...@@ -50,7 +50,9 @@ export default { ...@@ -50,7 +50,9 @@ export default {
this.stopPipelinePolling(); this.stopPipelinePolling();
}, },
methods: { methods: {
...mapActions(['setRightPane']), ...mapActions('rightPane', {
openRightPane: 'open',
}),
...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']), ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
startTimer() { startTimer() {
this.intervalId = setInterval(() => { this.intervalId = setInterval(() => {
...@@ -88,7 +90,7 @@ export default { ...@@ -88,7 +90,7 @@ export default {
<button <button
type="button" type="button"
class="p-0 border-0 h-50" class="p-0 border-0 h-50"
@click="setRightPane($options.rightSidebarViews.pipelines)" @click="openRightPane($options.rightSidebarViews.pipelines)"
> >
<ci-icon <ci-icon
v-tooltip v-tooltip
......
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import _ from 'underscore';
import { __ } from '~/locale'; import { __ } from '~/locale';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
...@@ -30,14 +31,10 @@ export default { ...@@ -30,14 +31,10 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['rightPane', 'currentMergeRequestId', 'clientsidePreviewEnabled']), ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']),
...mapState('rightPane', ['isOpen', 'currentView']),
...mapGetters(['packageJson']), ...mapGetters(['packageJson']),
pipelinesActive() { ...mapGetters('rightPane', ['isActiveView', 'isAliveView']),
return (
this.rightPane === rightSidebarViews.pipelines ||
this.rightPane === rightSidebarViews.jobsDetail
);
},
showLivePreview() { showLivePreview() {
return this.packageJson && this.clientsidePreviewEnabled; return this.packageJson && this.clientsidePreviewEnabled;
}, },
...@@ -46,22 +43,26 @@ export default { ...@@ -46,22 +43,26 @@ export default {
{ {
show: this.currentMergeRequestId, show: this.currentMergeRequestId,
title: __('Merge Request'), title: __('Merge Request'),
isActive: this.rightPane === rightSidebarViews.mergeRequestInfo, views: [
view: rightSidebarViews.mergeRequestInfo, rightSidebarViews.mergeRequestInfo,
],
icon: 'text-description', icon: 'text-description',
}, },
{ {
show: true, show: true,
title: __('Pipelines'), title: __('Pipelines'),
isActive: this.pipelinesActive, views: [
view: rightSidebarViews.pipelines, rightSidebarViews.pipelines,
rightSidebarViews.jobsDetail,
],
icon: 'rocket', icon: 'rocket',
}, },
{ {
show: this.showLivePreview, show: this.showLivePreview,
title: __('Live preview'), title: __('Live preview'),
isActive: this.rightPane === rightSidebarViews.clientSidePreview, views: [
view: rightSidebarViews.clientSidePreview, rightSidebarViews.clientSidePreview,
],
icon: 'live-preview', icon: 'live-preview',
}, },
]; ];
...@@ -71,13 +72,26 @@ export default { ...@@ -71,13 +72,26 @@ export default {
.concat(this.extensionTabs) .concat(this.extensionTabs)
.filter(tab => tab.show); .filter(tab => tab.show);
}, },
tabViews() {
return _.flatten(this.tabs.map(tab => tab.views));
},
aliveTabViews() {
return this.tabViews.filter(view => this.isAliveView(view.name));
},
}, },
methods: { methods: {
...mapActions(['setRightPane']), ...mapActions('rightPane', ['toggleOpen', 'open']),
clickTab(e, view) { clickTab(e, tab) {
e.target.blur(); 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 { ...@@ -88,15 +102,22 @@ export default {
class="multi-file-commit-panel ide-right-sidebar" class="multi-file-commit-panel ide-right-sidebar"
> >
<resizable-panel <resizable-panel
v-if="rightPane" v-show="isOpen"
:collapsible="false" :collapsible="false"
:initial-width="350" :initial-width="350"
:min-size="350" :min-size="350"
:class="`ide-right-sidebar-${rightPane}`" :class="`ide-right-sidebar-${currentView}`"
side="right" side="right"
class="multi-file-commit-panel-inner" 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> </resizable-panel>
<nav class="ide-activity-bar"> <nav class="ide-activity-bar">
<ul class="list-unstyled"> <ul class="list-unstyled">
...@@ -109,13 +130,13 @@ export default { ...@@ -109,13 +130,13 @@ export default {
:title="tab.title" :title="tab.title"
:aria-label="tab.title" :aria-label="tab.title"
:class="{ :class="{
active: tab.isActive active: isActiveTab(tab) && isOpen
}" }"
data-container="body" data-container="body"
data-placement="left" data-placement="left"
class="ide-sidebar-link is-right" class="ide-sidebar-link is-right"
type="button" type="button"
@click="clickTab($event, tab.view)" @click="clickTab($event, tab)"
> >
<icon <icon
:size="16" :size="16"
......
...@@ -22,12 +22,14 @@ export default { ...@@ -22,12 +22,14 @@ export default {
}, },
}, },
computed: { computed: {
...mapState('rightPane', {
rightPaneIsOpen: 'isOpen',
}),
...mapState([ ...mapState([
'rightPanelCollapsed', 'rightPanelCollapsed',
'viewer', 'viewer',
'panelResizing', 'panelResizing',
'currentActivityView', 'currentActivityView',
'rightPane',
]), ]),
...mapGetters([ ...mapGetters([
'currentMergeRequest', 'currentMergeRequest',
...@@ -99,7 +101,7 @@ export default { ...@@ -99,7 +101,7 @@ export default {
this.editor.updateDimensions(); this.editor.updateDimensions();
} }
}, },
rightPane() { rightPaneIsOpen() {
this.editor.updateDimensions(); this.editor.updateDimensions();
}, },
}, },
......
...@@ -29,10 +29,10 @@ export const diffModes = { ...@@ -29,10 +29,10 @@ export const diffModes = {
}; };
export const rightSidebarViews = { export const rightSidebarViews = {
pipelines: 'pipelines-list', pipelines: { name: 'pipelines-list', keepAlive: true },
jobsDetail: 'jobs-detail', jobsDetail: { name: 'jobs-detail', keepAlive: false },
mergeRequestInfo: 'merge-request-info', mergeRequestInfo: { name: 'merge-request-info', keepAlive: true },
clientSidePreview: 'clientside', clientSidePreview: { name: 'clientside', keepAlive: false },
}; };
export const stageKeys = { export const stageKeys = {
......
...@@ -184,10 +184,6 @@ export const burstUnusedSeal = ({ state, commit }) => { ...@@ -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 setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
export const setErrorMessage = ({ commit }, errorMessage) => export const setErrorMessage = ({ commit }, errorMessage) =>
......
...@@ -9,6 +9,7 @@ import pipelines from './modules/pipelines'; ...@@ -9,6 +9,7 @@ import pipelines from './modules/pipelines';
import mergeRequests from './modules/merge_requests'; import mergeRequests from './modules/merge_requests';
import branches from './modules/branches'; import branches from './modules/branches';
import fileTemplates from './modules/file_templates'; import fileTemplates from './modules/file_templates';
import paneModule from './modules/pane';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -24,6 +25,7 @@ export const createStore = () => ...@@ -24,6 +25,7 @@ export const createStore = () =>
mergeRequests, mergeRequests,
branches, branches,
fileTemplates: fileTemplates(), 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) => ...@@ -113,7 +113,7 @@ export const toggleStageCollapsed = ({ commit }, stageId) =>
export const setDetailJob = ({ commit, dispatch }, job) => { export const setDetailJob = ({ commit, dispatch }, job) => {
commit(types.SET_DETAIL_JOB, job); commit(types.SET_DETAIL_JOB, job);
dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, { dispatch('rightPane/open', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, {
root: true, root: true,
}); });
}; };
......
...@@ -68,8 +68,6 @@ export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; ...@@ -68,8 +68,6 @@ export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; export const CLEAR_PROJECTS = 'CLEAR_PROJECTS';
export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
......
...@@ -166,11 +166,6 @@ export default { ...@@ -166,11 +166,6 @@ export default {
unusedSeal: false, unusedSeal: false,
}); });
}, },
[types.SET_RIGHT_PANE](state, view) {
Object.assign(state, {
rightPane: state.rightPane === view ? null : view,
});
},
[types.SET_LINKS](state, links) { [types.SET_LINKS](state, links) {
Object.assign(state, { links }); Object.assign(state, { links });
}, },
......
...@@ -23,7 +23,6 @@ export default () => ({ ...@@ -23,7 +23,6 @@ export default () => ({
currentActivityView: activityBarViews.edit, currentActivityView: activityBarViews.edit,
unusedSeal: true, unusedSeal: true,
fileFindVisible: false, fileFindVisible: false,
rightPane: null,
links: {}, links: {},
errorMessage: null, errorMessage: null,
entryModal: { entryModal: {
......
...@@ -324,6 +324,16 @@ img.emoji { ...@@ -324,6 +324,16 @@ img.emoji {
word-wrap: break-word; word-wrap: break-word;
} }
.checkbox-icon-inline-wrapper {
.checkbox {
display: inline;
label {
display: inline;
}
}
}
/** COMMON CLASSES **/ /** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; } .prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; } .prepend-top-2 { margin-top: 2px; }
......
...@@ -673,6 +673,14 @@ class Repository ...@@ -673,6 +673,14 @@ class Repository
end end
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) def last_commit_for_path(sha, path)
commit = raw_repository.last_commit_for_path(sha, path) commit = raw_repository.last_commit_for_path(sha, path)
::Commit.new(commit, @project) if commit ::Commit.new(commit, @project) if commit
......
...@@ -57,7 +57,6 @@ module Users ...@@ -57,7 +57,6 @@ module Users
:force_random_password, :force_random_password,
:hide_no_password, :hide_no_password,
:hide_no_ssh_key, :hide_no_ssh_key,
:key_id,
:linkedin, :linkedin,
:name, :name,
:password, :password,
...@@ -71,7 +70,10 @@ module Users ...@@ -71,7 +70,10 @@ module Users
:twitter, :twitter,
:username, :username,
:website_url, :website_url,
:private_profile :private_profile,
:organization,
:location,
:public_email
] ]
end end
......
...@@ -109,10 +109,11 @@ ...@@ -109,10 +109,11 @@
= f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters.") = f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters.")
%hr %hr
%h5= ("Private profile") %h5= ("Private profile")
- private_profile_label = capture do .checkbox-icon-inline-wrapper
= s_("Profiles|Don't display activity-related personal information on your profiles") - 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') = 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") %h5= s_("Profiles|Private contributions")
= f.check_box :include_private_contributions, label: 'Include private contributions on my profile' = f.check_box :include_private_contributions, label: 'Include private contributions on my profile'
.help-block .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: ...@@ -291,6 +291,7 @@ Parameters:
- `provider` (optional) - External provider name - `provider` (optional) - External provider name
- `bio` (optional) - User's biography - `bio` (optional) - User's biography
- `location` (optional) - User's location - `location` (optional) - User's location
- `public_email` (optional) - The public email of the user
- `admin` (optional) - User is admin - true or false (default) - `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false - `can_create_group` (optional) - User can create groups - true or false
- `skip_confirmation` (optional) - Skip confirmation - true or false (default) - `skip_confirmation` (optional) - Skip confirmation - true or false (default)
...@@ -309,27 +310,34 @@ PUT /users/:id ...@@ -309,27 +310,34 @@ PUT /users/:id
Parameters: Parameters:
- `email` - Email - `email` - Email
- `username` - Username - `username` - Username
- `name` - Name - `name` - Name
- `password` - Password - `password` - Password
- `skype` - Skype ID - `skype` - Skype ID
- `linkedin` - LinkedIn - `linkedin` - LinkedIn
- `twitter` - Twitter account - `twitter` - Twitter account
- `website_url` - Website URL - `website_url` - Website URL
- `organization` - Organization name - `organization` - Organization name
- `projects_limit` - Limit projects each user can create - `projects_limit` - Limit projects each user can create
- `extern_uid` - External UID - `extern_uid` - External UID
- `provider` - External provider name - `provider` - External provider name
- `bio` - User's biography - `bio` - User's biography
- `location` (optional) - User's location - `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default) - `public_email` (optional) - The public email of the user
- `can_create_group` (optional) - User can create groups - true or false - `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) - `skip_reconfirmation` (optional) - Skip reconfirmation - true or false (default)
<<<<<<< HEAD
- `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 - `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user
- `avatar` (optional) - Image file for user's avatar - `avatar` (optional) - Image file for user's avatar
- `private_profile` (optional) - User's profile is private - true or false - `private_profile` (optional) - User's profile is private - true or false
=======
- `external` (optional) - Flags the user as external - true or false(default)
- `avatar` (optional) - Image file for user's avatar
- `private_profile` (optional) - User's profile is private - true or false
>>>>>>> upstream/master
On password update, user will be forced to change it upon next login. 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, 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: ...@@ -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 - The Runner needs to have [`[session_server]` configured
properly][session-server] 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) [enabled](../../administration/integration/terminal.md#enabling-and-disabling-terminal-support)
## Debugging a running job ## Debugging a running job
......
...@@ -42,12 +42,12 @@ module API ...@@ -42,12 +42,12 @@ module API
optional :provider, type: String, desc: 'The external provider' optional :provider, type: String, desc: 'The external provider'
optional :bio, type: String, desc: 'The biography of the user' optional :bio, type: String, desc: 'The biography of the user'
optional :location, type: String, desc: 'The location 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 :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 :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 :external, type: Boolean, desc: 'Flag indicating the user is an external user'
optional :avatar, type: File, desc: 'Avatar image for 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 :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 all_or_none_of :extern_uid, :provider
# EE # EE
......
...@@ -60,7 +60,11 @@ module Banzai ...@@ -60,7 +60,11 @@ module Banzai
path_parts.unshift(relative_url_root, project.full_path) path_parts.unshift(relative_url_root, project.full_path)
end 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 = html_attr.value =
if context[:only_path] if context[:only_path]
......
...@@ -953,6 +953,12 @@ module Gitlab ...@@ -953,6 +953,12 @@ module Gitlab
end end
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) def last_commit_for_path(sha, path)
wrapped_gitaly_errors do wrapped_gitaly_errors do
gitaly_commit_client.last_commit_for_path(sha, path) gitaly_commit_client.last_commit_for_path(sha, path)
......
...@@ -148,6 +148,24 @@ module Gitlab ...@@ -148,6 +148,24 @@ module Gitlab
GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count
end 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) def last_commit_for_path(revision, path)
request = Gitaly::LastCommitForPathRequest.new( request = Gitaly::LastCommitForPathRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
......
...@@ -75,25 +75,29 @@ module Gitlab ...@@ -75,25 +75,29 @@ module Gitlab
end end
def fill_last_commits!(entries) def fill_last_commits!(entries)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433 # Ensure the path is in "path/" format
Gitlab::GitalyClient.allow_n_plus_1_calls do ensured_path =
entries.each do |entry| if path
raw_commit = repository.last_commit_for_path(commit.id, entry_path(entry)) File.join(*[path, ""])
end
commits_hsh = repository.list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit)
if raw_commit entries.each do |entry|
commit = resolve_commit(raw_commit) path_key = entry_path(entry)
commit = cache_commit(commits_hsh[path_key])
entry[:commit] = commit if commit
entry[:commit_path] = commit_path(commit) entry[:commit] = commit
end entry[:commit_path] = commit_path(commit)
end end
end end
end end
def resolve_commit(raw_commit) def cache_commit(commit)
return nil unless raw_commit.present? return nil unless commit.present?
resolved_commits[raw_commit.id] ||= ::Commit.new(raw_commit, project) resolved_commits[commit.id] ||= commit
end end
def commit_path(commit) def commit_path(commit)
......
...@@ -97,13 +97,13 @@ end ...@@ -97,13 +97,13 @@ end
automated_cleanup = AutomatedCleanup.new automated_cleanup = AutomatedCleanup.new
timed('Review apps cleanup') do 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 end
puts puts
timed('Helm releases cleanup') do timed('Helm releases cleanup') do
automated_cleanup.perform_helm_releases_cleanup!(days: 7) automated_cleanup.perform_helm_releases_cleanup!(days: 3)
end end
exit(0) exit(0)
import Vue from 'vue'; import Vue from 'vue';
import store from '~/ide/stores'; import store from '~/ide/stores';
import ideStatusBar from '~/ide/components/ide_status_bar.vue'; import ideStatusBar from '~/ide/components/ide_status_bar.vue';
import { rightSidebarViews } from '~/ide/constants';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers'; import { resetStore } from '../helpers';
import { projectData } from '../mock_data'; import { projectData } from '../mock_data';
...@@ -64,7 +65,7 @@ describe('ideStatusBar', () => { ...@@ -64,7 +65,7 @@ describe('ideStatusBar', () => {
describe('pipeline status', () => { describe('pipeline status', () => {
it('opens right sidebar on clicking icon', done => { it('opens right sidebar on clicking icon', done => {
spyOn(vm, 'setRightPane'); spyOn(vm, 'openRightPane');
Vue.set(vm.$store.state.pipelines, 'latestPipeline', { Vue.set(vm.$store.state.pipelines, 'latestPipeline', {
details: { details: {
status: { status: {
...@@ -80,7 +81,7 @@ describe('ideStatusBar', () => { ...@@ -80,7 +81,7 @@ describe('ideStatusBar', () => {
.then(() => { .then(() => {
vm.$el.querySelector('.ide-status-pipeline button').click(); vm.$el.querySelector('.ide-status-pipeline button').click();
expect(vm.setRightPane).toHaveBeenCalledWith('pipelines-list'); expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
......
...@@ -25,7 +25,8 @@ describe('IDE right pane', () => { ...@@ -25,7 +25,8 @@ describe('IDE right pane', () => {
describe('active', () => { describe('active', () => {
it('renders merge request button as active', done => { 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.currentMergeRequestId = '123';
vm.$store.state.currentProjectId = 'gitlab-ce'; vm.$store.state.currentProjectId = 'gitlab-ce';
vm.$store.state.currentMergeRequestId = 1; vm.$store.state.currentMergeRequestId = 1;
...@@ -41,20 +42,21 @@ describe('IDE right pane', () => { ...@@ -41,20 +42,21 @@ describe('IDE right pane', () => {
}, },
}; };
vm.$nextTick(() => { vm.$nextTick()
expect(vm.$el.querySelector('.ide-sidebar-link.active')).not.toBe(null); .then(() => {
expect( expect(vm.$el.querySelector('.ide-sidebar-link.active')).not.toBe(null);
vm.$el.querySelector('.ide-sidebar-link.active').getAttribute('data-original-title'), expect(
).toBe('Merge Request'); vm.$el.querySelector('.ide-sidebar-link.active').getAttribute('data-original-title'),
).toBe('Merge Request');
done(); })
}); .then(done)
.catch(done.fail);
}); });
}); });
describe('click', () => { describe('click', () => {
beforeEach(() => { beforeEach(() => {
spyOn(vm, 'setRightPane'); spyOn(vm, 'open');
}); });
it('sets view to merge request', done => { it('sets view to merge request', done => {
...@@ -63,7 +65,7 @@ describe('IDE right pane', () => { ...@@ -63,7 +65,7 @@ describe('IDE right pane', () => {
vm.$nextTick(() => { vm.$nextTick(() => {
vm.$el.querySelector('.ide-sidebar-link').click(); vm.$el.querySelector('.ide-sidebar-link').click();
expect(vm.setRightPane).toHaveBeenCalledWith(rightSidebarViews.mergeRequestInfo); expect(vm.open).toHaveBeenCalledWith(rightSidebarViews.mergeRequestInfo);
done(); done();
}); });
......
...@@ -319,8 +319,8 @@ describe('RepoEditor', () => { ...@@ -319,8 +319,8 @@ describe('RepoEditor', () => {
}); });
}); });
it('calls updateDimensions when rightPane is updated', done => { it('calls updateDimensions when rightPane is opened', done => {
vm.$store.state.rightPane = 'testing'; vm.$store.state.rightPane.isOpen = true;
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.editor.updateDimensions).toHaveBeenCalled(); expect(vm.editor.updateDimensions).toHaveBeenCalled();
......
...@@ -6,6 +6,7 @@ import mergeRequestsState from '~/ide/stores/modules/merge_requests/state'; ...@@ -6,6 +6,7 @@ import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
import pipelinesState from '~/ide/stores/modules/pipelines/state'; import pipelinesState from '~/ide/stores/modules/pipelines/state';
import branchesState from '~/ide/stores/modules/branches/state'; import branchesState from '~/ide/stores/modules/branches/state';
import fileTemplatesState from '~/ide/stores/modules/file_templates/state'; import fileTemplatesState from '~/ide/stores/modules/file_templates/state';
import paneState from '~/ide/stores/modules/pane/state';
export const resetStore = store => { export const resetStore = store => {
const newState = { const newState = {
...@@ -15,6 +16,7 @@ export const resetStore = store => { ...@@ -15,6 +16,7 @@ export const resetStore = store => {
pipelines: pipelinesState(), pipelines: pipelinesState(),
branches: branchesState(), branches: branchesState(),
fileTemplates: fileTemplatesState(), fileTemplates: fileTemplatesState(),
rightPane: paneState(),
}; };
store.replaceState(newState); 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', () => { ...@@ -315,29 +315,29 @@ describe('IDE pipelines actions', () => {
'job', 'job',
mockedState, mockedState,
[{ type: types.SET_DETAIL_JOB, payload: 'job' }], [{ type: types.SET_DETAIL_JOB, payload: 'job' }],
[{ type: 'setRightPane', payload: 'jobs-detail' }], [{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }],
done, done,
); );
}); });
it('dispatches setRightPane as pipeline when job is null', done => { it('dispatches rightPane/open as pipeline when job is null', done => {
testAction( testAction(
setDetailJob, setDetailJob,
null, null,
mockedState, mockedState,
[{ type: types.SET_DETAIL_JOB, payload: null }], [{ type: types.SET_DETAIL_JOB, payload: null }],
[{ type: 'setRightPane', payload: rightSidebarViews.pipelines }], [{ type: 'rightPane/open', payload: rightSidebarViews.pipelines }],
done, done,
); );
}); });
it('dispatches setRightPane as job', done => { it('dispatches rightPane/open as job', done => {
testAction( testAction(
setDetailJob, setDetailJob,
'job', 'job',
mockedState, mockedState,
[{ type: types.SET_DETAIL_JOB, payload: 'job' }], [{ type: types.SET_DETAIL_JOB, payload: 'job' }],
[{ type: 'setRightPane', payload: rightSidebarViews.jobsDetail }], [{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }],
done, done,
); );
}); });
......
...@@ -83,6 +83,11 @@ describe Banzai::Filter::RelativeLinkFilter do ...@@ -83,6 +83,11 @@ describe Banzai::Filter::RelativeLinkFilter do
expect { filter(act) }.not_to raise_error expect { filter(act) }.not_to raise_error
end 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 it 'ignores ref if commit is passed' do
doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') ) doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') )
expect(doc.at_css('a')['href']) expect(doc.at_css('a')['href'])
......
...@@ -188,6 +188,57 @@ describe Repository do ...@@ -188,6 +188,57 @@ describe Repository do
end end
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 describe '#last_commit_for_path' do
shared_examples 'getting last commit for path' do shared_examples 'getting last commit for path' do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
......
...@@ -14,6 +14,49 @@ describe Users::BuildService do ...@@ -14,6 +14,49 @@ describe Users::BuildService do
expect(service.execute).to be_valid expect(service.execute).to be_valid
end 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 context 'with "user_default_external" application setting' do
using RSpec::Parameterized::TableSyntax 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