Commit 14b8ac3e authored by Phil Hughes's avatar Phil Hughes

Merge branch '44846-improve-web-ide-left-panel-and-modes' into ide-sidebar-merge-request

parents aeb4b750 805302c0
...@@ -9,6 +9,10 @@ terms. ...@@ -9,6 +9,10 @@ terms.
[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md) [DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
All Documentation content that resides under the [doc/ directory](/doc) of this
repository is licensed under Creative Commons:
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
_This notice should stay as the first item in the CONTRIBUTING.md file._ _This notice should stay as the first item in the CONTRIBUTING.md file._
--- ---
......
...@@ -33,7 +33,7 @@ gem 'grape-route-helpers', '~> 2.1.0' ...@@ -33,7 +33,7 @@ gem 'grape-route-helpers', '~> 2.1.0'
gem 'faraday', '~> 0.12' gem 'faraday', '~> 0.12'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.4'
gem 'doorkeeper', '~> 4.3' gem 'doorkeeper', '~> 4.3'
gem 'doorkeeper-openid_connect', '~> 1.3' gem 'doorkeeper-openid_connect', '~> 1.3'
gem 'omniauth', '~> 1.8' gem 'omniauth', '~> 1.8'
...@@ -41,7 +41,7 @@ gem 'omniauth-auth0', '~> 2.0.0' ...@@ -41,7 +41,7 @@ gem 'omniauth-auth0', '~> 2.0.0'
gem 'omniauth-azure-oauth2', '~> 0.0.9' gem 'omniauth-azure-oauth2', '~> 0.0.9'
gem 'omniauth-cas3', '~> 1.1.4' gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-github', '~> 1.3'
gem 'omniauth-gitlab', '~> 1.0.2' gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.3' gem 'omniauth-google-oauth2', '~> 0.5.3'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
...@@ -90,7 +90,7 @@ gem 'github-linguist', '~> 5.3.3', require: 'linguist' ...@@ -90,7 +90,7 @@ gem 'github-linguist', '~> 5.3.3', require: 'linguist'
# API # API
gem 'grape', '~> 1.0' gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.6.0' gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors' gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# Disable strong_params so that Mash does not respond to :permitted? # Disable strong_params so that Mash does not respond to :permitted?
......
...@@ -143,7 +143,7 @@ GEM ...@@ -143,7 +143,7 @@ GEM
connection_pool (2.2.1) connection_pool (2.2.1)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.3) crass (1.0.4)
creole (0.5.0) creole (0.5.0)
css_parser (1.5.0) css_parser (1.5.0)
addressable addressable
...@@ -162,10 +162,10 @@ GEM ...@@ -162,10 +162,10 @@ GEM
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.0) device_detector (1.0.0)
devise (4.2.0) devise (4.4.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.1) railties (>= 4.1.0, < 6.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (3.0.0) devise-two-factor (3.0.0)
...@@ -366,8 +366,8 @@ GEM ...@@ -366,8 +366,8 @@ GEM
rack (>= 1.3.0) rack (>= 1.3.0)
rack-accept rack-accept
virtus (>= 1.0.0) virtus (>= 1.0.0)
grape-entity (0.6.0) grape-entity (0.7.1)
activesupport activesupport (>= 4.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grape-route-helpers (2.1.0) grape-route-helpers (2.1.0)
activesupport activesupport
...@@ -546,9 +546,9 @@ GEM ...@@ -546,9 +546,9 @@ GEM
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-facebook (4.0.0) omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2) omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2) omniauth-github (1.3.0)
omniauth (~> 1.0) omniauth (~> 1.5)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.2) omniauth-gitlab (1.0.2)
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0) omniauth-oauth2 (~> 1.0)
...@@ -646,7 +646,7 @@ GEM ...@@ -646,7 +646,7 @@ GEM
pry (>= 0.9.10) pry (>= 0.9.10)
public_suffix (3.0.2) public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (1.6.9) rack (1.6.10)
rack-accept (0.4.5) rack-accept (0.4.5)
rack (>= 0.4) rack (>= 0.4)
rack-attack (4.4.1) rack-attack (4.4.1)
...@@ -694,7 +694,7 @@ GEM ...@@ -694,7 +694,7 @@ GEM
rainbow (2.2.2) rainbow (2.2.2)
rake rake
raindrops (0.18.0) raindrops (0.18.0)
rake (12.3.0) rake (12.3.1)
rb-fsevent (0.10.2) rb-fsevent (0.10.2)
rb-inotify (0.9.10) rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2) ffi (>= 0.5.0, < 2)
...@@ -735,8 +735,9 @@ GEM ...@@ -735,8 +735,9 @@ GEM
declarative-option (< 0.2.0) declarative-option (< 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
request_store (1.3.1) request_store (1.3.1)
responders (2.3.0) responders (2.4.0)
railties (>= 4.2.0, < 5.1) actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
rest-client (2.0.2) rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
...@@ -966,7 +967,7 @@ GEM ...@@ -966,7 +967,7 @@ GEM
descendants_tracker (~> 0.0, >= 0.0.3) descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9) equalizer (~> 0.0, >= 0.0.9)
vmstat (2.3.0) vmstat (2.3.0)
warden (1.2.6) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
webmock (2.3.2) webmock (2.3.2)
addressable (>= 2.3.6) addressable (>= 2.3.6)
...@@ -1028,7 +1029,7 @@ DEPENDENCIES ...@@ -1028,7 +1029,7 @@ DEPENDENCIES
deckar01-task_list (= 2.0.0) deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0) default_value_for (~> 3.0.0)
device_detector device_detector
devise (~> 4.2) devise (~> 4.4)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
doorkeeper (~> 4.3) doorkeeper (~> 4.3)
...@@ -1072,7 +1073,7 @@ DEPENDENCIES ...@@ -1072,7 +1073,7 @@ DEPENDENCIES
google-protobuf (= 3.5.1) google-protobuf (= 3.5.1)
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.11.0) grpc (~> 1.11.0)
...@@ -1113,7 +1114,7 @@ DEPENDENCIES ...@@ -1113,7 +1114,7 @@ DEPENDENCIES
omniauth-azure-oauth2 (~> 0.0.9) omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4) omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0) omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1) omniauth-github (~> 1.3)
omniauth-gitlab (~> 1.0.2) omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3) omniauth-google-oauth2 (~> 0.5.3)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
......
...@@ -4,4 +4,9 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of ...@@ -4,4 +4,9 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
---
All Documentation content that resides under the doc/ directory of this
repository is licensed under Creative Commons: CC BY-SA 4.0.
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
<div class="environments-container"> <div class="environments-container">
<loading-icon <loading-icon
class="prepend-top-default"
label="Loading environments" label="Loading environments"
v-if="isLoading" v-if="isLoading"
size="3" size="3"
......
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
return `${this.changedIcon}-solid`; return `${this.changedIcon}-solid`;
}, },
changedIconClass() { changedIconClass() {
return `multi-${this.changedIcon} prepend-left-5 pull-left`; return `multi-${this.changedIcon} pull-left`;
}, },
tooltipTitle() { tooltipTitle() {
if (!this.showTooltip) return undefined; if (!this.showTooltip) return undefined;
...@@ -79,13 +79,7 @@ export default { ...@@ -79,13 +79,7 @@ export default {
class="ide-file-changed-icon" class="ide-file-changed-icon"
> >
<icon <icon
v-if="file.staged && showStagedIcon" v-if="file.changed || file.tempFile || file.staged"
:name="stagedIcon"
:size="12"
:css-classes="changedIconClass"
/>
<icon
v-if="file.changed || file.tempFile || (file.staged && !showStagedIcon)"
:name="changedIcon" :name="changedIcon"
:size="12" :size="12"
:css-classes="changedIconClass" :css-classes="changedIconClass"
......
...@@ -37,7 +37,7 @@ export default { ...@@ -37,7 +37,7 @@ export default {
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`; return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
}, },
iconClass() { iconClass() {
return `multi-file-${this.file.tempFile ? 'additions' : 'modified'} append-right-8`; return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
}, },
}, },
methods: { methods: {
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import { n__, __, sprintf } from '~/locale';
import fileIcon from '~/vue_shared/components/file_icon.vue'; import tooltip from '~/vue_shared/directives/tooltip';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import router from '../ide_router'; import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue'; import NewDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue'; import FileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue'; import ChangedFileIcon from './changed_file_icon.vue';
import mrFileIcon from './mr_file_icon.vue'; import MrFileIcon from './mr_file_icon.vue';
export default { export default {
name: 'RepoFile', name: 'RepoFile',
directives: {
tooltip,
},
components: { components: {
skeletonLoadingContainer, SkeletonLoadingContainer,
newDropdown, NewDropdown,
fileStatusIcon, FileStatusIcon,
fileIcon, FileIcon,
changedFileIcon, ChangedFileIcon,
mrFileIcon, MrFileIcon,
Icon,
}, },
props: { props: {
file: { file: {
...@@ -34,6 +41,34 @@ export default { ...@@ -34,6 +41,34 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters([
'getChangesInFolder',
'getUnstagedFilesCountForPath',
'getStagedFilesCountForPath',
]),
folderUnstagedCount() {
return this.getUnstagedFilesCountForPath(this.file.path);
},
folderStagedCount() {
return this.getStagedFilesCountForPath(this.file.path);
},
changesCount() {
return this.getChangesInFolder(this.file.path);
},
folderChangesTooltip() {
if (this.changesCount === 0) return undefined;
if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
} else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
}
return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
unstaged: this.folderUnstagedCount,
staged: this.folderStagedCount,
});
},
isTree() { isTree() {
return this.file.type === 'tree'; return this.file.type === 'tree';
}, },
...@@ -53,10 +88,19 @@ export default { ...@@ -53,10 +88,19 @@ export default {
'is-open': this.file.opened, 'is-open': this.file.opened,
}; };
}, },
showTreeChangesCount() {
return this.isTree && this.changesCount > 0 && !this.file.opened;
},
showChangedFileIcon() {
return this.file.changed || this.file.tempFile || this.file.staged;
},
}, },
updated() { updated() {
if (this.file.type === 'blob' && this.file.active) { if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView(); this.$el.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
} }
}, },
methods: { methods: {
...@@ -104,8 +148,23 @@ export default { ...@@ -104,8 +148,23 @@ export default {
<mr-file-icon <mr-file-icon
v-if="file.mrChange" v-if="file.mrChange"
/> />
<span
v-if="showTreeChangesCount"
class="ide-tree-changes"
>
{{ changesCount }}
<icon
v-tooltip
:title="folderChangesTooltip"
data-container="body"
data-placement="right"
name="file-modified"
:size="12"
css-classes="prepend-left-5 multi-file-modified"
/>
</span>
<changed-file-icon <changed-file-icon
v-if="file.changed || file.tempFile || file.staged" v-else-if="showChangedFileIcon"
:file="file" :file="file"
:show-tooltip="true" :show-tooltip="true"
:show-staged-icon="true" :show-staged-icon="true"
......
import { getChangesCountForFiles, filePathMatches } from './utils';
import { activityBarViews } from '../constants'; import { activityBarViews } from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state => state.openFiles.find(file => file.active) || null;
...@@ -52,11 +53,27 @@ export const allBlobs = state => ...@@ -52,11 +53,27 @@ export const allBlobs = state =>
}, []) }, [])
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path); export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
export const isEditModeActive = state => state.currentActivityView === activityBarViews.edit; export const isEditModeActive = state => state.currentActivityView === activityBarViews.edit;
export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit; export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review; export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
export const getChangesInFolder = state => path => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
const stagedFilesCount = state.stagedFiles.filter(
f => filePathMatches(f, path) && !getChangedFile(state)(f.path),
).length;
return changedFilesCount + stagedFilesCount;
};
export const getUnstagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.changedFiles, path);
export const getStagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.stagedFiles, path);
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -33,7 +33,6 @@ export const dataStructure = () => ({ ...@@ -33,7 +33,6 @@ export const dataStructure = () => ({
raw: '', raw: '',
content: '', content: '',
parentTreeUrl: '', parentTreeUrl: '',
parentPath: '',
renderError: false, renderError: false,
base64: false, base64: false,
editorRow: 1, editorRow: 1,
...@@ -43,6 +42,7 @@ export const dataStructure = () => ({ ...@@ -43,6 +42,7 @@ export const dataStructure = () => ({
viewMode: 'edit', viewMode: 'edit',
previewMode: null, previewMode: null,
size: 0, size: 0,
parentPath: null,
lastOpenedAt: 0, lastOpenedAt: 0,
}); });
...@@ -83,7 +83,6 @@ export const decorateData = entity => { ...@@ -83,7 +83,6 @@ export const decorateData = entity => {
opened, opened,
active, active,
parentTreeUrl, parentTreeUrl,
parentPath,
changed, changed,
renderError, renderError,
content, content,
...@@ -91,6 +90,7 @@ export const decorateData = entity => { ...@@ -91,6 +90,7 @@ export const decorateData = entity => {
previewMode, previewMode,
file_lock, file_lock,
html, html,
parentPath,
}; };
}; };
...@@ -137,3 +137,9 @@ export const sortTree = sortedTree => ...@@ -137,3 +137,9 @@ export const sortTree = sortedTree =>
}), }),
) )
.sort(sortTreesByTypeAndName); .sort(sortTreesByTypeAndName);
export const filePathMatches = (f, path) =>
f.path.replace(new RegExp(`${f.name}$`), '').indexOf(`${path}/`) === 0;
export const getChangesCountForFiles = (files, path) =>
files.filter(f => filePathMatches(f, path)).length;
...@@ -70,6 +70,9 @@ ...@@ -70,6 +70,9 @@
toggleMoreParticipants() { toggleMoreParticipants() {
this.isShowingMoreParticipants = !this.isShowingMoreParticipants; this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
}, },
onClickCollapsedIcon() {
this.$emit('toggleSidebar');
},
}, },
}; };
</script> </script>
...@@ -82,6 +85,7 @@ ...@@ -82,6 +85,7 @@
data-container="body" data-container="body"
data-placement="left" data-placement="left"
:title="participantLabel" :title="participantLabel"
@click="onClickCollapsedIcon"
> >
<i <i
class="fa fa-users" class="fa fa-users"
......
<script> <script>
import Store from '../../stores/sidebar_store'; import Store from '../../stores/sidebar_store';
import eventHub from '../../event_hub';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue'; import subscriptions from './subscriptions.vue';
...@@ -20,12 +19,6 @@ export default { ...@@ -20,12 +19,6 @@ export default {
store: new Store(), store: new Store(),
}; };
}, },
created() {
eventHub.$on('toggleSubscription', this.onToggleSubscription);
},
beforeDestroy() {
eventHub.$off('toggleSubscription', this.onToggleSubscription);
},
methods: { methods: {
onToggleSubscription() { onToggleSubscription() {
this.mediator.toggleSubscription() this.mediator.toggleSubscription()
...@@ -42,6 +35,7 @@ export default { ...@@ -42,6 +35,7 @@ export default {
<subscriptions <subscriptions
:loading="store.isFetching.subscriptions" :loading="store.isFetching.subscriptions"
:subscribed="store.subscribed" :subscribed="store.subscribed"
@toggleSubscription="onToggleSubscription"
/> />
</div> </div>
</template> </template>
...@@ -47,8 +47,25 @@ ...@@ -47,8 +47,25 @@
}, },
}, },
methods: { methods: {
/**
* We need to emit this event on both component & eventHub
* for 2 dependencies;
*
* 1. eventHub: This component is used in Issue Boards sidebar
* where component template is part of HAML
* and event listeners are tied to app's eventHub.
* 2. Component: This compone is also used in Epics in EE
* where listeners are tied to component event.
*/
toggleSubscription() { toggleSubscription() {
// App's eventHub event emission.
eventHub.$emit('toggleSubscription', this.id); eventHub.$emit('toggleSubscription', this.id);
// Component event emission.
this.$emit('toggleSubscription', this.id);
},
onClickCollapsedIcon() {
this.$emit('toggleSidebar');
}, },
}, },
}; };
...@@ -56,7 +73,10 @@ ...@@ -56,7 +73,10 @@
<template> <template>
<div> <div>
<div class="sidebar-collapsed-icon"> <div
class="sidebar-collapsed-icon"
@click="onClickCollapsedIcon"
>
<span <span
v-tooltip v-tooltip
:title="notificationTooltip" :title="notificationTooltip"
......
<script>
export default { export default {
name: 'time-tracking-spent-only-pane', name: 'TimeTrackingSpentOnlyPane',
props: { props: {
timeSpentHumanReadable: { timeSpentHumanReadable: {
type: String, type: String,
required: true, required: true,
}, },
}, },
template: `
<div class="time-tracking-spend-only-pane">
<span class="bold">Spent:</span>
{{ timeSpentHumanReadable }}
</div>
`,
}; };
</script>
<template>
<div class="time-tracking-spend-only-pane">
<span class="bold">Spent:</span>
{{ timeSpentHumanReadable }}
</div>
</template>
<script> <script>
import TimeTrackingHelpState from './help_state.vue'; import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue'; import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane'; import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
import TimeTrackingNoTrackingPane from './no_tracking_pane.vue'; import TimeTrackingNoTrackingPane from './no_tracking_pane.vue';
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue'; import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue'; import TimeTrackingComparisonPane from './comparison_pane.vue';
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
components: { components: {
TimeTrackingCollapsedState, TimeTrackingCollapsedState,
TimeTrackingEstimateOnlyPane, TimeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane, TimeTrackingSpentOnlyPane,
TimeTrackingNoTrackingPane, TimeTrackingNoTrackingPane,
TimeTrackingComparisonPane, TimeTrackingComparisonPane,
TimeTrackingHelpState, TimeTrackingHelpState,
......
...@@ -317,6 +317,7 @@ ...@@ -317,6 +317,7 @@
a { a {
color: $gl-text-color; color: $gl-text-color;
word-wrap: break-word; word-wrap: break-word;
word-break: break-word;
margin-right: 2px; margin-right: 2px;
} }
} }
...@@ -462,6 +463,7 @@ ...@@ -462,6 +463,7 @@
.issuable-header-text { .issuable-header-text {
padding-right: 35px; padding-right: 35px;
word-break: break-word;
> strong { > strong {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
......
...@@ -539,14 +539,14 @@ ...@@ -539,14 +539,14 @@
} }
} }
.multi-file-additions, .multi-file-addition,
.multi-file-additions-solid { .multi-file-addition-solid {
fill: $green-500; color: $green-500;
} }
.multi-file-modified, .multi-file-modified,
.multi-file-modified-solid { .multi-file-modified-solid {
fill: $orange-500; color: $orange-500;
} }
.multi-file-commit-list-collapsed { .multi-file-commit-list-collapsed {
...@@ -986,6 +986,12 @@ ...@@ -986,6 +986,12 @@
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
.ide-tree-changes {
display: flex;
align-items: center;
font-size: 12px;
}
.ide-new-modal-label { .ide-new-modal-label {
line-height: 34px; line-height: 34px;
} }
......
...@@ -52,6 +52,12 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -52,6 +52,12 @@ class Projects::RunnersController < Projects::ApplicationController
redirect_to project_settings_ci_cd_path(@project) redirect_to project_settings_ci_cd_path(@project)
end end
def toggle_group_runners
project.toggle_ci_cd_settings!(:group_runners_enabled)
redirect_to project_settings_ci_cd_path(@project)
end
protected protected
def set_runner def set_runner
......
...@@ -67,10 +67,18 @@ module Projects ...@@ -67,10 +67,18 @@ module Projects
def define_runners_variables def define_runners_variables
@project_runners = @project.runners.ordered @project_runners = @project.runners.ordered
@assignable_runners = current_user.ci_authorized_runners
.assignable_for(project).ordered.page(params[:page]).per(20) @assignable_runners = current_user
.ci_authorized_runners
.assignable_for(project)
.ordered
.page(params[:page]).per(20)
@shared_runners = ::Ci::Runner.shared.active @shared_runners = ::Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all) @shared_runners_count = @shared_runners.count(:all)
@group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id)
end end
def define_secret_variables def define_secret_variables
......
...@@ -14,31 +14,49 @@ module Ci ...@@ -14,31 +14,49 @@ module Ci
has_many :builds has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects has_many :projects, through: :runner_projects
has_many :runner_namespaces
has_many :groups, through: :runner_namespaces
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
before_validation :set_default_values before_validation :set_default_values
scope :specific, ->() { where(is_shared: false) } scope :specific, -> { where(is_shared: false) }
scope :shared, ->() { where(is_shared: true) } scope :shared, -> { where(is_shared: true) }
scope :active, ->() { where(active: true) } scope :active, -> { where(active: true) }
scope :paused, ->() { where(active: false) } scope :paused, -> { where(active: false) }
scope :online, ->() { where('contacted_at > ?', contact_time_deadline) } scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
scope :ordered, ->() { order(id: :desc) } scope :ordered, -> { order(id: :desc) }
scope :owned_or_shared, ->(project_id) do scope :belonging_to_project, -> (project_id) {
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
.where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) }
scope :belonging_to_parent_group_of_project, -> (project_id) {
project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
hierarchy_groups = Gitlab::GroupHierarchy.new(project_groups).base_and_ancestors
joins(:groups).where(namespaces: { id: hierarchy_groups })
}
scope :owned_or_shared, -> (project_id) do
union = Gitlab::SQL::Union.new(
[belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), shared],
remove_duplicates: false
)
from("(#{union.to_sql}) ci_runners")
end end
scope :assignable_for, ->(project) do scope :assignable_for, ->(project) do
# FIXME: That `to_sql` is needed to workaround a weird Rails bug. # FIXME: That `to_sql` is needed to workaround a weird Rails bug.
# Without that, placeholders would miss one and couldn't match. # Without that, placeholders would miss one and couldn't match.
where(locked: false) where(locked: false)
.where.not("id IN (#{project.runners.select(:id).to_sql})").specific .where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})")
.specific
end end
validate :tag_constraints validate :tag_constraints
validate :either_projects_or_group
validates :access_level, presence: true validates :access_level, presence: true
acts_as_taggable acts_as_taggable
...@@ -50,6 +68,12 @@ module Ci ...@@ -50,6 +68,12 @@ module Ci
ref_protected: 1 ref_protected: 1
} }
enum runner_type: {
instance_type: 1,
group_type: 2,
project_type: 3
}
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
...@@ -120,6 +144,14 @@ module Ci ...@@ -120,6 +144,14 @@ module Ci
!shared? !shared?
end end
def assigned_to_group?
runner_namespaces.any?
end
def assigned_to_project?
runner_projects.any?
end
def can_pick?(build) def can_pick?(build)
return false if self.ref_protected? && !build.protected? return false if self.ref_protected? && !build.protected?
...@@ -174,6 +206,12 @@ module Ci ...@@ -174,6 +206,12 @@ module Ci
end end
end end
def pick_build!(build)
if can_pick?(build)
tick_runner_queue
end
end
private private
def cleanup_runner_queue def cleanup_runner_queue
...@@ -205,7 +243,17 @@ module Ci ...@@ -205,7 +243,17 @@ module Ci
end end
def assignable_for?(project_id) def assignable_for?(project_id)
is_shared? || projects.exists?(id: project_id) self.class.owned_or_shared(project_id).where(id: self.id).any?
end
def either_projects_or_group
if groups.many?
errors.add(:runner, 'can only be assigned to one group')
end
if assigned_to_group? && assigned_to_project?
errors.add(:runner, 'can only be assigned either to projects or to a group')
end
end end
def accepting_tags?(build) def accepting_tags?(build)
......
module Ci
class RunnerNamespace < ActiveRecord::Base
extend Gitlab::Ci::Model
belongs_to :runner
belongs_to :namespace, class_name: '::Namespace'
belongs_to :group, class_name: '::Group', foreign_key: :namespace_id
end
end
...@@ -9,6 +9,7 @@ class Group < Namespace ...@@ -9,6 +9,7 @@ class Group < Namespace
include SelectForProjectAuthorization include SelectForProjectAuthorization
include LoadedInGroupList include LoadedInGroupList
include GroupDescendant include GroupDescendant
include TokenAuthenticatable
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members alias_method :members, :group_members
...@@ -43,6 +44,8 @@ class Group < Namespace ...@@ -43,6 +44,8 @@ class Group < Namespace
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
add_authentication_token_field :runners_token
after_create :post_create_hook after_create :post_create_hook
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
after_save :update_two_factor_requirement after_save :update_two_factor_requirement
...@@ -294,6 +297,13 @@ class Group < Namespace ...@@ -294,6 +297,13 @@ class Group < Namespace
refresh_members_authorized_projects(blocking: false) refresh_members_authorized_projects(blocking: false)
end end
# each existing group needs to have a `runners_token`.
# we do this on read since migrating all existing groups is not a feasible
# solution.
def runners_token
ensure_runners_token!
end
private private
def update_two_factor_requirement def update_two_factor_requirement
......
...@@ -21,6 +21,9 @@ class Namespace < ActiveRecord::Base ...@@ -21,6 +21,9 @@ class Namespace < ActiveRecord::Base
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_statistics has_many :project_statistics
has_many :runner_namespaces, class_name: 'Ci::RunnerNamespace'
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
# This should _not_ be `inverse_of: :namespace`, because that would also set # This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`. # `user.namespace` when this user creates a group with themselves as `owner`.
belongs_to :owner, class_name: "User" belongs_to :owner, class_name: "User"
......
...@@ -230,13 +230,11 @@ class Project < ActiveRecord::Base ...@@ -230,13 +230,11 @@ class Project < ActiveRecord::Base
has_many :project_deploy_tokens has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens has_many :deploy_tokens, through: :project_deploy_tokens
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_one :auto_devops, class_name: 'ProjectAutoDevops' has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute' has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
has_many :project_badges, class_name: 'ProjectBadge' has_many :project_badges, class_name: 'ProjectBadge'
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting' has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :project_feature, update_only: true
...@@ -247,6 +245,7 @@ class Project < ActiveRecord::Base ...@@ -247,6 +245,7 @@ class Project < ActiveRecord::Base
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
# Validations # Validations
validates :creator, presence: true, on: :create validates :creator, presence: true, on: :create
...@@ -332,6 +331,11 @@ class Project < ActiveRecord::Base ...@@ -332,6 +331,11 @@ class Project < ActiveRecord::Base
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) } scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) } scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
scope :with_group_runners_enabled, -> do
joins(:ci_cd_settings)
.where(project_ci_cd_settings: { group_runners_enabled: true })
end
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600 chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
...@@ -1301,12 +1305,17 @@ class Project < ActiveRecord::Base ...@@ -1301,12 +1305,17 @@ class Project < ActiveRecord::Base
@shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none @shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
end end
def active_shared_runners def group_runners
@active_shared_runners ||= shared_runners.active @group_runners ||= group_runners_enabled? ? Ci::Runner.belonging_to_parent_group_of_project(self.id) : Ci::Runner.none
end
def all_runners
union = Gitlab::SQL::Union.new([runners, group_runners, shared_runners])
Ci::Runner.from("(#{union.to_sql}) ci_runners")
end end
def any_runners?(&block) def any_runners?(&block)
active_runners.any?(&block) || active_shared_runners.any?(&block) all_runners.active.any?(&block)
end end
def valid_runners_token?(token) def valid_runners_token?(token)
...@@ -1874,6 +1883,10 @@ class Project < ActiveRecord::Base ...@@ -1874,6 +1883,10 @@ class Project < ActiveRecord::Base
[] []
end end
def toggle_ci_cd_settings!(settings_attribute)
ci_cd_settings.toggle!(settings_attribute)
end
def gitlab_deploy_token def gitlab_deploy_token
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token @gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
end end
......
class ProjectCiCdSetting < ActiveRecord::Base class ProjectCiCdSetting < ActiveRecord::Base
belongs_to :project belongs_to :project, inverse_of: :ci_cd_settings
# The version of the schema that first introduced this model/table. # The version of the schema that first introduced this model/table.
MINIMUM_SCHEMA_VERSION = 20180403035759 MINIMUM_SCHEMA_VERSION = 20180403035759
......
...@@ -17,8 +17,10 @@ module Ci ...@@ -17,8 +17,10 @@ module Ci
builds = builds =
if runner.shared? if runner.shared?
builds_for_shared_runner builds_for_shared_runner
elsif runner.group_type?
builds_for_group_runner
else else
builds_for_specific_runner builds_for_project_runner
end end
valid = true valid = true
...@@ -75,15 +77,24 @@ module Ci ...@@ -75,15 +77,24 @@ module Ci
.joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id') .joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id')
.where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
# Implement fair scheduling # Implement fair scheduling
# this returns builds that are ordered by number of running builds # this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all # we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id") joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
.order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') .order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end end
def builds_for_specific_runner def builds_for_project_runner
new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('created_at ASC') new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('id ASC')
end
def builds_for_group_runner
hierarchy_groups = Gitlab::GroupHierarchy.new(runner.groups).base_and_descendants
projects = Project.where(namespace_id: hierarchy_groups)
.with_group_runners_enabled
.with_builds_enabled
.without_deleted
new_builds.where(project: projects).order('id ASC')
end end
def running_builds_for_shared_runners def running_builds_for_shared_runners
...@@ -97,10 +108,6 @@ module Ci ...@@ -97,10 +108,6 @@ module Ci
builds builds
end end
def shared_runner_build_limits_feature_enabled?
ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
end
def register_failure def register_failure
failed_attempt_counter.increment failed_attempt_counter.increment
attempt_counter.increment attempt_counter.increment
......
module Ci module Ci
class UpdateBuildQueueService class UpdateBuildQueueService
def execute(build) def execute(build)
build.project.runners.each do |runner| tick_for(build, build.project.all_runners)
if runner.can_pick?(build) end
runner.tick_runner_queue
end
end
return unless build.project.shared_runners_enabled? private
Ci::Runner.shared.each do |runner| def tick_for(build, runners)
if runner.can_pick?(build) runners.each do |runner|
runner.tick_runner_queue runner.pick_build!(build)
end
end end
end end
end end
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
%td %td
- if runner.shared? - if runner.shared?
%span.label.label-success shared %span.label.label-success shared
- elsif runner.group_type?
%span.label.label-success group
- else - else
%span.label.label-info specific %span.label.label-info specific
- if runner.locked? - if runner.locked?
...@@ -19,7 +21,7 @@ ...@@ -19,7 +21,7 @@
%td %td
= runner.ip_address = runner.ip_address
%td %td
- if runner.shared? - if runner.shared? || runner.group_type?
n/a n/a
- else - else
= runner.projects.count(:all) = runner.projects.count(:all)
......
...@@ -16,6 +16,9 @@ ...@@ -16,6 +16,9 @@
%li %li
%span.label.label-success shared %span.label.label-success shared
\- Runner runs jobs from all unassigned projects \- Runner runs jobs from all unassigned projects
%li
%span.label.label-success group
\- Runner runs jobs from all unassigned projects in its group
%li %li
%span.label.label-info specific %span.label.label-info specific
\- Runner runs jobs from assigned projects \- Runner runs jobs from assigned projects
......
...@@ -19,6 +19,9 @@ ...@@ -19,6 +19,9 @@
%p %p
If you want Runners to build only specific projects, enable them in the table below. If you want Runners to build only specific projects, enable them in the table below.
Keep in mind that this is a one way transition. Keep in mind that this is a one way transition.
- elsif @runner.group_type?
.bs-callout.bs-callout-success
%h4 This runner will process jobs from all projects in its group and subgroups
- else - else
.bs-callout.bs-callout-info .bs-callout.bs-callout-info
%h4 This Runner will process jobs only from ASSIGNED projects %h4 This Runner will process jobs only from ASSIGNED projects
......
...@@ -42,31 +42,31 @@ ...@@ -42,31 +42,31 @@
= nav_link(html_options: { class: active_when(params[:filter].nil?) }) do = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
= link_to admin_users_path do = link_to admin_users_path do
Active Active
%small.badge= number_with_delimiter(User.active.count) %small.badge= limited_counter_with_delimiter(User.active)
= nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do = link_to admin_users_path(filter: "admins") do
Admins Admins
%small.badge= number_with_delimiter(User.admins.count) %small.badge= limited_counter_with_delimiter(User.admins)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
= link_to admin_users_path(filter: 'two_factor_enabled') do = link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled 2FA Enabled
%small.badge= number_with_delimiter(User.with_two_factor.count) %small.badge= limited_counter_with_delimiter(User.with_two_factor)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
= link_to admin_users_path(filter: 'two_factor_disabled') do = link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled 2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count) %small.badge= limited_counter_with_delimiter(User.without_two_factor)
= nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
= link_to admin_users_path(filter: 'external') do = link_to admin_users_path(filter: 'external') do
External External
%small.badge= number_with_delimiter(User.external.count) %small.badge= limited_counter_with_delimiter(User.external)
= nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
= link_to admin_users_path(filter: "blocked") do = link_to admin_users_path(filter: "blocked") do
Blocked Blocked
%small.badge= number_with_delimiter(User.blocked.count) %small.badge= limited_counter_with_delimiter(User.blocked)
= nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do = link_to admin_users_path(filter: "wop") do
Without projects Without projects
%small.badge= number_with_delimiter(User.without_projects.count) %small.badge= limited_counter_with_delimiter(User.without_projects)
%ul.flex-list.content-list %ul.flex-list.content-list
- if @users.empty? - if @users.empty?
......
- active_tab = local_assigns.fetch(:active_tab, 'blank')
- f = local_assigns.fetch(:f)
.project-import.row
.col-lg-12
.form-group.import-btn-container.clearfix
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong
Import project from
.import-buttons
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
%div
- if github_import_enabled?
= link_to new_import_github_path, class: 'btn js-import-github' do
= icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled?
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
= icon('bitbucket', text: 'Bitbucket')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
%div
- if gitlab_import_enabled?
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
%div
- if google_code_import_enabled?
= link_to new_import_google_code_path, class: 'btn import_google_code' do
= icon('google', text: 'Google Code')
%div
- if fogbugz_import_enabled?
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= icon('bug', text: 'Fogbugz')
%div
- if gitea_import_enabled?
= link_to new_import_gitea_path, class: 'btn import_gitea' do
= custom_icon('go_logo')
Gitea
%div
- if git_import_enabled?
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
= icon('git', text: 'Repo by URL')
.col-lg-12
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
%hr
= render "shared/import_form", f: f
= render 'new_project_fields', f: f, project_name_id: "import-url-name"
...@@ -57,54 +57,11 @@ ...@@ -57,54 +57,11 @@
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' } .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f| = form_for @project, html: { class: 'new_project' } do |f|
- if import_sources_enabled? - if import_sources_enabled?
.project-import.row = render 'import_project_pane', f: f, active_tab: active_tab
.col-lg-12 - else
.form-group.import-btn-container.clearfix .nothing-here-block
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong %h4 No import options available
Import project from %p Contact an administrator to enable options for importing your project.
.import-buttons
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
%div
- if github_import_enabled?
= link_to new_import_github_path, class: 'btn js-import-github' do
= icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled?
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
= icon('bitbucket', text: 'Bitbucket')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
%div
- if gitlab_import_enabled?
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
%div
- if google_code_import_enabled?
= link_to new_import_google_code_path, class: 'btn import_google_code' do
= icon('google', text: 'Google Code')
%div
- if fogbugz_import_enabled?
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= icon('bug', text: 'Fogbugz')
%div
- if gitea_import_enabled?
= link_to new_import_gitea_path, class: 'btn import_gitea' do
= custom_icon('go_logo')
Gitea
%div
- if git_import_enabled?
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
= icon('git', text: 'Repo by URL')
.col-lg-12
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
%hr
= render "shared/import_form", f: f
= render 'new_project_fields', f: f, project_name_id: "import-url-name"
.save-project-loader.hide .save-project-loader.hide
.center .center
......
%h3 Group Runners
.bs-callout.bs-callout-warning
GitLab Group Runners can execute code for all the projects in this group.
They can be managed using the #{link_to 'Runners API', help_page_path('api/runners.md')}.
- if @project.group
%hr
- if @project.group_runners_enabled?
= link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-warning', method: :post do
Disable group Runners
- else
= link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do
Enable group Runners
&nbsp; for this project
- if !@project.group
This project does not belong to a group and can therefore not make use of group Runners.
- elsif @group_runners.empty?
This group does not provide any group Runners yet.
- if can?(current_user, :admin_pipeline, @project.group)
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @project.group.runners_token, type: 'group' }
- else
Ask your group master to setup a group Runner.
- else
%h4.underlined-title Available group Runners : #{@group_runners.count}
%ul.bordered-list
= render partial: 'projects/runners/runner', collection: @group_runners, as: :runner
...@@ -23,3 +23,7 @@ ...@@ -23,3 +23,7 @@
= render 'projects/runners/specific_runners' = render 'projects/runners/specific_runners'
.col-sm-6 .col-sm-6
= render 'projects/runners/shared_runners' = render 'projects/runners/shared_runners'
.row
.col-sm-6
.col-sm-6
= render 'projects/runners/group_runners'
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
- else - else
- runner_project = @project.runner_projects.find_by(runner_id: runner) - runner_project = @project.runner_projects.find_by(runner_id: runner)
= link_to 'Disable for this project', project_runner_project_path(@project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' = link_to 'Disable for this project', project_runner_project_path(@project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- elsif runner.specific? - elsif !(runner.is_shared? || runner.group_type?) # We can simplify this to `runner.project_type?` when migrating #runner_type is complete
= form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f| = form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id = f.hidden_field :runner_id, value: runner.id
= f.submit 'Enable for this project', class: 'btn btn-sm' = f.submit 'Enable for this project', class: 'btn btn-sm'
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon_for_user(@user, 90), class: "avatar s90", alt: '' = image_tag avatar_icon_for_user(@user, 90), class: "avatar s90", alt: ''
.user-info .user-info.prepend-left-default.append-right-default
.cover-title .cover-title
= @user.name = @user.name
......
---
title: Reconcile project templates with Auto DevOps
merge_request: 18737
author:
type: changed
---
title: Add loading icon padding for pipeline environments
merge_request: 18631
author: George Tsiolis
type: fixed
---
title: Add padding to profile description
merge_request: 18663
author: George Tsiolis
type: changed
---
title: Break issue title for board card title and issuable header text
merge_request: 18674
author: George Tsiolis
type: changed
---
title: Output some useful information when running the rails console
merge_request: 18697
author:
type: added
---
title: Allow group masters to configure runners for groups
merge_request: 9646
author: Alexis Reigel
type: added
---
title: Inform the user when there are no project import options available
merge_request: 18716
author: George Tsiolis
type: changed
---
title: Move TimeTrackingSpentOnlyPane vue component
merge_request: 18710
author: George Tsiolis
type: performance
# rubocop:disable Rails/Output
if defined?(Rails::Console)
# note that this will not print out when using `spring`
justify = 15
puts "-------------------------------------------------------------------------------------"
puts " Gitlab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab::REVISION})"
puts " Gitlab Shell:".ljust(justify) + Gitlab::Shell.new.version
puts " #{Gitlab::Database.adapter_name}:".ljust(justify) + Gitlab::Database.version
puts "-------------------------------------------------------------------------------------"
end
...@@ -22,3 +22,16 @@ end.compact ...@@ -22,3 +22,16 @@ end.compact
Rails.application.config.action_dispatch.trusted_proxies = ( Rails.application.config.action_dispatch.trusted_proxies = (
['127.0.0.1', '::1'] + gitlab_trusted_proxies) ['127.0.0.1', '::1'] + gitlab_trusted_proxies)
# A monkey patch to make trusted proxies work with Rails 5.0.
# Inspired by https://github.com/rails/rails/issues/5223#issuecomment-263778719
# Remove this monkey patch when upstream is fixed.
if Gitlab.rails5?
module TrustedProxyMonkeyPatch
def ip
@ip ||= (get_header("action_dispatch.remote_ip") || super).to_s
end
end
ActionDispatch::Request.send(:include, TrustedProxyMonkeyPatch)
end
...@@ -409,6 +409,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -409,6 +409,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
collection do collection do
post :toggle_shared_runners post :toggle_shared_runners
post :toggle_group_runners
end end
end end
......
class AddCiRunnerNamespaces < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :ci_runner_namespaces do |t|
t.integer :runner_id
t.integer :namespace_id
t.index [:runner_id, :namespace_id], unique: true
t.index :namespace_id
t.foreign_key :ci_runners, column: :runner_id, on_delete: :cascade
t.foreign_key :namespaces, column: :namespace_id, on_delete: :cascade
end
end
end
class AddRunnersTokenToGroups < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :namespaces, :runners_token, :string
end
end
class AddRunnerTypeToCiRunners < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_runners, :runner_type, :smallint
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexToNamespacesRunnersToken < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :namespaces, :runners_token, unique: true
end
def down
if index_exists?(:namespaces, :runners_token, unique: true)
remove_index :namespaces, :runners_token
end
end
end
class BackfillRunnerTypeForCiRunnersPostMigrate < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INSTANCE_RUNNER_TYPE = 1
PROJECT_RUNNER_TYPE = 3
disable_ddl_transaction!
def up
update_column_in_batches(:ci_runners, :runner_type, INSTANCE_RUNNER_TYPE) do |table, query|
query.where(table[:is_shared].eq(true)).where(table[:runner_type].eq(nil))
end
update_column_in_batches(:ci_runners, :runner_type, PROJECT_RUNNER_TYPE) do |table, query|
query.where(table[:is_shared].eq(false)).where(table[:runner_type].eq(nil))
end
end
def down
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180425131009) do ActiveRecord::Schema.define(version: 20180503150427) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -444,6 +444,14 @@ ActiveRecord::Schema.define(version: 20180425131009) do ...@@ -444,6 +444,14 @@ ActiveRecord::Schema.define(version: 20180425131009) do
add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
add_index "ci_pipelines", ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree add_index "ci_pipelines", ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree
create_table "ci_runner_namespaces", force: :cascade do |t|
t.integer "runner_id"
t.integer "namespace_id"
end
add_index "ci_runner_namespaces", ["namespace_id"], name: "index_ci_runner_namespaces_on_namespace_id", using: :btree
add_index "ci_runner_namespaces", ["runner_id", "namespace_id"], name: "index_ci_runner_namespaces_on_runner_id_and_namespace_id", unique: true, using: :btree
create_table "ci_runner_projects", force: :cascade do |t| create_table "ci_runner_projects", force: :cascade do |t|
t.integer "runner_id", null: false t.integer "runner_id", null: false
t.datetime "created_at" t.datetime "created_at"
...@@ -472,6 +480,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do ...@@ -472,6 +480,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do
t.integer "access_level", default: 0, null: false t.integer "access_level", default: 0, null: false
t.string "ip_address" t.string "ip_address"
t.integer "maximum_timeout" t.integer "maximum_timeout"
t.integer "runner_type", limit: 2
end end
add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
...@@ -1261,6 +1270,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do ...@@ -1261,6 +1270,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do
t.boolean "require_two_factor_authentication", default: false, null: false t.boolean "require_two_factor_authentication", default: false, null: false
t.integer "two_factor_grace_period", default: 48, null: false t.integer "two_factor_grace_period", default: 48, null: false
t.integer "cached_markdown_version" t.integer "cached_markdown_version"
t.string "runners_token"
end end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
...@@ -1271,6 +1281,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do ...@@ -1271,6 +1281,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do
add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "namespaces", ["require_two_factor_authentication"], name: "index_namespaces_on_require_two_factor_authentication", using: :btree add_index "namespaces", ["require_two_factor_authentication"], name: "index_namespaces_on_require_two_factor_authentication", using: :btree
add_index "namespaces", ["runners_token"], name: "index_namespaces_on_runners_token", unique: true, using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: :cascade do |t| create_table "notes", force: :cascade do |t|
...@@ -2087,6 +2098,8 @@ ActiveRecord::Schema.define(version: 20180425131009) do ...@@ -2087,6 +2098,8 @@ ActiveRecord::Schema.define(version: 20180425131009) do
add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade
add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade
add_foreign_key "ci_runner_namespaces", "namespaces", on_delete: :cascade
add_foreign_key "ci_runner_projects", "projects", name: "fk_4478a6f1e4", on_delete: :cascade add_foreign_key "ci_runner_projects", "projects", name: "fk_4478a6f1e4", on_delete: :cascade
add_foreign_key "ci_stages", "ci_pipelines", column: "pipeline_id", name: "fk_fb57e6cc56", on_delete: :cascade add_foreign_key "ci_stages", "ci_pipelines", column: "pipeline_id", name: "fk_fb57e6cc56", on_delete: :cascade
add_foreign_key "ci_stages", "projects", name: "fk_2360681d1d", on_delete: :cascade add_foreign_key "ci_stages", "projects", name: "fk_2360681d1d", on_delete: :cascade
......
...@@ -310,7 +310,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. ...@@ -310,7 +310,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
})); }));
``` ```
1. Don not use a singleton for the service or the store 1. Do not use a singleton for the service or the store
```javascript ```javascript
// bad // bad
class Store { class Store {
...@@ -328,9 +328,11 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. ...@@ -328,9 +328,11 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
} }
} }
``` ```
1. Use `.vue` for Vue templates. Do not use `%template` in HAML.
#### Naming #### Naming
1. **Extensions**: Use `.vue` extension for Vue components.
1. **Extensions**: Use `.vue` extension for Vue components. Do not use `.js` as file extension ([#34371]).
1. **Reference Naming**: Use PascalCase for their instances: 1. **Reference Naming**: Use PascalCase for their instances:
```javascript ```javascript
// bad // bad
...@@ -364,6 +366,8 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. ...@@ -364,6 +366,8 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
<component my-prop="prop" /> <component my-prop="prop" />
``` ```
[#34371]: https://gitlab.com/gitlab-org/gitlab-ce/issues/34371
#### Alignment #### Alignment
1. Follow these alignment styles for the template method: 1. Follow these alignment styles for the template method:
1. With more than one attribute, all attributes should be on a new line: 1. With more than one attribute, all attributes should be on a new line:
......
...@@ -5,10 +5,6 @@ module SharedGroup ...@@ -5,10 +5,6 @@ module SharedGroup
is_member_of(current_user.name, "Owned", Gitlab::Access::DEVELOPER) is_member_of(current_user.name, "Owned", Gitlab::Access::DEVELOPER)
end end
step '"John Doe" is owner of group "Owned"' do
is_member_of("John Doe", "Owned", Gitlab::Access::OWNER)
end
step '"John Doe" is guest of group "Guest"' do step '"John Doe" is guest of group "Guest"' do
is_member_of("John Doe", "Guest", Gitlab::Access::GUEST) is_member_of("John Doe", "Guest", Gitlab::Access::GUEST)
end end
......
...@@ -48,10 +48,6 @@ module SharedPaths ...@@ -48,10 +48,6 @@ module SharedPaths
visit group_group_members_path(Group.find_by(name: "Owned")) visit group_group_members_path(Group.find_by(name: "Owned"))
end end
step 'I visit group "Owned" settings page' do
visit edit_group_path(Group.find_by(name: "Owned"))
end
step 'I visit group "Owned" projects page' do step 'I visit group "Owned" projects page' do
visit projects_group_path(Group.find_by(name: "Owned")) visit projects_group_path(Group.find_by(name: "Owned"))
end end
......
...@@ -149,11 +149,11 @@ module API ...@@ -149,11 +149,11 @@ module API
expose_url(api_v4_projects_path(id: project.id)) expose_url(api_v4_projects_path(id: project.id))
end end
expose :issues, if: -> (*args) { issues_available?(*args) } do |project| expose :issues, if: -> (project, options) { issues_available?(project, options) } do |project|
expose_url(api_v4_projects_issues_path(id: project.id)) expose_url(api_v4_projects_issues_path(id: project.id))
end end
expose :merge_requests, if: -> (*args) { mrs_available?(*args) } do |project| expose :merge_requests, if: -> (project, options) { mrs_available?(project, options) } do |project|
expose_url(api_v4_projects_merge_requests_path(id: project.id)) expose_url(api_v4_projects_merge_requests_path(id: project.id))
end end
...@@ -242,13 +242,18 @@ module API ...@@ -242,13 +242,18 @@ module API
expose :requested_at expose :requested_at
end end
class Group < Grape::Entity class BasicGroupDetails < Grape::Entity
expose :id, :name, :path, :description, :visibility expose :id
expose :web_url
expose :name
end
class Group < BasicGroupDetails
expose :path, :description, :visibility
expose :lfs_enabled?, as: :lfs_enabled expose :lfs_enabled?, as: :lfs_enabled
expose :avatar_url do |group, options| expose :avatar_url do |group, options|
group.avatar_url(only_path: false) group.avatar_url(only_path: false)
end end
expose :web_url
expose :request_access_enabled expose :request_access_enabled
expose :full_name, :full_path expose :full_name, :full_path
...@@ -984,6 +989,13 @@ module API ...@@ -984,6 +989,13 @@ module API
options[:current_user].authorized_projects.where(id: runner.projects) options[:current_user].authorized_projects.where(id: runner.projects)
end end
end end
expose :groups, with: Entities::BasicGroupDetails do |runner, options|
if options[:current_user].admin?
runner.groups
else
options[:current_user].authorized_groups.where(id: runner.groups)
end
end
end end
class RunnerRegistrationDetails < Grape::Entity class RunnerRegistrationDetails < Grape::Entity
......
...@@ -23,10 +23,13 @@ module API ...@@ -23,10 +23,13 @@ module API
runner = runner =
if runner_registration_token_valid? if runner_registration_token_valid?
# Create shared runner. Requires admin access # Create shared runner. Requires admin access
Ci::Runner.create(attributes.merge(is_shared: true)) Ci::Runner.create(attributes.merge(is_shared: true, runner_type: :instance_type))
elsif project = Project.find_by(runners_token: params[:token]) elsif project = Project.find_by(runners_token: params[:token])
# Create a specific runner for project. # Create a specific runner for the project
project.runners.create(attributes) project.runners.create(attributes.merge(runner_type: :project_type))
elsif group = Group.find_by(runners_token: params[:token])
# Create a specific runner for the group
group.runners.create(attributes.merge(runner_type: :group_type))
end end
break forbidden! unless runner break forbidden! unless runner
......
...@@ -20,6 +20,9 @@ module Gitlab ...@@ -20,6 +20,9 @@ module Gitlab
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
].freeze ].freeze
SEARCH_CONTEXT_LINES = 3 SEARCH_CONTEXT_LINES = 3
# In https://gitlab.com/gitlab-org/gitaly/merge_requests/698
# We copied these two prefixes into gitaly-go, so don't change these
# or things will break! (REBASE_WORKTREE_PREFIX and SQUASH_WORKTREE_PREFIX)
REBASE_WORKTREE_PREFIX = 'rebase'.freeze REBASE_WORKTREE_PREFIX = 'rebase'.freeze
SQUASH_WORKTREE_PREFIX = 'squash'.freeze SQUASH_WORKTREE_PREFIX = 'squash'.freeze
GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze
...@@ -1671,10 +1674,14 @@ module Gitlab ...@@ -1671,10 +1674,14 @@ module Gitlab
end end
end end
# This function is duplicated in Gitaly-Go, don't change it!
# https://gitlab.com/gitlab-org/gitaly/merge_requests/698
def fresh_worktree?(path) def fresh_worktree?(path)
File.exist?(path) && !clean_stuck_worktree(path) File.exist?(path) && !clean_stuck_worktree(path)
end end
# This function is duplicated in Gitaly-Go, don't change it!
# https://gitlab.com/gitlab-org/gitaly/merge_requests/698
def clean_stuck_worktree(path) def clean_stuck_worktree(path)
return false unless File.mtime(path) < 15.minutes.ago return false unless File.mtime(path) < 15.minutes.ago
......
...@@ -25,9 +25,9 @@ module Gitlab ...@@ -25,9 +25,9 @@ module Gitlab
end end
TEMPLATES_TABLE = [ TEMPLATES_TABLE = [
ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes an MVC structure, gemfile, rakefile, and .gitlab-ci.yml file, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'), ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'),
ProjectTemplate.new('spring', 'Spring', 'Includes an MVC structure, mvnw, pom.xml, and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'), ProjectTemplate.new('spring', 'Spring', 'Includes an MVC structure, mvnw and pom.xml to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'),
ProjectTemplate.new('express', 'NodeJS Express', 'Includes an MVC structure and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express') ProjectTemplate.new('express', 'NodeJS Express', 'Includes an MVC structure to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express')
].freeze ].freeze
class << self class << self
......
...@@ -17,6 +17,23 @@ describe Projects::Settings::CiCdController do ...@@ -17,6 +17,23 @@ describe Projects::Settings::CiCdController do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:show) expect(response).to render_template(:show)
end end
context 'with group runners' do
let(:group_runner) { create(:ci_runner) }
let(:parent_group) { create(:group) }
let(:group) { create(:group, runners: [group_runner], parent: parent_group) }
let(:other_project) { create(:project, group: group) }
let!(:project_runner) { create(:ci_runner, projects: [other_project]) }
let!(:shared_runner) { create(:ci_runner, :shared) }
it 'sets assignable project runners only' do
group.add_master(user)
get :show, namespace_id: project.namespace, project_id: project
expect(assigns(:assignable_runners)).to eq [project_runner]
end
end
end end
describe '#reset_cache' do describe '#reset_cache' do
......
...@@ -15,14 +15,18 @@ FactoryBot.define do ...@@ -15,14 +15,18 @@ FactoryBot.define do
namespace namespace
creator { group ? create(:user) : namespace&.owner } creator { group ? create(:user) : namespace&.owner }
# Nest Project Feature attributes
transient do transient do
# Nest Project Feature attributes
wiki_access_level ProjectFeature::ENABLED wiki_access_level ProjectFeature::ENABLED
builds_access_level ProjectFeature::ENABLED builds_access_level ProjectFeature::ENABLED
snippets_access_level ProjectFeature::ENABLED snippets_access_level ProjectFeature::ENABLED
issues_access_level ProjectFeature::ENABLED issues_access_level ProjectFeature::ENABLED
merge_requests_access_level ProjectFeature::ENABLED merge_requests_access_level ProjectFeature::ENABLED
repository_access_level ProjectFeature::ENABLED repository_access_level ProjectFeature::ENABLED
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first
group_runners_enabled nil
end end
after(:create) do |project, evaluator| after(:create) do |project, evaluator|
...@@ -47,6 +51,9 @@ FactoryBot.define do ...@@ -47,6 +51,9 @@ FactoryBot.define do
end end
project.group&.refresh_members_authorized_projects project.group&.refresh_members_authorized_projects
# assign the delegated `#ci_cd_settings` attributes after create
project.reload.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil?
end end
trait :public do trait :public do
......
...@@ -59,6 +59,47 @@ describe "Admin Runners" do ...@@ -59,6 +59,47 @@ describe "Admin Runners" do
expect(page).to have_text 'No runners found' expect(page).to have_text 'No runners found'
end end
end end
context 'group runner' do
let(:group) { create(:group) }
let!(:runner) { create(:ci_runner, groups: [group], runner_type: :group_type) }
it 'shows the label and does not show the project count' do
visit admin_runners_path
within "#runner_#{runner.id}" do
expect(page).to have_selector '.label', text: 'group'
expect(page).to have_text 'n/a'
end
end
end
context 'shared runner' do
it 'shows the label and does not show the project count' do
runner = create :ci_runner, :shared
visit admin_runners_path
within "#runner_#{runner.id}" do
expect(page).to have_selector '.label', text: 'shared'
expect(page).to have_text 'n/a'
end
end
end
context 'specific runner' do
it 'shows the label and the project count' do
project = create :project
runner = create :ci_runner, projects: [project]
visit admin_runners_path
within "#runner_#{runner.id}" do
expect(page).to have_selector '.label', text: 'specific'
expect(page).to have_text '1'
end
end
end
end end
describe "Runner show page" do describe "Runner show page" do
......
...@@ -285,7 +285,7 @@ describe "Admin::Users" do ...@@ -285,7 +285,7 @@ describe "Admin::Users" do
it "lists group projects" do it "lists group projects" do
within(:css, '.append-bottom-default + .panel') do within(:css, '.append-bottom-default + .panel') do
expect(page).to have_content 'Group projects' expect(page).to have_content 'Group projects'
expect(page).to have_link group.name, admin_group_path(group) expect(page).to have_link group.name, href: admin_group_path(group)
end end
end end
......
...@@ -161,6 +161,7 @@ feature 'Issues > User uses quick actions', :js do ...@@ -161,6 +161,7 @@ feature 'Issues > User uses quick actions', :js do
before do before do
target_project.add_master(user) target_project.add_master(user)
gitlab_sign_out
sign_in(user) sign_in(user)
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
end end
...@@ -220,6 +221,7 @@ feature 'Issues > User uses quick actions', :js do ...@@ -220,6 +221,7 @@ feature 'Issues > User uses quick actions', :js do
before do before do
target_project.add_master(user) target_project.add_master(user)
gitlab_sign_out
sign_in(user) sign_in(user)
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
end end
......
...@@ -181,4 +181,84 @@ feature 'Runners' do ...@@ -181,4 +181,84 @@ feature 'Runners' do
expect(page.find('.shared-runners-description')).to have_content('Disable shared Runners') expect(page.find('.shared-runners-description')).to have_content('Disable shared Runners')
end end
end end
context 'group runners' do
background do
project.add_master(user)
end
given(:group) { create :group }
context 'as project and group master' do
background do
group.add_master(user)
end
context 'project with a group but no group runner' do
given(:project) { create :project, group: group }
scenario 'group runners are not available' do
visit runners_path(project)
expect(page).to have_content 'This group does not provide any group Runners yet.'
expect(page).to have_content 'Setup a group Runner manually'
expect(page).not_to have_content 'Ask your group master to setup a group Runner.'
end
end
end
context 'as project master' do
context 'project without a group' do
given(:project) { create :project }
scenario 'group runners are not available' do
visit runners_path(project)
expect(page).to have_content 'This project does not belong to a group and can therefore not make use of group Runners.'
end
end
context 'project with a group but no group runner' do
given(:group) { create :group }
given(:project) { create :project, group: group }
scenario 'group runners are not available' do
visit runners_path(project)
expect(page).to have_content 'This group does not provide any group Runners yet.'
expect(page).not_to have_content 'Setup a group Runner manually'
expect(page).to have_content 'Ask your group master to setup a group Runner.'
end
end
context 'project with a group and a group runner' do
given(:group) { create :group }
given(:project) { create :project, group: group }
given!(:ci_runner) { create :ci_runner, groups: [group], description: 'group-runner' }
scenario 'group runners are available' do
visit runners_path(project)
expect(page).to have_content 'Available group Runners : 1'
expect(page).to have_content 'group-runner'
end
scenario 'group runners may be disabled for a project' do
visit runners_path(project)
click_on 'Disable group Runners'
expect(page).to have_content 'Enable group Runners'
expect(project.reload.group_runners_enabled).to be false
click_on 'Enable group Runners'
expect(page).to have_content 'Disable group Runners'
expect(project.reload.group_runners_enabled).to be true
end
end
end
end
end end
...@@ -48,36 +48,32 @@ describe('RepoFile', () => { ...@@ -48,36 +48,32 @@ describe('RepoFile', () => {
}); });
}); });
describe('locked file', () => { describe('folder', () => {
let f; it('renders changes count inside folder', () => {
const f = {
beforeEach(() => { ...file('folder'),
f = file('locked file'); path: 'testing',
f.file_lock = { type: 'tree',
user: { branchId: 'master',
name: 'testuser', projectId: 'project',
updated_at: new Date(),
},
}; };
store.state.changedFiles.push({
...file('fileName'),
path: 'testing/fileName',
});
createComponent({ createComponent({
file: f, file: f,
level: 0, level: 0,
}); });
});
it('renders lock icon', () => { const treeChangesEl = vm.$el.querySelector('.ide-tree-changes');
expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
});
it('renders a tooltip', () => { expect(treeChangesEl).not.toBeNull();
expect( expect(treeChangesEl.textContent).toContain('1');
vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset.originalTitle,
).toContain('Locked by testuser');
}); });
});
describe('folder', () => {
it('renders action dropdown', done => { it('renders action dropdown', done => {
createComponent({ createComponent({
file: { file: {
...@@ -115,4 +111,33 @@ describe('RepoFile', () => { ...@@ -115,4 +111,33 @@ describe('RepoFile', () => {
}); });
}); });
}); });
describe('locked file', () => {
let f;
beforeEach(() => {
f = file('locked file');
f.file_lock = {
user: {
name: 'testuser',
updated_at: new Date(),
},
};
createComponent({
file: f,
level: 0,
});
});
it('renders lock icon', () => {
expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
});
it('renders a tooltip', () => {
expect(
vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset.originalTitle,
).toContain('Locked by testuser');
});
});
}); });
...@@ -78,4 +78,67 @@ describe('IDE store getters', () => { ...@@ -78,4 +78,67 @@ describe('IDE store getters', () => {
expect(getters.allBlobs(localState)[0].name).toBe('blob'); expect(getters.allBlobs(localState)[0].name).toBe('blob');
}); });
}); });
describe('getChangesInFolder', () => {
it('returns length of changed files for a path', () => {
localState.changedFiles.push(
{
path: 'test/index',
name: 'index',
},
{
path: 'app/123',
name: '123',
},
);
expect(getters.getChangesInFolder(localState)('test')).toBe(1);
});
it('returns length of changed & staged files for a path', () => {
localState.changedFiles.push(
{
path: 'test/index',
name: 'index',
},
{
path: 'testing/123',
name: '123',
},
);
localState.stagedFiles.push(
{
path: 'test/123',
name: '123',
},
{
path: 'test/index',
name: 'index',
},
{
path: 'testing/12345',
name: '12345',
},
);
expect(getters.getChangesInFolder(localState)('test')).toBe(2);
});
it('returns length of changed & tempFiles files for a path', () => {
localState.changedFiles.push(
{
path: 'test/index',
name: 'index',
},
{
path: 'test/newfile',
name: 'newfile',
tempFile: true,
},
);
expect(getters.getChangesInFolder(localState)('test')).toBe(2);
});
});
}); });
...@@ -170,5 +170,19 @@ describe('Participants', function () { ...@@ -170,5 +170,19 @@ describe('Participants', function () {
expect(vm.isShowingMoreParticipants).toBe(true); expect(vm.isShowingMoreParticipants).toBe(true);
}); });
it('clicking on participants icon emits `toggleSidebar` event', () => {
vm = mountComponent(Participants, {
loading: false,
participants: PARTICIPANT_LIST,
numberOfLessParticipants: 2,
});
spyOn(vm, '$emit');
const participantsIconEl = vm.$el.querySelector('.sidebar-collapsed-icon');
participantsIconEl.click();
expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
});
}); });
}); });
...@@ -3,7 +3,6 @@ import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_sub ...@@ -3,7 +3,6 @@ import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_sub
import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store'; import SidebarStore from '~/sidebar/stores/sidebar_store';
import eventHub from '~/sidebar/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import Mock from './mock_data'; import Mock from './mock_data';
...@@ -32,7 +31,7 @@ describe('Sidebar Subscriptions', function () { ...@@ -32,7 +31,7 @@ describe('Sidebar Subscriptions', function () {
mediator, mediator,
}); });
eventHub.$emit('toggleSubscription'); vm.onToggleSubscription();
expect(mediator.toggleSubscription).toHaveBeenCalled(); expect(mediator.toggleSubscription).toHaveBeenCalled();
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue'; import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import eventHub from '~/sidebar/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Subscriptions', function () { describe('Subscriptions', function () {
...@@ -39,4 +40,22 @@ describe('Subscriptions', function () { ...@@ -39,4 +40,22 @@ describe('Subscriptions', function () {
expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-checked'); expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-checked');
}); });
it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
vm = mountComponent(Subscriptions, { subscribed: true });
spyOn(eventHub, '$emit');
spyOn(vm, '$emit');
vm.toggleSubscription();
expect(eventHub.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
});
it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
vm = mountComponent(Subscriptions, { subscribed: true });
spyOn(vm, '$emit');
vm.onClickCollapsedIcon();
expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
});
}); });
...@@ -258,7 +258,6 @@ project: ...@@ -258,7 +258,6 @@ project:
- builds - builds
- runner_projects - runner_projects
- runners - runners
- active_runners
- variables - variables
- triggers - triggers
- pipeline_schedules - pipeline_schedules
...@@ -286,6 +285,7 @@ project: ...@@ -286,6 +285,7 @@ project:
- internal_ids - internal_ids
- project_deploy_tokens - project_deploy_tokens
- deploy_tokens - deploy_tokens
- settings
- ci_cd_settings - ci_cd_settings
award_emoji: award_emoji:
- awardable - awardable
......
...@@ -19,6 +19,63 @@ describe Ci::Runner do ...@@ -19,6 +19,63 @@ describe Ci::Runner do
end end
end end
end end
context 'either_projects_or_group' do
let(:group) { create(:group) }
it 'disallows assigning to a group if already assigned to a group' do
runner = create(:ci_runner, groups: [group])
runner.groups << build(:group)
expect(runner).not_to be_valid
expect(runner.errors.full_messages).to eq ['Runner can only be assigned to one group']
end
it 'disallows assigning to a group if already assigned to a project' do
project = create(:project)
runner = create(:ci_runner, projects: [project])
runner.groups << build(:group)
expect(runner).not_to be_valid
expect(runner.errors.full_messages).to eq ['Runner can only be assigned either to projects or to a group']
end
it 'disallows assigning to a project if already assigned to a group' do
runner = create(:ci_runner, groups: [group])
runner.projects << build(:project)
expect(runner).not_to be_valid
expect(runner.errors.full_messages).to eq ['Runner can only be assigned either to projects or to a group']
end
it 'allows assigning to a group if not assigned to a group nor a project' do
runner = create(:ci_runner)
runner.groups << build(:group)
expect(runner).to be_valid
end
it 'allows assigning to a project if not assigned to a group nor a project' do
runner = create(:ci_runner)
runner.projects << build(:project)
expect(runner).to be_valid
end
it 'allows assigning to a project if already assigned to a project' do
project = create(:project)
runner = create(:ci_runner, projects: [project])
runner.projects << build(:project)
expect(runner).to be_valid
end
end
end end
describe '#access_level' do describe '#access_level' do
...@@ -49,6 +106,80 @@ describe Ci::Runner do ...@@ -49,6 +106,80 @@ describe Ci::Runner do
end end
end end
describe '.shared' do
let(:group) { create(:group) }
let(:project) { create(:project) }
it 'returns the shared group runner' do
runner = create(:ci_runner, :shared, groups: [group])
expect(described_class.shared).to eq [runner]
end
it 'returns the shared project runner' do
runner = create(:ci_runner, :shared, projects: [project])
expect(described_class.shared).to eq [runner]
end
end
describe '.belonging_to_project' do
it 'returns the specific project runner' do
# own
specific_project = create(:project)
specific_runner = create(:ci_runner, :specific, projects: [specific_project])
# other
other_project = create(:project)
create(:ci_runner, :specific, projects: [other_project])
expect(described_class.belonging_to_project(specific_project.id)).to eq [specific_runner]
end
end
describe '.belonging_to_parent_group_of_project' do
let(:project) { create(:project, group: group) }
let(:group) { create(:group) }
let(:runner) { create(:ci_runner, :specific, groups: [group]) }
let!(:unrelated_group) { create(:group) }
let!(:unrelated_project) { create(:project, group: unrelated_group) }
let!(:unrelated_runner) { create(:ci_runner, :specific, groups: [unrelated_group]) }
it 'returns the specific group runner' do
expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner)
end
context 'with a parent group with a runner', :nested_groups do
let(:runner) { create(:ci_runner, :specific, groups: [parent_group]) }
let(:project) { create(:project, group: group) }
let(:group) { create(:group, parent: parent_group) }
let(:parent_group) { create(:group) }
it 'returns the group runner from the parent group' do
expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner)
end
end
end
describe '.owned_or_shared' do
it 'returns a globally shared, a project specific and a group specific runner' do
# group specific
group = create(:group)
project = create(:project, group: group)
group_runner = create(:ci_runner, :specific, groups: [group])
# project specific
project_runner = create(:ci_runner, :specific, projects: [project])
# globally shared
shared_runner = create(:ci_runner, :shared)
expect(described_class.owned_or_shared(project.id)).to contain_exactly(
group_runner, project_runner, shared_runner
)
end
end
describe '#display_name' do describe '#display_name' do
it 'returns the description if it has a value' do it 'returns the description if it has a value' do
runner = FactoryBot.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') runner = FactoryBot.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
...@@ -163,7 +294,9 @@ describe Ci::Runner do ...@@ -163,7 +294,9 @@ describe Ci::Runner do
describe '#can_pick?' do describe '#can_pick?' do
let(:pipeline) { create(:ci_pipeline) } let(:pipeline) { create(:ci_pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) }
let(:runner) { create(:ci_runner) } let(:runner) { create(:ci_runner, tag_list: tag_list, run_untagged: run_untagged) }
let(:tag_list) { [] }
let(:run_untagged) { true }
subject { runner.can_pick?(build) } subject { runner.can_pick?(build) }
...@@ -171,6 +304,13 @@ describe Ci::Runner do ...@@ -171,6 +304,13 @@ describe Ci::Runner do
build.project.runners << runner build.project.runners << runner
end end
context 'a different runner' do
it 'cannot handle builds' do
other_runner = create(:ci_runner)
expect(other_runner.can_pick?(build)).to be_falsey
end
end
context 'when runner does not have tags' do context 'when runner does not have tags' do
it 'can handle builds without tags' do it 'can handle builds without tags' do
expect(runner.can_pick?(build)).to be_truthy expect(runner.can_pick?(build)).to be_truthy
...@@ -184,9 +324,7 @@ describe Ci::Runner do ...@@ -184,9 +324,7 @@ describe Ci::Runner do
end end
context 'when runner has tags' do context 'when runner has tags' do
before do let(:tag_list) { %w(bb cc) }
runner.tag_list = %w(bb cc)
end
shared_examples 'tagged build picker' do shared_examples 'tagged build picker' do
it 'can handle build with matching tags' do it 'can handle build with matching tags' do
...@@ -211,9 +349,7 @@ describe Ci::Runner do ...@@ -211,9 +349,7 @@ describe Ci::Runner do
end end
context 'when runner cannot pick untagged jobs' do context 'when runner cannot pick untagged jobs' do
before do let(:run_untagged) { false }
runner.run_untagged = false
end
it 'cannot handle builds without tags' do it 'cannot handle builds without tags' do
expect(runner.can_pick?(build)).to be_falsey expect(runner.can_pick?(build)).to be_falsey
...@@ -224,8 +360,9 @@ describe Ci::Runner do ...@@ -224,8 +360,9 @@ describe Ci::Runner do
end end
context 'when runner is shared' do context 'when runner is shared' do
let(:runner) { create(:ci_runner, :shared) }
before do before do
runner.is_shared = true
build.project.runners = [] build.project.runners = []
end end
...@@ -234,9 +371,7 @@ describe Ci::Runner do ...@@ -234,9 +371,7 @@ describe Ci::Runner do
end end
context 'when runner is locked' do context 'when runner is locked' do
before do let(:runner) { create(:ci_runner, :shared, locked: true) }
runner.locked = true
end
it 'can handle builds' do it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy expect(runner.can_pick?(build)).to be_truthy
...@@ -260,6 +395,17 @@ describe Ci::Runner do ...@@ -260,6 +395,17 @@ describe Ci::Runner do
expect(runner.can_pick?(build)).to be_falsey expect(runner.can_pick?(build)).to be_falsey
end end
end end
context 'when runner is assigned to a group' do
before do
build.project.runners = []
runner.groups << create(:group, projects: [build.project])
end
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
end
end
end end
context 'when access_level of runner is not_protected' do context 'when access_level of runner is not_protected' do
...@@ -583,4 +729,76 @@ describe Ci::Runner do ...@@ -583,4 +729,76 @@ describe Ci::Runner do
expect(described_class.search(runner.description.upcase)).to eq([runner]) expect(described_class.search(runner.description.upcase)).to eq([runner])
end end
end end
describe '#assigned_to_group?' do
subject { runner.assigned_to_group? }
context 'when project runner' do
let(:runner) { create(:ci_runner, description: 'Project runner', projects: [project]) }
let(:project) { create(:project) }
it { is_expected.to be_falsey }
end
context 'when shared runner' do
let(:runner) { create(:ci_runner, :shared, description: 'Shared runner') }
it { is_expected.to be_falsey }
end
context 'when group runner' do
let(:group) { create(:group) }
let(:runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
it { is_expected.to be_truthy }
end
end
describe '#assigned_to_project?' do
subject { runner.assigned_to_project? }
context 'when group runner' do
let(:runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
let(:group) { create(:group) }
it { is_expected.to be_falsey }
end
context 'when shared runner' do
let(:runner) { create(:ci_runner, :shared, description: 'Shared runner') }
it { is_expected.to be_falsey }
end
context 'when project runner' do
let(:runner) { create(:ci_runner, description: 'Group runner', projects: [project]) }
let(:project) { create(:project) }
it { is_expected.to be_truthy }
end
end
describe '#pick_build!' do
context 'runner can pick the build' do
it 'calls #tick_runner_queue' do
ci_build = build(:ci_build)
runner = build(:ci_runner)
allow(runner).to receive(:can_pick?).with(ci_build).and_return(true)
expect(runner).to receive(:tick_runner_queue)
runner.pick_build!(ci_build)
end
end
context 'runner cannot pick the build' do
it 'does not call #tick_runner_queue' do
ci_build = build(:ci_build)
runner = build(:ci_runner)
allow(runner).to receive(:can_pick?).with(ci_build).and_return(false)
expect(runner).not_to receive(:tick_runner_queue)
runner.pick_build!(ci_build)
end
end
end
end end
...@@ -63,7 +63,6 @@ describe Project do ...@@ -63,7 +63,6 @@ describe Project do
it { is_expected.to have_many(:build_trace_section_names)} it { is_expected.to have_many(:build_trace_section_names)}
it { is_expected.to have_many(:runner_projects) } it { is_expected.to have_many(:runner_projects) }
it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:active_runners) }
it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) } it { is_expected.to have_many(:triggers) }
it { is_expected.to have_many(:pages_domains) } it { is_expected.to have_many(:pages_domains) }
...@@ -1139,45 +1138,106 @@ describe Project do ...@@ -1139,45 +1138,106 @@ describe Project do
end end
end end
describe '#any_runners' do describe '#any_runners?' do
let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) } context 'shared runners' do
let(:specific_runner) { create(:ci_runner) } let(:project) { create :project, shared_runners_enabled: shared_runners_enabled }
let(:shared_runner) { create(:ci_runner, :shared) } let(:specific_runner) { create :ci_runner }
let(:shared_runner) { create :ci_runner, :shared }
context 'for shared runners disabled' do context 'for shared runners disabled' do
let(:shared_runners_enabled) { false } let(:shared_runners_enabled) { false }
it 'has no runners available' do it 'has no runners available' do
expect(project.any_runners?).to be_falsey expect(project.any_runners?).to be_falsey
end end
it 'has a specific runner' do it 'has a specific runner' do
project.runners << specific_runner project.runners << specific_runner
expect(project.any_runners?).to be_truthy
end expect(project.any_runners?).to be_truthy
end
it 'has a shared runner, but they are prohibited to use' do
shared_runner
expect(project.any_runners?).to be_falsey
end
it 'checks the presence of specific runner' do
project.runners << specific_runner
expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
end
it 'has a shared runner, but they are prohibited to use' do it 'returns false if match cannot be found' do
shared_runner project.runners << specific_runner
expect(project.any_runners?).to be_falsey
expect(project.any_runners? { false }).to be_falsey
end
end end
it 'checks the presence of specific runner' do context 'for shared runners enabled' do
project.runners << specific_runner let(:shared_runners_enabled) { true }
expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
it 'has a shared runner' do
shared_runner
expect(project.any_runners?).to be_truthy
end
it 'checks the presence of shared runner' do
shared_runner
expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy
end
it 'returns false if match cannot be found' do
shared_runner
expect(project.any_runners? { false }).to be_falsey
end
end end
end end
context 'for shared runners enabled' do context 'group runners' do
let(:shared_runners_enabled) { true } let(:project) { create :project, group_runners_enabled: group_runners_enabled }
let(:group) { create :group, projects: [project] }
let(:group_runner) { create :ci_runner, groups: [group] }
context 'for group runners disabled' do
let(:group_runners_enabled) { false }
it 'has no runners available' do
expect(project.any_runners?).to be_falsey
end
it 'has a group runner, but they are prohibited to use' do
group_runner
it 'has a shared runner' do expect(project.any_runners?).to be_falsey
shared_runner end
expect(project.any_runners?).to be_truthy
end end
it 'checks the presence of shared runner' do context 'for group runners enabled' do
shared_runner let(:group_runners_enabled) { true }
expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy
it 'has a group runner' do
group_runner
expect(project.any_runners?).to be_truthy
end
it 'checks the presence of group runner' do
group_runner
expect(project.any_runners? { |runner| runner == group_runner }).to be_truthy
end
it 'returns false if match cannot be found' do
group_runner
expect(project.any_runners? { false }).to be_falsey
end
end end
end end
end end
...@@ -3541,6 +3601,18 @@ describe Project do ...@@ -3541,6 +3601,18 @@ describe Project do
end end
end end
describe '#toggle_ci_cd_settings!' do
it 'toggles the value on #settings' do
project = create(:project, group_runners_enabled: false)
expect(project.group_runners_enabled).to be false
project.toggle_ci_cd_settings!(:group_runners_enabled)
expect(project.group_runners_enabled).to be true
end
end
describe '#gitlab_deploy_token' do describe '#gitlab_deploy_token' do
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -40,18 +40,36 @@ describe API::Runner do ...@@ -40,18 +40,36 @@ describe API::Runner do
expect(json_response['token']).to eq(runner.token) expect(json_response['token']).to eq(runner.token)
expect(runner.run_untagged).to be true expect(runner.run_untagged).to be true
expect(runner.token).not_to eq(registration_token) expect(runner.token).not_to eq(registration_token)
expect(runner).to be_instance_type
end end
context 'when project token is used' do context 'when project token is used' do
let(:project) { create(:project) } let(:project) { create(:project) }
it 'creates runner' do it 'creates project runner' do
post api('/runners'), token: project.runners_token post api('/runners'), token: project.runners_token
expect(response).to have_gitlab_http_status 201 expect(response).to have_gitlab_http_status 201
expect(project.runners.size).to eq(1) expect(project.runners.size).to eq(1)
expect(Ci::Runner.first.token).not_to eq(registration_token) runner = Ci::Runner.first
expect(Ci::Runner.first.token).not_to eq(project.runners_token) expect(runner.token).not_to eq(registration_token)
expect(runner.token).not_to eq(project.runners_token)
expect(runner).to be_project_type
end
end
context 'when group token is used' do
let(:group) { create(:group) }
it 'creates a group runner' do
post api('/runners'), token: group.runners_token
expect(response).to have_http_status 201
expect(group.runners.size).to eq(1)
runner = Ci::Runner.first
expect(runner.token).not_to eq(registration_token)
expect(runner.token).not_to eq(group.runners_token)
expect(runner).to be_group_type
end end
end end
end end
......
This diff is collapsed.
...@@ -118,7 +118,7 @@ describe PipelineSerializer do ...@@ -118,7 +118,7 @@ describe PipelineSerializer do
it 'verifies number of queries', :request_store do it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject } recorded = ActiveRecord::QueryRecorder.new { subject }
expect(recorded.count).to be_within(1).of(36) expect(recorded.count).to be_within(1).of(44)
expect(recorded.cached_count).to eq(0) expect(recorded.cached_count).to eq(0)
end end
end end
......
...@@ -2,11 +2,13 @@ require 'spec_helper' ...@@ -2,11 +2,13 @@ require 'spec_helper'
module Ci module Ci
describe RegisterJobService do describe RegisterJobService do
let!(:project) { FactoryBot.create :project, shared_runners_enabled: false } set(:group) { create(:group) }
let!(:pipeline) { FactoryBot.create :ci_pipeline, project: project } set(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
let!(:pending_job) { FactoryBot.create :ci_build, pipeline: pipeline } set(:pipeline) { create(:ci_pipeline, project: project) }
let!(:shared_runner) { FactoryBot.create(:ci_runner, is_shared: true) } let!(:shared_runner) { create(:ci_runner, is_shared: true) }
let!(:specific_runner) { FactoryBot.create(:ci_runner, is_shared: false) } let!(:specific_runner) { create(:ci_runner, is_shared: false) }
let!(:group_runner) { create(:ci_runner, groups: [group], runner_type: :group_type) }
let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
before do before do
specific_runner.assign_to(project) specific_runner.assign_to(project)
...@@ -150,7 +152,7 @@ module Ci ...@@ -150,7 +152,7 @@ module Ci
context 'disallow when builds are disabled' do context 'disallow when builds are disabled' do
before do before do
project.update(shared_runners_enabled: true) project.update(shared_runners_enabled: true, group_runners_enabled: true)
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
end end
...@@ -160,13 +162,90 @@ module Ci ...@@ -160,13 +162,90 @@ module Ci
it { expect(build).to be_nil } it { expect(build).to be_nil }
end end
context 'and uses specific runner' do context 'and uses group runner' do
let(:build) { execute(group_runner) }
it { expect(build).to be_nil }
end
context 'and uses project runner' do
let(:build) { execute(specific_runner) } let(:build) { execute(specific_runner) }
it { expect(build).to be_nil } it { expect(build).to be_nil }
end end
end end
context 'allow group runners' do
before do
project.update!(group_runners_enabled: true)
end
context 'for multiple builds' do
let!(:project2) { create :project, group_runners_enabled: true, group: group }
let!(:pipeline2) { create :ci_pipeline, project: project2 }
let!(:project3) { create :project, group_runners_enabled: true, group: group }
let!(:pipeline3) { create :ci_pipeline, project: project3 }
let!(:build1_project1) { pending_job }
let!(:build2_project1) { create :ci_build, pipeline: pipeline }
let!(:build3_project1) { create :ci_build, pipeline: pipeline }
let!(:build1_project2) { create :ci_build, pipeline: pipeline2 }
let!(:build2_project2) { create :ci_build, pipeline: pipeline2 }
let!(:build1_project3) { create :ci_build, pipeline: pipeline3 }
# these shouldn't influence the scheduling
let!(:unrelated_group) { create :group }
let!(:unrelated_project) { create :project, group_runners_enabled: true, group: unrelated_group }
let!(:unrelated_pipeline) { create :ci_pipeline, project: unrelated_project }
let!(:build1_unrelated_project) { create :ci_build, pipeline: unrelated_pipeline }
let!(:unrelated_group_runner) { create :ci_runner, groups: [unrelated_group] }
it 'does not consider builds from other group runners' do
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 6
execute(group_runner)
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 5
execute(group_runner)
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 4
execute(group_runner)
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 3
execute(group_runner)
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 2
execute(group_runner)
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 1
execute(group_runner)
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 0
expect(execute(group_runner)).to be_nil
end
end
context 'group runner' do
let(:build) { execute(group_runner) }
it { expect(build).to be_kind_of(Build) }
it { expect(build).to be_valid }
it { expect(build).to be_running }
it { expect(build.runner).to eq(group_runner) }
end
end
context 'disallow group runners' do
before do
project.update!(group_runners_enabled: false)
end
context 'group runner' do
let(:build) { execute(group_runner) }
it { expect(build).to be_nil }
end
end
context 'when first build is stalled' do context 'when first build is stalled' do
before do before do
pending_job.update(lock_version: 0) pending_job.update(lock_version: 0)
...@@ -178,7 +257,7 @@ module Ci ...@@ -178,7 +257,7 @@ module Ci
let!(:other_build) { create :ci_build, pipeline: pipeline } let!(:other_build) { create :ci_build, pipeline: pipeline }
before do before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner) allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
.and_return(Ci::Build.where(id: [pending_job, other_build])) .and_return(Ci::Build.where(id: [pending_job, other_build]))
end end
...@@ -190,7 +269,7 @@ module Ci ...@@ -190,7 +269,7 @@ module Ci
context 'when single build is in queue' do context 'when single build is in queue' do
before do before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner) allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
.and_return(Ci::Build.where(id: pending_job)) .and_return(Ci::Build.where(id: pending_job))
end end
...@@ -201,7 +280,7 @@ module Ci ...@@ -201,7 +280,7 @@ module Ci
context 'when there is no build in queue' do context 'when there is no build in queue' do
before do before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner) allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
.and_return(Ci::Build.none) .and_return(Ci::Build.none)
end end
......
...@@ -8,21 +8,19 @@ describe Ci::UpdateBuildQueueService do ...@@ -8,21 +8,19 @@ describe Ci::UpdateBuildQueueService do
context 'when updating specific runners' do context 'when updating specific runners' do
let(:runner) { create(:ci_runner) } let(:runner) { create(:ci_runner) }
context 'when there are runner that can pick build' do context 'when there is a runner that can pick build' do
before do before do
build.project.runners << runner build.project.runners << runner
end end
it 'ticks runner queue value' do it 'ticks runner queue value' do
expect { subject.execute(build) } expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
.to change { runner.ensure_runner_queue_value }
end end
end end
context 'when there are no runners that can pick build' do context 'when there is no runner that can pick build' do
it 'does not tick runner queue value' do it 'does not tick runner queue value' do
expect { subject.execute(build) } expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
.not_to change { runner.ensure_runner_queue_value }
end end
end end
end end
...@@ -30,21 +28,61 @@ describe Ci::UpdateBuildQueueService do ...@@ -30,21 +28,61 @@ describe Ci::UpdateBuildQueueService do
context 'when updating shared runners' do context 'when updating shared runners' do
let(:runner) { create(:ci_runner, :shared) } let(:runner) { create(:ci_runner, :shared) }
context 'when there are runner that can pick build' do context 'when there is no runner that can pick build' do
it 'ticks runner queue value' do it 'ticks runner queue value' do
expect { subject.execute(build) } expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
.to change { runner.ensure_runner_queue_value }
end end
end end
context 'when there are no runners that can pick build' do context 'when there is no runner that can pick build due to tag mismatch' do
before do before do
build.tag_list = [:docker] build.tag_list = [:docker]
end end
it 'does not tick runner queue value' do it 'does not tick runner queue value' do
expect { subject.execute(build) } expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
.not_to change { runner.ensure_runner_queue_value } end
end
context 'when there is no runner that can pick build due to being disabled on project' do
before do
build.project.shared_runners_enabled = false
end
it 'does not tick runner queue value' do
expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
end
end
end
context 'when updating group runners' do
let(:group) { create :group }
let(:project) { create :project, group: group }
let(:runner) { create :ci_runner, groups: [group] }
context 'when there is a runner that can pick build' do
it 'ticks runner queue value' do
expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
end
end
context 'when there is no runner that can pick build due to tag mismatch' do
before do
build.tag_list = [:docker]
end
it 'does not tick runner queue value' do
expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
end
end
context 'when there is no runner that can pick build due to being disabled on project' do
before do
build.project.group_runners_enabled = false
end
it 'does not tick runner queue value' do
expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
end end
end end
end end
......
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