Commit 7f0f8594 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 5845-extract-ee-environments-files

* master: (69 commits)
  Calculating repository checksums executed by Gitaly
  Resolve "Expand API: Render an arbitrary Markdown document"
  Update EE > CE downgrade service removal steps
  Make stores export a createStore() which can be used in tests
  Simplify pattern lexeme fabrication and matcher
  Simplify untrusted regexp factory method
  Fix api_json.log not always reporting the right HTTP status code
  Move group lists css from framework/lists.scss to pages/groups.scss
  Resolve "Web IDE: Previewing Markdown in Firefox doesn’t show a scroll bar"
  Add Keyboard shortcuts for "Kubernetes" and "Environments"
  Move API group deletion to Sidekiq
  fix typos. add a reference to deliverable and stretch for design artifact
  fix / assigne username wrapping problem has been fixed
  Memoize Gitlab::Database.version
  Conditionally add Gitaly deprecation warnings based on ENV variable
  Bring CE-EE parity to app/services/milestones/base_service.rb
  Bring CE-EE parity to app/services/lfs/unlock_file_service.rb
  Fixes 500 error on /estimate BIG_VALUE
  Fix: Use case in-sensitive ordering by name for groups
  Fix group lists visual
  ...
parents 06818655 8bacfbd1
......@@ -189,7 +189,7 @@ stages:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
variables:
CREATE_DB_USER: "true"
SETUP_DB: "false"
script:
# Manually clone gitlab-test and only seed this project in
# db/fixtures/development/04_project.rb thanks to SIZE=1 below
......@@ -233,7 +233,7 @@ stages:
.migration-paths: &migration-paths
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables:
CREATE_DB_USER: "true"
SETUP_DB: "false"
script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
......@@ -242,7 +242,7 @@ stages:
- cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- date
- git checkout $CI_COMMIT_SHA
- git checkout -f $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS
- date
- . scripts/prepare_build.sh
......
......@@ -168,7 +168,7 @@ hits. They are not always necessary, but very convenient.
If you are an expert in a particular area, it makes it easier to find issues to
work on. You can also subscribe to those labels to receive an email each time an
issue is labelled with a subject label corresponding to your expertise.
issue is labeled with a subject label corresponding to your expertise.
Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
~issues, ~"merge requests", ~labels, and ~"container registry".
......@@ -296,7 +296,24 @@ any potential community contributor to @-mention per above.
## Implement design & UI elements
Please see the [UX Guide for GitLab].
For guidance on UX implementation at GitLab, please refer to our [Design System](https://design.gitlab.com/).
The UX team uses labels to manage their workflow.
The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/ux/) of the handbook.
Once an issue has been worked on and is ready for development, a UXer applies the ~"UX ready" label to that issue.
The UX team has a special type label called ~"design artifact". This label indicates that the final output
for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone.
Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
needed until the solution has been decided.
~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone.
Once the ~"design artifact" issue has been completed, the UXer removes the ~"design artifact" label and applies the ~"UX ready" label. The Product Manager can use the
existing issue or decide to create a whole new issue for the purpose of development.
## Issue tracker
......
......@@ -5,7 +5,7 @@ import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
export default {
components: {
......@@ -70,7 +70,7 @@ export default {
? this.$refs.formEl && this.$refs.formEl.offsetHeight
: this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
this.componentHeight = elHeight;
},
enterTransition() {
this.$nextTick(() => {
......@@ -78,7 +78,7 @@ export default {
? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
: this.$refs.formEl && this.$refs.formEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
this.componentHeight = elHeight;
});
},
afterEndTransition() {
......
......@@ -122,11 +122,11 @@ export default {
<div
class="file"
:class="fileClass"
@click="clickFile"
role="button"
>
<div
class="file-name"
@click="clickFile"
role="button"
>
<span
class="ide-file-name str-truncated"
......
......@@ -5,8 +5,6 @@ export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
export const COMMIT_ITEM_PADDING = 32;
// Commit message textarea
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
......
<script>
import $ from 'jquery';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import { dasherize } from '../../../lib/utils/text_utility';
import eventHub from '../../event_hub';
import axios from '~/lib/utils/axios_utils';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import createFlash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* Renders either a cancel, retry or play icon button and handles the post request
*
* Used in:
* - mr widget mini pipeline graph: `mr_widget_pipeline.vue`
* - pipelines table
* - pipelines table in merge request page
* - pipelines table in commit page
* - pipelines detail page in big graph
*/
export default {
components: {
......@@ -32,16 +42,10 @@ export default {
required: true,
},
requestFinishedFor: {
type: String,
required: false,
default: '',
},
},
data() {
return {
isDisabled: false,
linkRequested: '',
};
},
......@@ -51,19 +55,28 @@ export default {
return `${actionIconDash} js-icon-${actionIconDash}`;
},
},
watch: {
requestFinishedFor() {
if (this.requestFinishedFor === this.linkRequested) {
this.isDisabled = false;
}
},
},
methods: {
/**
* The request should not be handled here.
* However due to this component being used in several
* different apps it avoids repetition & complexity.
*
*/
onClickAction() {
$(this.$el).tooltip('hide');
eventHub.$emit('postAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
axios.post(`${this.link}.json`)
.then(() => {
this.isDisabled = false;
this.$emit('pipelineActionRequestComplete');
})
.catch(() => {
this.isDisabled = false;
createFlash(__('An error occurred while making the request.'));
});
},
},
};
......@@ -80,6 +93,6 @@ btn-transparent ci-action-icon-container ci-action-icon-wrapper"
data-container="body"
:disabled="isDisabled"
>
<icon :name="actionIcon" />
<icon :name="actionIcon"/>
</button>
</template>
......@@ -42,11 +42,6 @@ export default {
type: Object,
required: true,
},
requestFinishedFor: {
type: String,
required: false,
default: '',
},
},
computed: {
......@@ -76,11 +71,15 @@ export default {
e.stopPropagation();
});
},
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
},
},
};
</script>
<template>
<div class="ci-job-dropdown-container">
<div class="ci-job-dropdown-container dropdown">
<button
v-tooltip
type="button"
......@@ -110,7 +109,7 @@ export default {
<job-component
:job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item"
:request-finished-for="requestFinishedFor"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
......
......@@ -16,11 +16,6 @@ export default {
type: Object,
required: true,
},
requestFinishedFor: {
type: String,
required: false,
default: '',
},
},
computed: {
......@@ -51,6 +46,10 @@ export default {
return className;
},
refreshPipelineGraph() {
this.$emit('refreshPipelineGraph');
},
},
};
</script>
......@@ -74,7 +73,7 @@ export default {
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
:request-finished-for="requestFinishedFor"
@refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
</div>
......
......@@ -46,11 +46,6 @@ export default {
required: false,
default: '',
},
requestFinishedFor: {
type: String,
required: false,
default: '',
},
},
computed: {
status() {
......@@ -84,6 +79,11 @@ export default {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
},
methods: {
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
},
},
};
</script>
<template>
......@@ -126,7 +126,7 @@ export default {
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:request-finished-for="requestFinishedFor"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</div>
</template>
......@@ -29,12 +29,6 @@ export default {
required: false,
default: '',
},
requestFinishedFor: {
type: String,
required: false,
default: '',
},
},
methods: {
......@@ -49,6 +43,10 @@ export default {
buildConnnectorClass(index) {
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
pipelineActionRequestComplete() {
this.$emit('refreshPipelineGraph');
},
},
};
</script>
......@@ -75,12 +73,13 @@ export default {
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
:request-finished-for="requestFinishedFor"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
......
......@@ -9,6 +9,7 @@
import CommitComponent from '../../vue_shared/components/commit.vue';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
import Icon from '../../vue_shared/components/icon.vue';
import { PIPELINES_TABLE } from '../constants';
/**
* Pipeline table row.
......@@ -46,6 +47,7 @@
required: true,
},
},
pipelinesTable: PIPELINES_TABLE,
data() {
return {
isRetrying: false,
......@@ -297,6 +299,7 @@
v-for="(stage, index) in pipeline.details.stages"
:key="index">
<pipeline-stage
:type="$options.pipelinesTable"
:stage="stage"
:update-dropdown="updateGraphDropdown"
/>
......
......@@ -21,6 +21,7 @@ import Icon from '../../vue_shared/components/icon.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import JobComponent from './graph/job_component.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import { PIPELINES_TABLE } from '../constants';
export default {
components: {
......@@ -44,6 +45,12 @@ export default {
required: false,
default: false,
},
type: {
type: String,
required: false,
default: '',
},
},
data() {
......@@ -133,6 +140,16 @@ export default {
isDropdownOpen() {
return this.$el.classList.contains('open');
},
pipelineActionRequestComplete() {
if (this.type === PIPELINES_TABLE) {
// warn the table to update
eventHub.$emit('refreshPipelinesTable');
} else {
// close the dropdown in mr widget
$(this.$refs.dropdown).dropdown('toggle');
}
},
},
};
</script>
......@@ -151,6 +168,7 @@ export default {
id="stageDropdown"
aria-haspopup="true"
aria-expanded="false"
ref="dropdown"
>
<span
......@@ -188,6 +206,7 @@ export default {
<job-component
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
......
// eslint-disable-next-line import/prefer-default-export
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
export const PIPELINES_TABLE = 'PIPELINES_TABLE';
......@@ -55,11 +55,13 @@ export default {
eventHub.$on('postAction', this.postAction);
eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
},
destroyed() {
this.poll.stop();
......
......@@ -25,30 +25,14 @@ export default () => {
data() {
return {
mediator,
requestFinishedFor: null,
};
},
created() {
eventHub.$on('postAction', this.postAction);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
},
methods: {
postAction(action) {
// Click was made, reset this variable
this.requestFinishedFor = null;
this.mediator.service
.postAction(action)
.then(() => {
this.mediator.refreshPipeline();
this.requestFinishedFor = action;
})
.catch(() => {
this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.'));
});
requestRefreshPipelineGraph() {
// When an action is clicked
// (wether in the dropdown or in the main nodes, we refresh the big graph)
this.mediator.refreshPipeline()
.catch(() => Flash(__('An error occurred while making the request.')));
},
},
render(createElement) {
......@@ -56,7 +40,9 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
requestFinishedFor: this.requestFinishedFor,
},
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
},
});
},
......
......@@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts {
super();
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity'));
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
......@@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project');
......
......@@ -279,251 +279,14 @@ ul.indent-list {
padding: 10px 0 0 30px;
}
// Specific styles for tree list
@keyframes spin-avatar {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.groups-list-tree-container {
.has-no-search-results {
text-align: center;
padding: $gl-padding;
font-style: italic;
color: $well-light-text-color;
}
> .group-list-tree > .group-row.has-children:first-child {
border-top: 0;
}
}
.group-list-tree {
.avatar-container.content-loading {
position: relative;
> a,
> a .avatar {
height: 100%;
border-radius: 50%;
}
> a {
padding: 2px;
.avatar {
border: 2px solid $white-normal;
&.identicon {
line-height: 15px;
}
}
}
&::after {
content: "";
position: absolute;
height: 100%;
width: 100%;
background-color: transparent;
border: 2px outset $kdb-border;
border-radius: 50%;
animation: spin-avatar 3s infinite linear;
}
}
.folder-toggle-wrap {
float: left;
line-height: $list-text-height;
font-size: 0;
span {
font-size: $gl-font-size;
}
}
.folder-caret,
.item-type-icon {
display: inline-block;
}
.folder-caret {
width: 15px;
svg {
margin-bottom: 2px;
}
}
.item-type-icon {
margin-top: 2px;
width: 20px;
}
> .group-row:not(.has-children) {
.folder-caret {
opacity: 0;
}
}
.content-list li:last-child {
padding-bottom: 0;
}
.group-list-tree {
margin-bottom: 0;
margin-left: 30px;
position: relative;
&::before {
content: '';
display: block;
width: 0;
position: absolute;
top: 5px;
bottom: 0;
left: -16px;
border-left: 2px solid $border-white-normal;
}
.group-row {
position: relative;
&::before {
content: "";
display: block;
width: 10px;
height: 0;
border-top: 2px solid $border-white-normal;
position: absolute;
top: 30px;
left: -16px;
}
&:last-child::before {
background: $white-light;
height: auto;
top: 30px;
bottom: 0;
}
&.being-removed {
opacity: 0.5;
}
}
}
.group-row {
padding: 0;
&.has-children {
border-top: 0;
}
&:first-child {
border-top: 1px solid $white-normal;
}
&:last-of-type {
.group-row-contents:not(:hover) {
border-bottom: 1px solid transparent;
}
}
}
.group-row-contents {
padding: 10px 10px 8px;
border-top: solid 1px transparent;
border-bottom: solid 1px $white-normal;
&:hover {
border-color: $row-hover-border;
background-color: $row-hover;
cursor: pointer;
}
.avatar-container > a {
width: 100%;
text-decoration: none;
}
&.has-more-items {
display: block;
padding: 20px 10px;
}
.stats {
position: relative;
line-height: 46px;
> span {
display: inline-flex;
align-items: center;
height: 16px;
min-width: 30px;
}
> span:last-child {
margin-right: 0;
}
.stat-value {
margin: 2px 0 0 5px;
}
}
.controls {
margin-left: 5px;
> .btn {
margin-right: $btn-xs-side-margin;
}
}
}
.project-row-contents .stats {
line-height: inherit;
> span:first-child {
margin-left: 25px;
}
.item-visibility {
margin-right: 0;
}
.last-updated {
position: absolute;
right: 12px;
min-width: 250px;
text-align: right;
color: $gl-text-color-secondary;
}
}
}
.namespace-title {
.tooltip-inner {
max-width: 350px;
}
}
ul.group-list-tree {
li.group-row {
> .group-row-contents .title {
line-height: $list-text-height;
}
&.has-description > .group-row-contents .title {
line-height: inherit;
}
}
}
.js-groups-list-holder {
.groups-list-loading {
font-size: 34px;
text-align: center;
}
}
......@@ -18,6 +18,10 @@
.group-row {
@include basic-list-stats;
.description p {
margin-bottom: 0;
}
}
.ldap-group-links {
......@@ -237,3 +241,231 @@
overflow-y: unset;
}
}
.groups-list-tree-container {
.has-no-search-results {
text-align: center;
padding: $gl-padding;
font-style: italic;
color: $well-light-text-color;
}
> .group-list-tree > .group-row.has-children:first-child {
border-top: 0;
}
}
.group-list-tree {
.avatar-container.content-loading {
position: relative;
> a,
> a .avatar {
height: 100%;
border-radius: 50%;
}
> a {
padding: 2px;
.avatar {
border: 2px solid $white-normal;
&.identicon {
line-height: 15px;
}
}
}
&::after {
content: "";
position: absolute;
height: 100%;
width: 100%;
background-color: transparent;
border: 2px outset $kdb-border;
border-radius: 50%;
animation: spin-avatar 3s infinite linear;
}
}
.folder-toggle-wrap {
float: left;
line-height: $list-text-height;
font-size: 0;
span {
font-size: $gl-font-size;
}
}
.folder-caret,
.item-type-icon {
display: inline-block;
}
.folder-caret {
width: 15px;
svg {
margin-bottom: 2px;
}
}
.item-type-icon {
margin-top: 2px;
width: 20px;
}
> .group-row:not(.has-children) {
.folder-caret {
opacity: 0;
}
}
.content-list li:last-child {
padding-bottom: 0;
}
.group-list-tree {
margin-bottom: 0;
margin-left: 30px;
position: relative;
&::before {
content: '';
display: block;
width: 0;
position: absolute;
top: 5px;
bottom: 0;
left: -16px;
border-left: 2px solid $border-white-normal;
}
.group-row {
position: relative;
&::before {
content: "";
display: block;
width: 10px;
height: 0;
border-top: 2px solid $border-white-normal;
position: absolute;
top: 30px;
left: -16px;
}
&:last-child::before {
background: $white-light;
height: auto;
top: 30px;
bottom: 0;
}
&.being-removed {
opacity: 0.5;
}
}
}
.group-row {
padding: 0;
&.has-children {
border-top: 0;
}
&:first-child {
border-top: 1px solid $white-normal;
}
}
.group-row-contents {
padding: $gl-padding-top;
&:hover {
border-color: $row-hover-border;
background-color: $row-hover;
cursor: pointer;
}
.avatar-container > a {
width: 100%;
text-decoration: none;
}
&.has-more-items {
display: block;
padding: 20px 10px;
}
.stats {
position: relative;
line-height: 46px;
> span {
display: inline-flex;
align-items: center;
height: 16px;
min-width: 30px;
}
> span:last-child {
margin-right: 0;
}
.stat-value {
margin: 2px 0 0 5px;
}
}
.controls {
margin-left: 5px;
> .btn {
margin-right: $btn-xs-side-margin;
}
}
}
.project-row-contents .stats {
line-height: inherit;
> span:first-child {
margin-left: 25px;
}
.item-visibility {
margin-right: 0;
}
.last-updated {
position: absolute;
right: 12px;
min-width: 250px;
text-align: right;
color: $gl-text-color-secondary;
}
}
}
ul.group-list-tree {
li.group-row {
> .group-row-contents .title {
line-height: $list-text-height;
}
&.has-description > .group-row-contents .title {
line-height: inherit;
}
}
}
.js-groups-list-holder {
.groups-list-loading {
font-size: 34px;
text-align: center;
}
}
......@@ -197,9 +197,21 @@
}
&.assignee {
.author_link:hover {
.author {
text-decoration: underline;
.author_link {
display: block;
padding-left: 42px;
position: relative;
&:hover {
.author {
text-decoration: underline;
}
}
.avatar {
left: 0;
position: absolute;
top: 0;
}
}
}
......
......@@ -66,13 +66,9 @@
}
}
.btn-group {
&.open {
.btn-default {
background-color: $white-normal;
border-color: $border-white-normal;
}
}
.btn-group.open .btn-default {
background-color: $white-normal;
border-color: $border-white-normal;
}
.btn .text-center {
......@@ -361,16 +357,14 @@
&:not(:first-child) {
margin-left: 44px;
.left-connector {
&::before {
content: '';
position: absolute;
top: 48%;
left: -44px;
border-top: 2px solid $border-color;
width: 44px;
height: 1px;
}
.left-connector::before {
content: '';
position: absolute;
top: 48%;
left: -44px;
border-top: 2px solid $border-color;
width: 44px;
height: 1px;
}
}
}
......@@ -386,22 +380,16 @@
&:last-child {
.build {
// Remove right connecting horizontal line from first build in last stage
&:first-child {
&::after {
border: 0;
}
&:first-child::after {
border: 0;
}
// Remove right curved connectors from all builds in last stage
&:not(:first-child) {
&::after {
border: 0;
}
&:not(:first-child)::after {
border: 0;
}
// Remove opposite curve
.curve {
&::before {
display: none;
}
.curve::before {
display: none;
}
}
}
......@@ -409,16 +397,12 @@
&:first-child {
.build {
// Remove left curved connectors from all builds in first stage
&:not(:first-child) {
&::before {
border: 0;
}
&:not(:first-child)::before {
border: 0;
}
// Remove opposite curve
.curve {
&::after {
display: none;
}
.curve::after {
display: none;
}
}
}
......
......@@ -39,12 +39,15 @@
.ide-file-list {
flex: 1;
padding-left: $gl-padding;
padding-right: $gl-padding;
padding-bottom: $grid-size;
.file {
cursor: pointer;
&.file-open {
background: $link-active-background;
background: $white-normal;
}
&.file-active {
......@@ -84,12 +87,11 @@
.ide-new-btn {
display: none;
margin-right: -8px;
}
&:hover,
&:focus {
background: $link-active-background;
background: $white-normal;
.ide-new-btn {
display: block;
......@@ -111,12 +113,11 @@
}
}
.file-name,
.file-col-commit-message {
.file-name {
display: flex;
overflow: visible;
align-items: center;
padding: 6px 12px;
width: 100%;
}
.multi-file-loading-container {
......@@ -306,8 +307,18 @@
}
.preview-container {
height: 100%;
overflow: auto;
flex-grow: 1;
position: relative;
.md-previewer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: auto;
padding: $gl-padding;
}
.file-container {
background-color: $gray-darker;
......@@ -347,10 +358,6 @@
color: $diff-image-info-color;
}
}
.md-previewer {
padding: $gl-padding;
}
}
.ide-mode-tabs {
......@@ -501,7 +508,7 @@
align-items: center;
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding $gl-padding;
padding: 12px 0;
}
.multi-file-commit-panel-header-title {
......@@ -523,32 +530,31 @@
.multi-file-commit-list {
flex: 1;
overflow: auto;
padding: $gl-padding;
padding: $grid-size 0;
margin-left: -$grid-size;
margin-right: -$grid-size;
min-height: 60px;
.multi-file-commit-list-item {
margin-left: 0;
margin-right: 0;
}
&.help-block {
margin-left: 0;
right: 0;
}
}
.multi-file-commit-list-item {
display: flex;
padding: 0;
align-items: center;
border-radius: $border-radius-default;
.multi-file-discard-btn {
display: none;
margin-top: -2px;
margin-left: auto;
margin-right: $grid-size;
color: $gl-link-color;
&:focus,
&:hover {
text-decoration: underline;
}
}
&:hover {
background: $white-normal;
.multi-file-discard-btn {
display: flex;
}
......@@ -584,25 +590,39 @@
}
}
.multi-file-commit-list-item,
.ide-file-list .file {
display: flex;
align-items: center;
margin-left: -$grid-size;
margin-right: -$grid-size;
padding: $grid-size / 2 $grid-size;
border-radius: $border-radius-default;
text-align: left;
&:hover,
&:focus {
background: $white-normal;
}
}
.multi-file-commit-list-path {
padding: $grid-size / 2;
padding-left: $grid-size;
padding: 0;
background: none;
border: 0;
text-align: left;
width: 100%;
min-width: 0;
&:hover,
&:focus {
outline: 0;
}
svg {
min-width: 16px;
vertical-align: middle;
display: inline-block;
}
&:hover,
&:focus {
outline: 0;
}
}
.multi-file-commit-list-file-path {
......@@ -619,12 +639,18 @@
.multi-file-commit-form {
position: relative;
padding: $gl-padding;
background-color: $white-light;
border-top: 1px solid $white-dark;
border-left: 1px solid $white-dark;
transition: all 0.3s ease;
> form,
> .commit-form-compact {
padding: $gl-padding 0;
margin-left: $gl-padding;
margin-right: $gl-padding;
border-top: 1px solid $white-dark;
}
.btn {
font-size: $gl-font-size;
}
......@@ -787,8 +813,9 @@
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
min-height: 140px;
margin-left: $gl-padding;
margin-right: $gl-padding;
&.is-first {
border-bottom: 1px solid $white-dark;
......@@ -979,9 +1006,8 @@
.ide-tree-header {
display: flex;
align-items: center;
padding: 10px 0;
margin-left: 10px;
margin-right: 10px;
margin-bottom: 8px;
padding: 12px 0;
border-bottom: 1px solid $white-dark;
.ide-new-btn {
......@@ -1012,9 +1038,9 @@
.commit-form-slide-up-enter-active,
.commit-form-slide-up-leave-active {
position: absolute;
top: 16px;
left: 16px;
right: 16px;
top: 0;
left: 0;
right: 0;
transition: all 0.3s ease;
}
......
......@@ -94,7 +94,7 @@ module Boards
def serialize_as_json(resource)
resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight],
labels: true,
issue_endpoints: true,
include_full_project_path: board.group_board?,
......
module AcceptsPendingInvitations
extend ActiveSupport::Concern
def accept_pending_invitations
return unless resource.active_for_authentication?
clear_stored_location_for_resource if resource.accept_pending_invitations!.any?
end
def clear_stored_location_for_resource
session_key = stored_location_key_for(resource)
session.delete(session_key)
end
end
class ConfirmationsController < Devise::ConfirmationsController
include AcceptsPendingInvitations
def almost_there
flash[:notice] = nil
render layout: "devise_empty"
......@@ -11,6 +13,8 @@ class ConfirmationsController < Devise::ConfirmationsController
end
def after_confirmation_path_for(resource_name, resource)
accept_pending_invitations
# incoming resource can either be a :user or an :email
if signed_in?(:user)
after_sign_in(resource)
......
......@@ -23,7 +23,7 @@ class Profiles::KeysController < Profiles::ApplicationController
def destroy
@key = current_user.keys.find(params[:id])
@key.destroy
Keys::DestroyService.new(current_user).execute(@key)
respond_to do |format|
format.html { redirect_to profile_keys_url, status: 302 }
......
......@@ -18,19 +18,12 @@ class Projects::PipelinesController < Projects::ApplicationController
.page(params[:page])
.per(30)
@running_count = PipelinesFinder
.new(project, scope: 'running').execute.count
@running_count = limited_pipelines_count(project, 'running')
@pending_count = limited_pipelines_count(project, 'pending')
@finished_count = limited_pipelines_count(project, 'finished')
@pipelines_count = limited_pipelines_count(project)
@pending_count = PipelinesFinder
.new(project, scope: 'pending').execute.count
@finished_count = PipelinesFinder
.new(project, scope: 'finished').execute.count
@pipelines_count = PipelinesFinder
.new(project).execute.count
@pipelines.map(&:commit) # List commits for batch loading
Gitlab::Ci::Pipeline::Preloader.preload(@pipelines)
respond_to do |format|
format.html
......@@ -41,7 +34,7 @@ class Projects::PipelinesController < Projects::ApplicationController
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
.represent(@pipelines),
.represent(@pipelines, disable_coverage: true),
count: {
all: @pipelines_count,
running: @running_count,
......@@ -185,4 +178,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def authorize_update_pipeline!
return access_denied! unless can?(current_user, :update_pipeline, @pipeline)
end
def limited_pipelines_count(project, scope = nil)
finder = PipelinesFinder.new(project, scope: scope)
view_context.limited_counter_with_delimiter(finder.execute)
end
end
......@@ -11,7 +11,14 @@ module Projects
@hook = ProjectHook.new
# Services
@services = @project.find_or_initialize_services
@services = @project.find_or_initialize_services(exceptions: service_exceptions)
end
private
# Returns a list of services that should be hidden from the list
def service_exceptions
@project.disabled_services.dup
end
end
end
......
class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
include AcceptsPendingInvitations
before_action :whitelist_query_limiting, only: [:destroy]
......@@ -16,6 +17,7 @@ class RegistrationsController < Devise::RegistrationsController
end
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
accept_pending_invitations
super
else
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
......@@ -60,7 +62,7 @@ class RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(user)
Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}")
user.confirmed? ? dashboard_projects_path : users_almost_there_path
user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path
end
def after_inactive_sign_up_path_for(resource)
......
......@@ -184,7 +184,7 @@ module Ci
end
def playable?
action? && (manual? || complete?)
action? && (manual? || retryable?)
end
def action?
......
......@@ -406,7 +406,18 @@ module Ci
end
def has_warnings?
builds.latest.failed_but_allowed.any?
number_of_warnings.positive?
end
def number_of_warnings
BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
Build.where(commit_id: pipeline_ids)
.latest
.failed_but_allowed
.group(:commit_id)
.count
.each { |id, amount| loader.call(id, amount) }
end
end
def set_config_source
......
......@@ -224,8 +224,34 @@ class Commit
Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
end
def lazy_author
BatchLoader.for(author_email.downcase).batch do |emails, loader|
# A Hash that maps user Emails to the corresponding User objects. The
# Emails at this point are the _primary_ Emails of the Users.
users_for_emails = User
.by_any_email(emails)
.each_with_object({}) { |user, hash| hash[user.email] = user }
users_for_ids = users_for_emails
.values
.each_with_object({}) { |user, hash| hash[user.id] = user }
# Some commits may have used an alternative Email address. In this case we
# need to query the "emails" table to map those addresses to User objects.
Email
.where(email: emails - users_for_emails.keys)
.pluck(:email, :user_id)
.each { |(email, id)| users_for_emails[email] = users_for_ids[id] }
users_for_emails.each { |email, user| loader.call(email, user) }
end
end
def author
User.find_by_any_email(author_email.downcase)
# We use __sync so that we get the actual objects back (including an actual
# nil), instead of a wrapper, as returning a wrapped nil breaks a lot of
# code.
lazy_author.__sync
end
request_cache(:author) { author_email.downcase }
......
......@@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base
include HasStatus
include Importable
include AfterCommitQueue
include Presentable
self.table_name = 'ci_builds'
......
......@@ -12,8 +12,8 @@ module Sortable
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc) }
scope :order_name_asc, -> { reorder(name: :asc) }
scope :order_name_desc, -> { reorder(name: :desc) }
scope :order_name_asc, -> { reorder("lower(name) asc") }
scope :order_name_desc, -> { reorder("lower(name) desc") }
end
module ClassMethods
......
......@@ -53,6 +53,10 @@ module TimeTrackable
Gitlab::TimeTrackingFormatter.output(time_estimate)
end
def time_estimate=(val)
val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val)
end
private
def touchable?
......
......@@ -997,7 +997,7 @@ class Project < ActiveRecord::Base
available_services_names = Service.available_services_names - exceptions
available_services_names.map do |service_name|
available_services = available_services_names.map do |service_name|
service = find_service(services, service_name)
if service
......@@ -1014,6 +1014,14 @@ class Project < ActiveRecord::Base
end
end
end
available_services.reject do |service|
disabled_services.include?(service.to_param)
end
end
def disabled_services
[]
end
def find_or_initialize_service(name)
......
......@@ -860,6 +860,16 @@ class User < ActiveRecord::Base
confirmed? && !temp_oauth_email?
end
def accept_pending_invitations!
pending_invitations.select do |member|
member.accept_invite!(self)
end
end
def pending_invitations
Member.where(invite_email: verified_emails).invite
end
def all_emails
all_emails = []
all_emails << email unless temp_oauth_email?
......
module Ci
class BuildPresenter < Gitlab::View::Presenter::Delegated
CALLOUT_FAILURE_MESSAGES = {
unknown_failure: 'There is an unknown failure, please try again',
script_failure: 'There has been a script failure. Check the job log for more information',
api_failure: 'There has been an API failure, please try again',
stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
runner_system_failure: 'There has been a runner system failure, please try again',
missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
}.freeze
presents :build
class BuildPresenter < CommitStatusPresenter
def erased_by_user?
# Build can be erased through API, therefore it does not have
# `erased_by` user assigned in that case.
......@@ -44,14 +33,6 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}"
end
def callout_failure_message
CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
end
def recoverable?
failed? && !unrecoverable?
end
private
def tooltip_for_badge
......@@ -61,9 +42,5 @@ module Ci
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
def unrecoverable?
script_failure? || missing_dependency_failure?
end
end
end
class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
CALLOUT_FAILURE_MESSAGES = {
unknown_failure: 'There is an unknown failure, please try again',
script_failure: 'There has been a script failure. Check the job log for more information',
api_failure: 'There has been an API failure, please try again',
stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
runner_system_failure: 'There has been a runner system failure, please try again',
missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
}.freeze
presents :build
def callout_failure_message
CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
end
def recoverable?
failed? && !unrecoverable?
end
def unrecoverable?
script_failure? || missing_dependency_failure?
end
end
class GenericCommitStatusPresenter < CommitStatusPresenter
end
......@@ -4,7 +4,11 @@ class PipelineEntity < Grape::Entity
expose :id
expose :user, using: UserEntity
expose :active?, as: :active
expose :coverage
# Coverage isn't always necessary (e.g. when displaying project pipelines in
# the UI). Instead of creating an entirely different entity we just allow the
# disabling of this specific field whenever necessary.
expose :coverage, unless: proc { options[:disable_coverage] }
expose :source
expose :created_at, :updated_at
......
......@@ -2,7 +2,7 @@ module Keys
class BaseService
attr_accessor :user, :params
def initialize(user, params)
def initialize(user, params = {})
@user, @params = user, params
@ip_address = @params.delete(:ip_address)
end
......
module Keys
class DestroyService < ::Keys::BaseService
def execute(key)
key.destroy if destroy_possible?(key)
end
# overriden in EE::Keys::DestroyService
def destroy_possible?(key)
true
end
end
end
......@@ -2,14 +2,14 @@ module Lfs
class UnlockFileService < BaseService
def execute
unless can?(current_user, :push_code, project)
raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions'
raise Gitlab::GitAccess::UnauthorizedError, _('You have no permissions')
end
unlock_file
rescue Gitlab::GitAccess::UnauthorizedError => ex
error(ex.message, 403)
rescue ActiveRecord::RecordNotFound
error('Lock not found', 404)
error(_('Lock not found'), 404)
rescue => ex
error(ex.message, 500)
end
......@@ -24,9 +24,9 @@ module Lfs
success(lock: lock, http_status: :ok)
elsif forced
error('You must have master access to force delete a lock', 403)
error(_('You must have master access to force delete a lock'), 403)
else
error("#{lock.path} is locked by GitLab User #{lock.user_id}", 403)
error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403)
end
end
......
......@@ -5,6 +5,7 @@ module Milestones
def initialize(parent, user, params = {})
@parent, @current_user, @params = parent, user, params.dup
super
end
end
end
......@@ -121,7 +121,7 @@
%tr
%td.shortcut
.key g
.key e
.key v
%td
Go to the project's activity feed
%tr
......@@ -172,6 +172,18 @@
.key m
%td
Go to merge requests
%tr
%td.shortcut
.key g
.key e
%td
Go to environments
%tr
%td.shortcut
.key g
.key k
%td
Go to kubernetes
%tr
%td.shortcut
.key g
......
......@@ -212,7 +212,7 @@
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do
= link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-cluster' do
= link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%span
= _('Kubernetes')
- if show_cluster_hint
......
......@@ -4,7 +4,7 @@
by
= link_to member.created_by.name, user_url(member.created_by)
to join the
= link_to member_source.human_name, member_source.web_url
= link_to member_source.human_name, member_source.public? ? member_source.web_url : invite_url(@token)
#{member_source.model_name.singular} as #{member.human_access}.
%p
......
---
title: Add API endpoint to render markdown text
merge_request: 18926
author: "@blackst0ne"
type: added
---
title: Apply NestingDepth (level 5) (pages/pipelines.scss)
merge_request: 18830
author: Takuya Noguchi
type: other
---
title: Automatically accepts project/group invite by email after user signup
merge_request: 17634
author: Jacopo Beschi @jacopo-beschi
type: changed
---
title: Fix unscrollable Markdown preview of WebIDE on Firefox
merge_request:
author:
type: fixed
---
title: Allow CommitStatus class to use presentable methods
merge_request: 18979
author:
type: fixed
---
title: Fixes 500 error on /estimate BIG_VALUE
merge_request: 18964
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Adds keyboard shortcut `g e` for Environments on Project pages
merge_request: 19002
author:
type: added
---
title: Adds keyboard shortcut `g k` for Kubernetes on Project pages
merge_request: 19002
author:
type: added
---
title: Changes keyboard shortcut of Activity feed to `g v`
merge_request: 19002
author:
type: changed
---
title: Removes outdated `g t` shortcut for TODO in favor of `Shift+T`
merge_request: 19002
author:
type: removed
title: Add anchor for incoming email regex
merge_request: !18917
type: added
---
title: Add support for variables expression pattern matching syntax
merge_request: 18902
author:
type: added
---
title: Wrapping problem on the issues page has been fixed
merge_request:
author:
type: fixed
---
title: Do not allow to trigger manual actions that were skipped
merge_request: 18985
author:
type: fixed
---
title: Memoize Gitlab::Database.version
merge_request:
author:
type: performance
---
title: Improve performance of project pipelines pages
merge_request:
author:
type: performance
---
title: Fix api_json.log not always reporting the right HTTP status code
merge_request:
author:
type: fixed
---
title: Move API group deletion to Sidekiq
merge_request:
author:
type: changed
---
title: "Use case in-sensitive ordering by name for dashboard"
merge_request: 18553
author: "@vedharish"
type: fixed
---
title: Remove shellout implementation for Repository checksums
merge_request:
author:
type: other
if Gitlab.dev_env_or_com?
if Rails.env.development? || ENV['GITLAB_LEGACY_PATH_LOG_MESSAGE']
deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab')
deprecator.behavior = -> (message, callstack) {
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ProjectNameLowerIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
INDEX_NAME = 'index_projects_on_lower_name'
disable_ddl_transaction!
def up
return unless Gitlab::Database.postgresql?
disable_statement_timeout
execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON projects (LOWER(name))"
end
def down
return unless Gitlab::Database.postgresql?
disable_statement_timeout
if supports_drop_index_concurrently?
execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
else
execute "DROP INDEX IF EXISTS #{INDEX_NAME}"
end
end
end
......@@ -33,6 +33,7 @@ following locations:
- [Jobs](jobs.md)
- [Keys](keys.md)
- [Labels](labels.md)
- [Markdown](markdown.md)
- [Merge Requests](merge_requests.md)
- [Project milestones](milestones.md)
- [Group milestones](group_milestones.md)
......
......@@ -487,6 +487,9 @@ Parameters:
- `id` (required) - The ID or path of a user group
This will queue a background job to delete all projects in the group. The
response will be a 202 Accepted if the user has authorization.
## Search for group
Get all groups that match your string in their name or path.
......
# Markdown API
> [Introduced][ce-18926] in GitLab 11.0.
Available only in APIv4.
## Render an arbitrary Markdown document
```
POST /api/v4/markdown
```
| Attribute | Type | Required | Description |
| --------- | ------- | ------------- | ------------------------------------------ |
| `text` | string | yes | The markdown text to render |
| `gfm` | boolean | no (optional) | Render text using GitLab Flavored Markdown. Default is `false` |
| `project` | string | no (optional) | Use `project` as a context when creating references using GitLab Flavored Markdown. [Authentication](README.html#authentication) is required if a project is not public. |
```bash
curl --header Content-Type:application/json --data '{"text":"Hello world! :tada:", "gfm":true, "project":"group_example/project_example"}' https://gitlab.example.com/api/v4/markdown
```
Response example:
```json
{ "html": "<p dir=\"auto\">Hello world! <gl-emoji title=\"party popper\" data-name=\"tada\" data-unicode-version=\"6.0\">🎉</gl-emoji></p>" }
```
[ce-18926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18926
......@@ -530,6 +530,16 @@ Below you can find supported syntax reference:
`$STAGING` value needs to a string, with length higher than zero.
Variable that contains only whitespace characters is not an empty variable.
1. Pattern matching _(added in 11.0)_
> Example: `$VARIABLE =~ /^content.*/`
It is possible perform pattern matching against a variable and regular
expression. Expression like this evaluates to truth if matches are found.
Pattern matching is case-sensitive by default. Use `i` flag modifier, like
`/pattern/i` to make a pattern case-insensitive.
### Unsupported predefined variables
Because GitLab evaluates variables before creating jobs, we do not support a
......
......@@ -344,10 +344,11 @@ job:
kubernetes: active
```
Example of using variables expressions:
Examples of using variables expressions:
```yaml
deploy:
script: cap staging deploy
only:
refs:
- branches
......@@ -356,6 +357,16 @@ deploy:
- $STAGING
```
Another use case is exluding jobs depending on a commit message _(added in 11.0)_:
```yaml
end-to-end:
script: rake test:end-to-end
except:
variables:
- $CI_COMMIT_MESSAGE =~ /skip-end-to-end-tests/
```
Learn more about variables expressions on [a separate page][variables-expressions].
## `tags`
......
......@@ -37,12 +37,13 @@ import state from './state';
Vue.use(Vuex);
export default new Vuex.Store({
export const createStore = () => new Vuex.Store({
actions,
getters,
mutations,
state,
});
export default createStore();
```
### `state.js`
......@@ -320,10 +321,11 @@ In order to write unit tests for those components, we need to include the store
```javascript
//component_spec.js
import Vue from 'vue';
import store from './store';
import { createStore } from './store';
import component from './component.vue'
describe('component', () => {
let store;
let vm;
let Component;
......@@ -340,6 +342,8 @@ describe('component', () => {
name: 'Foo',
age: '30',
};
store = createStore();
// populate the store
store.dispatch('addUser', user);
......
......@@ -15,9 +15,9 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so
you should disable these mechanisms before downgrading and you should provide
alternative authentication methods to your users.
### Remove Jenkins CI Service entries from the database
### Remove Service Integration entries from the database
The `JenkinsService` class is only available on the Enterprise Edition codebase,
The `JenkinsService` and `GithubService` classes are only available in the Enterprise Edition codebase,
so if you downgrade to the Community Edition, you'll come across the following
error:
......@@ -30,20 +30,31 @@ column if you didn't intend it to be used for storing the inheritance class or o
use another column for that information.)
```
or
```
Completed 500 Internal Server Error in 497ms (ActiveRecord: 32.2ms)
ActionView::Template::Error (The single-table inheritance mechanism failed to locate the subclass: 'GithubService'. This
error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this
column if you didn't intend it to be used for storing the inheritance class or overwrite Service.inheritance_column to
use another column for that information.)
```
All services are created automatically for every project you have, so in order
to avoid getting this error, you need to remove all instances of the
`JenkinsService` from your database:
`JenkinsService` and `GithubService` from your database:
**Omnibus Installation**
```
$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all"
$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all"
```
**Source Installation**
```
$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" production
$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
```
### Secret variables environment scopes
......
......@@ -64,6 +64,13 @@ If you are running GitLab within a Docker container, you can run the backup from
docker exec -t <container name> gitlab-rake gitlab:backup:create
```
If you are using the gitlab-omnibus helm chart on a Kubernetes cluster, you can
run the backup task on the gitlab application pod using kubectl
```
kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:create
```
Example output:
```
......@@ -601,6 +608,34 @@ If there is a GitLab version mismatch between your backup tar file and the insta
version of GitLab, the restore command will abort with an error. Install the
[correct GitLab version](https://packages.gitlab.com/gitlab/) and try again.
### Restore for Docker image and gitlab-omnibus helm chart
For GitLab installations using docker image or the gitlab-omnibus helm chart on
a Kubernetes cluster, restore task expects the restore directories to be empty.
However, with docker and Kubernetes volume mounts, some system level directories
may be created at the volume roots, like `lost+found` directory found in Linux
operating systems. These directories are usually owned by `root`, which can
cause access permission errors since the restore rake task runs as `git` user.
So, to restore a GitLab installation, users have to confirm the restore target
directories are empty.
For both these installation types, the backup tarball has to be available in the
backup location (default location is `/var/opt/gitlab/backups`).
For docker installations, the restore task can be run from host using the
command
```
docker exec -it <name of container> gitlab-rake gitlab:backup:restore
```
Similarly, for gitlab-omnibus helm chart, the restore task can be run on the
gitlab application pod using kubectl
```
kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:restore
```
## Alternative backup strategies
If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow.
......
......@@ -23,6 +23,10 @@ page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the
**Fork** button. Soon you should have a project under your namespace with the
necessary files.
You can also start a new project from a
[GitLab project template](https://gitlab.com/gitlab-org/project-templates) if
you want to use a different language.
## Setup your own cluster on Google Kubernetes Engine
If you do not already have a Google Cloud account, create one at
......
......@@ -46,15 +46,19 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page |
| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed |
| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project's activity feed |
| <kbd>g</kbd> + <kbd>f</kbd> | Go to files |
| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
| <kbd>g</kbd> + <kbd>b</kbd> | Go to jobs |
| <kbd>g</kbd> + <kbd>j</kbd> | Go to jobs |
| <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph |
| <kbd>g</kbd> + <kbd>g</kbd> | Go to repository charts |
| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts |
| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
| <kbd>g</kbd> + <kbd>b</kbd> | Go to issue boards |
| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
| <kbd>g</kbd> + <kbd>e</kbd> | Go to environments |
| <kbd>g</kbd> + <kbd>k</kbd> | Go to kubernetes |
| <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets |
| <kbd>g</kbd> + <kbd>w</kbd> | Go to wiki |
| <kbd>t</kbd> | Go to finding file |
| <kbd>i</kbd> | New issue |
......
......@@ -8,14 +8,15 @@ module API
PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
use GrapeLogging::Middleware::RequestLogger,
logger: Logger.new(LOG_FILENAME),
formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
include: [
GrapeLogging::Loggers::FilterParameters.new,
GrapeLogging::Loggers::ClientEnv.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new
]
insert_before Grape::Middleware::Error,
GrapeLogging::Middleware::RequestLogger,
logger: Logger.new(LOG_FILENAME),
formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
include: [
GrapeLogging::Loggers::FilterParameters.new,
GrapeLogging::Loggers::ClientEnv.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new
]
allow_access_with_scope :api
prefix :api
......@@ -139,6 +140,7 @@ module API
mount ::API::Keys
mount ::API::Labels
mount ::API::Lint
mount ::API::Markdown
mount ::API::Members
mount ::API::MergeRequestDiffs
mount ::API::MergeRequests
......
......@@ -167,8 +167,10 @@ module API
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
destroy_conditionally!(group) do |group|
::Groups::DestroyService.new(group, current_user).execute
::Groups::DestroyService.new(group, current_user).async_execute
end
accepted!
end
desc 'Get a list of projects in this group.' do
......
module API
class Markdown < Grape::API
params do
requires :text, type: String, desc: "The markdown text to render"
optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown"
optional :project, type: String, desc: "The full path of a project to use as the context when creating references using GitLab Flavored Markdown"
end
resource :markdown do
desc "Render markdown text" do
detail "This feature was introduced in GitLab 11.0."
end
post do
# Explicitly set CommonMark as markdown engine to use.
# Remove this set when https://gitlab.com/gitlab-org/gitlab-ce/issues/43011 is done.
context = { markdown_engine: :common_mark, only_path: false }
if params[:project]
project = Project.find_by_full_path(params[:project])
not_found!("Project") unless can?(current_user, :read_project, project)
context[:project] = project
else
context[:skip_project_check] = true
end
context[:pipeline] = params[:gfm] ? :full : :plain_markdown
{ html: Banzai.render(params[:text], context) }
end
end
end
end
......@@ -131,8 +131,9 @@ module API
delete ":id" do
group = find_group!(params[:id])
authorize! :admin_group, group
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
::Groups::DestroyService.new(group, current_user).async_execute
accepted!
end
desc 'Get a list of projects in this group.' do
......
......@@ -73,7 +73,7 @@ module Banzai
#
# Note that while the key might exist, its value could be nil!
def validate
needs :project
needs :project unless skip_project_check?
end
# Iterates over all <a> and text() nodes in a document.
......
......@@ -42,9 +42,9 @@ module Banzai
end
def self.transform_context(context)
context.merge(
only_path: true,
context[:only_path] = true unless context.key?(:only_path)
context.merge(
# EmojiFilter
asset_host: Gitlab::Application.config.asset_host,
asset_root: Gitlab.config.gitlab.base_url
......
module Gitlab
module Ci
module Pipeline
module Expression
ExpressionError = Class.new(StandardError)
RuntimeError = Class.new(ExpressionError)
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
module Lexeme
class Matches < Lexeme::Operator
PATTERN = /=~/.freeze
def initialize(left, right)
@left = left
@right = right
end
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
regexp.scan(text.to_s).any?
end
def self.build(_value, behind, ahead)
new(behind, ahead)
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
module Lexeme
require_dependency 're2'
class Pattern < Lexeme::Value
PATTERN = %r{^/.+/[ismU]*$}.freeze
def initialize(regexp)
@value = regexp
unless Gitlab::UntrustedRegexp.valid?(@value)
raise Lexer::SyntaxError, 'Invalid regular expression!'
end
end
def evaluate(variables = {})
Gitlab::UntrustedRegexp.fabricate(@value)
rescue RegexpError
raise Expression::RuntimeError, 'Invalid regular expression!'
end
def self.build(string)
new(string)
end
end
end
end
end
end
end
......@@ -5,15 +5,17 @@ module Gitlab
class Lexer
include ::Gitlab::Utils::StrongMemoize
SyntaxError = Class.new(Expression::ExpressionError)
LEXEMES = [
Expression::Lexeme::Variable,
Expression::Lexeme::String,
Expression::Lexeme::Pattern,
Expression::Lexeme::Null,
Expression::Lexeme::Equals
Expression::Lexeme::Equals,
Expression::Lexeme::Matches
].freeze
SyntaxError = Class.new(Statement::StatementError)
MAX_TOKENS = 100
def initialize(statement, max_tokens: MAX_TOKENS)
......
......@@ -3,15 +3,16 @@ module Gitlab
module Pipeline
module Expression
class Statement
StatementError = Class.new(StandardError)
StatementError = Class.new(Expression::ExpressionError)
GRAMMAR = [
%w[variable],
%w[variable equals string],
%w[variable equals variable],
%w[variable equals null],
%w[string equals variable],
%w[null equals variable],
%w[variable]
%w[variable matches pattern]
].freeze
def initialize(statement, variables = {})
......@@ -35,11 +36,13 @@ module Gitlab
def truthful?
evaluate.present?
rescue Expression::ExpressionError
false
end
def valid?
parse_tree.is_a?(Lexeme::Base)
rescue StatementError
rescue Expression::ExpressionError
false
end
end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
# Class for preloading data associated with pipelines such as commit
# authors.
module Preloader
def self.preload(pipelines)
# This ensures that all the pipeline commits are eager loaded before we
# start using them.
pipelines.each(&:commit)
pipelines.each do |pipeline|
# This preloads the author of every commit. We're using "lazy_author"
# here since "author" immediately loads the data on the first call.
pipeline.commit.try(:lazy_author)
# This preloads the number of warnings for every pipeline, ensuring
# that Ci::Pipeline#has_warnings? doesn't execute any additional
# queries.
pipeline.number_of_warnings
end
end
end
end
end
end
......@@ -43,7 +43,7 @@ module Gitlab
end
def self.version
database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
@version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
def self.join_lateral_supported?
......
......@@ -1572,14 +1572,12 @@ module Gitlab
end
def checksum
gitaly_migrate(:calculate_checksum,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.calculate_checksum
else
calculate_checksum_by_shelling_out
end
end
# The exists? RPC is much cheaper, so we perform this request first
raise NoRepository, "Repository does not exists" unless exists?
gitaly_repository_client.calculate_checksum
rescue GRPC::NotFound
raise NoRepository # Guard against data races.
end
private
......@@ -2478,36 +2476,6 @@ module Gitlab
rev_parse_target(ref).oid
end
def calculate_checksum_by_shelling_out
raise NoRepository unless exists?
args = %W(--git-dir=#{path} show-ref --heads --tags)
output, status = run_git(args)
if status.nil? || !status.zero?
# Non-valid git repositories return 128 as the status code and an error output
raise InvalidRepository if status == 128 && output.to_s.downcase =~ /not a git repository/
# Empty repositories returns with a non-zero status and an empty output.
raise ChecksumError, output unless output.blank?
return EMPTY_REPOSITORY_CHECKSUM
end
refs = output.split("\n")
result = refs.inject(nil) do |checksum, ref|
value = Digest::SHA1.hexdigest(ref).hex
if checksum.nil?
value
else
checksum ^ value
end
end
result.to_s(16)
end
def build_git_cmd(*args)
object_directories = alternate_object_directories.join(File::PATH_SEPARATOR)
......
......@@ -57,7 +57,7 @@ module Gitlab
regex = Regexp.escape(wildcard_address)
regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)')
Regexp.new(regex).freeze
Regexp.new(/\A#{regex}\z/).freeze
end
end
end
......
......@@ -9,7 +9,9 @@ module Gitlab
# there is a strict limit on total execution time. See the RE2 documentation
# at https://github.com/google/re2/wiki/Syntax for more details.
class UntrustedRegexp
delegate :===, to: :regexp
require_dependency 're2'
delegate :===, :source, to: :regexp
def initialize(pattern, multiline: false)
if multiline
......@@ -35,6 +37,10 @@ module Gitlab
RE2.Replace(text, regexp, rewrite)
end
def ==(other)
self.source == other.source
end
# Handles regular expressions with the preferred RE2 library where possible
# via UntustedRegex. Falls back to Ruby's built-in regular expression library
# when the syntax would be invalid in RE2.
......@@ -48,6 +54,24 @@ module Gitlab
Regexp.new(pattern)
end
def self.valid?(pattern)
!!self.fabricate(pattern)
rescue RegexpError
false
end
def self.fabricate(pattern)
matches = pattern.match(%r{^/(?<regexp>.+)/(?<flags>[ismU]*)$})
raise RegexpError, 'Invalid regular expression!' if matches.nil?
expression = matches[:regexp]
flags = matches[:flags]
expression.prepend("(?#{flags})") if flags.present?
self.new(expression, multiline: false)
end
private
attr_reader :regexp
......
......@@ -8,6 +8,7 @@ task setup_postgresql: :environment do
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
require Rails.root.join('db/migrate/20180215181245_users_name_lower_index.rb')
require Rails.root.join('db/migrate/20180504195842_project_name_lower_index.rb')
require Rails.root.join('db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb')
NamespacesProjectsPathLowerIndexes.new.up
......@@ -18,5 +19,6 @@ task setup_postgresql: :environment do
IndexRedirectRoutesPathForLike.new.up
AddIndexOnNamespacesLowerName.new.up
UsersNameLowerIndex.new.up
ProjectNameLowerIndex.new.up
AddPathIndexToRedirectRoutes.new.up
end
......@@ -11,7 +11,7 @@ module QA
expect(page).to have_content('This is a merge request')
expect(page).to have_content('Great feature')
expect(page).to have_content('Opened less than a minute ago')
expect(page).to have_content(/Opened [\w\s]+ a minute ago/)
end
end
end
#!/bin/bash
mysql --user=root --host=mysql <<EOF
CREATE DATABASE IF NOT EXISTS gitlabhq_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'gitlab'@'%';
GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
FLUSH PRIVILEGES;
......
#!/bin/bash
psql -h postgres -U postgres postgres <<EOF
DROP DATABASE IF EXISTS gitlabhq_test;
CREATE DATABASE gitlabhq_test;
CREATE USER gitlab;
GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gitlab;
EOF
......@@ -49,20 +49,8 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml
cp config/redis.shared_state.yml.example config/redis.shared_state.yml
sed -i 's/localhost/redis/g' config/redis.shared_state.yml
# Some tasks (e.g. db:seed_fu) need to have a properly-configured database
# user but not necessarily a full schema loaded
if [ "$CREATE_DB_USER" != "false" ]; then
if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
. scripts/create_postgres_user.sh
else
. scripts/create_mysql_user.sh
fi
fi
if [ "$SETUP_DB" != "false" ]; then
bundle exec rake db:drop db:create db:schema:load db:migrate
if [ "$GITLAB_DATABASE" = "mysql" ]; then
bundle exec rake add_limits_mysql
fi
setup_db
elif getent hosts postgres || getent hosts mysql; then
setup_db_user_only
fi
......@@ -12,3 +12,21 @@ retry() {
done
return 1
}
setup_db_user_only() {
if [ "$GITLAB_DATABASE" = "postgresql" ]; then
. scripts/create_postgres_user.sh
else
. scripts/create_mysql_user.sh
fi
}
setup_db() {
setup_db_user_only
bundle exec rake db:drop db:create db:schema:load db:migrate
if [ "$GITLAB_DATABASE" = "mysql" ]; then
bundle exec rake add_limits_mysql
fi
}
......@@ -35,10 +35,16 @@ describe Projects::PipelinesController do
expect(json_response).to include('pipelines')
expect(json_response['pipelines'].count).to eq 4
expect(json_response['count']['all']).to eq 4
expect(json_response['count']['running']).to eq 1
expect(json_response['count']['pending']).to eq 1
expect(json_response['count']['finished']).to eq 1
expect(json_response['count']['all']).to eq '4'
expect(json_response['count']['running']).to eq '1'
expect(json_response['count']['pending']).to eq '1'
expect(json_response['count']['finished']).to eq '1'
end
it 'does not include coverage data for the pipelines' do
subject
expect(json_response['pipelines'][0]).not_to include('coverage')
end
context 'when performing gitaly calls', :request_store do
......
......@@ -5,18 +5,41 @@ describe 'Invites' do
let(:owner) { create(:user, name: 'John Doe') }
let(:group) { create(:group, name: 'Owned') }
let(:project) { create(:project, :repository, namespace: group) }
let(:invite) { group.group_members.invite.last }
let(:group_invite) { group.group_members.invite.last }
before do
project.add_master(owner)
group.add_user(owner, Gitlab::Access::OWNER)
group.add_developer('user@example.com', owner)
invite.generate_invite_token!
group_invite.generate_invite_token!
end
def confirm_email_and_sign_in(new_user)
new_user_token = User.find_by_email(new_user.email).confirmation_token
visit user_confirmation_path(confirmation_token: new_user_token)
fill_in_sign_in_form(new_user)
end
def fill_in_sign_up_form(new_user)
fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
fill_in 'new_user_email_confirmation', with: new_user.email
fill_in 'new_user_password', with: new_user.password
click_button "Register"
end
def fill_in_sign_in_form(user)
fill_in 'user_login', with: user.email
fill_in 'user_password', with: user.password
check 'user_remember_me'
click_button 'Sign in'
end
context 'when signed out' do
before do
visit invite_path(invite.raw_invite_token)
visit invite_path(group_invite.raw_invite_token)
end
it 'renders sign in page with sign in notice' do
......@@ -25,12 +48,9 @@ describe 'Invites' do
end
it 'sign in and redirects to invitation page' do
fill_in 'user_login', with: user.email
fill_in 'user_password', with: user.password
check 'user_remember_me'
click_button 'Sign in'
fill_in_sign_in_form(user)
expect(current_path).to eq(invite_path(invite.raw_invite_token))
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(page).to have_content(
'You have been invited by John Doe to join group Owned as Developer.'
)
......@@ -45,7 +65,7 @@ describe 'Invites' do
end
it 'shows message user already a member' do
visit invite_path(invite.raw_invite_token)
visit invite_path(group_invite.raw_invite_token)
expect(page).to have_content('However, you are already a member of this group.')
end
end
......@@ -53,7 +73,7 @@ describe 'Invites' do
describe 'accepting the invitation' do
before do
sign_in(user)
visit invite_path(invite.raw_invite_token)
visit invite_path(group_invite.raw_invite_token)
end
it 'grants access and redirects to group page' do
......@@ -69,7 +89,7 @@ describe 'Invites' do
context 'when signed in' do
before do
sign_in(user)
visit invite_path(invite.raw_invite_token)
visit invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to dashboard' do
......@@ -83,7 +103,7 @@ describe 'Invites' do
context 'when signed out' do
before do
visit decline_invite_path(invite.raw_invite_token)
visit decline_invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to sign in page' do
......@@ -94,4 +114,72 @@ describe 'Invites' do
end
end
end
describe 'invite an user using their email address' do
let(:new_user) { build_stubbed(:user) }
let(:invite_email) { new_user.email }
let(:group_invite) { create(:group_member, :invited, group: group, invite_email: invite_email) }
let!(:project_invite) { create(:project_member, :invited, project: project, invite_email: invite_email) }
before do
stub_application_setting(send_user_confirmation_email: send_email_confirmation)
visit invite_path(group_invite.raw_invite_token)
end
context 'email confirmation disabled' do
let(:send_email_confirmation) { false }
it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do
fill_in_sign_up_form(new_user)
expect(current_path).to eq(dashboard_projects_path)
expect(page).to have_content(project.full_name)
visit group_path(group)
expect(page).to have_content(group.full_name)
end
context 'the user sign-up using a different email address' do
let(:invite_email) { build_stubbed(:user).email }
it 'signs up and redirects to the invitation page' do
fill_in_sign_up_form(new_user)
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
end
end
end
context 'email confirmation enabled' do
let(:send_email_confirmation) { true }
it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
fill_in_sign_up_form(new_user)
confirm_email_and_sign_in(new_user)
expect(current_path).to eq(root_path)
expect(page).to have_content(project.full_name)
visit group_path(group)
expect(page).to have_content(group.full_name)
end
it "doesn't accept invitations until the user confirm his email" do
fill_in_sign_up_form(new_user)
sign_in(owner)
visit project_project_members_path(project)
expect(page).to have_content 'Invited'
end
context 'the user sign-up using a different email address' do
let(:invite_email) { build_stubbed(:user).email }
it 'signs up and redirects to the invitation page' do
fill_in_sign_up_form(new_user)
confirm_email_and_sign_in(new_user)
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
end
end
end
end
end
......@@ -13,6 +13,8 @@ describe 'User uses shortcuts', :js do
context 'when navigating to the Project pages' do
it 'redirects to the details page' do
visit project_issues_path(project)
find('body').native.send_key('g')
find('body').native.send_key('p')
......@@ -22,7 +24,7 @@ describe 'User uses shortcuts', :js do
it 'redirects to the activity page' do
find('body').native.send_key('g')
find('body').native.send_key('e')
find('body').native.send_key('v')
expect(page).to have_active_navigation('Project')
expect(page).to have_active_sub_navigation('Activity')
......@@ -72,10 +74,19 @@ describe 'User uses shortcuts', :js do
expect(page).to have_active_sub_navigation('List')
end
it 'redirects to the issue board page' do
find('body').native.send_key('g')
find('body').native.send_key('b')
expect(page).to have_active_navigation('Issues')
expect(page).to have_active_sub_navigation('Board')
end
it 'redirects to the new issue page' do
find('body').native.send_key('i')
expect(page).to have_content(project.title)
expect(page).to have_content('New Issue')
end
end
......@@ -88,6 +99,34 @@ describe 'User uses shortcuts', :js do
end
end
context 'when navigating to the CI / CD pages' do
it 'redirects to the Jobs page' do
find('body').native.send_key('g')
find('body').native.send_key('j')
expect(page).to have_active_navigation('CI / CD')
expect(page).to have_active_sub_navigation('Jobs')
end
end
context 'when navigating to the Operations pages' do
it 'redirects to the Environments page' do
find('body').native.send_key('g')
find('body').native.send_key('e')
expect(page).to have_active_navigation('Operations')
expect(page).to have_active_sub_navigation('Environments')
end
it 'redirects to the Kubernetes page' do
find('body').native.send_key('g')
find('body').native.send_key('k')
expect(page).to have_active_navigation('Operations')
expect(page).to have_active_sub_navigation('Kubernetes')
end
end
context 'when navigating to the Snippets pages' do
it 'redirects to the snippets page' do
find('body').native.send_key('g')
......
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import actionComponent from '~/pipelines/components/graph/action_component.vue';
import eventHub from '~/pipelines/event_hub';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('pipeline graph action component', () => {
let component;
let mock;
beforeEach(done => {
const ActionComponent = Vue.extend(actionComponent);
mock = new MockAdapter(axios);
mock.onPost('foo.json').reply(200);
component = mountComponent(ActionComponent, {
tooltipText: 'bar',
link: 'foo',
......@@ -18,15 +24,10 @@ describe('pipeline graph action component', () => {
});
afterEach(() => {
mock.restore();
component.$destroy();
});
it('should emit an event with the provided link', () => {
eventHub.$on('postAction', link => {
expect(link).toEqual('foo');
});
});
it('should render the provided title as a bootstrap tooltip', () => {
expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
});
......@@ -34,10 +35,12 @@ describe('pipeline graph action component', () => {
it('should update bootstrap tooltip when title changes', done => {
component.tooltipText = 'changed';
setTimeout(() => {
component.$nextTick()
.then(() => {
expect(component.$el.getAttribute('data-original-title')).toBe('changed');
done();
});
})
.then(done)
.catch(done.fail);
});
it('should render an svg', () => {
......@@ -45,44 +48,18 @@ describe('pipeline graph action component', () => {
expect(component.$el.querySelector('svg')).toBeDefined();
});
it('disables the button when clicked', done => {
component.$el.click();
describe('on click', () => {
it('emits `pipelineActionRequestComplete` after a successfull request', done => {
spyOn(component, '$emit');
component.$nextTick(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
done();
});
});
it('re-enabled the button when `requestFinishedFor` matches `linkRequested`', done => {
component.$el.click();
component
.$nextTick()
.then(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
component.requestFinishedFor = 'foo';
})
.then(() => {
expect(component.$el.getAttribute('disabled')).toBeNull();
})
.then(done)
.catch(done.fail);
});
it('does not re-enable the button when `requestFinishedFor` does not matches `linkRequested`', done => {
component.$el.click();
component.$el.click();
component
.$nextTick()
.then(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
component.requestFinishedFor = 'bar';
})
.then(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
})
.then(done)
.catch(done.fail);
component.$nextTick()
.then(() => {
expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
})
.then(done)
.catch(done.fail);
});
});
});
......@@ -102,4 +102,31 @@ describe('Pipelines stage component', () => {
});
});
});
describe('pipelineActionRequestComplete', () => {
beforeEach(() => {
mock.onGet('path.json').reply(200, stageReply);
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
});
describe('within pipeline table', () => {
it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', done => {
spyOn(eventHub, '$emit');
component.type = 'PIPELINES_TABLE';
component.$el.querySelector('button').click();
setTimeout(() => {
component.$el.querySelector('.js-ci-action').click();
component.$nextTick()
.then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
})
.then(done)
.catch(done.fail);
}, 0);
});
});
});
});
......@@ -111,7 +111,15 @@ describe Gitlab::Ci::Config::Entry::Policy do
context 'when specifying invalid variables expressions token' do
let(:config) { { variables: ['$MY_VAR == 123'] } }
it 'reports an error about invalid statement' do
it 'reports an error about invalid expression' do
expect(entry.errors).to include /invalid expression syntax/
end
end
context 'when using invalid variables expressions regexp' do
let(:config) { { variables: ['$MY_VAR =~ /some ( thing/'] } }
it 'reports an error about invalid expression' do
expect(entry.errors).to include /invalid expression syntax/
end
end
......
require 'fast_spec_helper'
require_dependency 're2'
describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
let(:left) { double('left') }
let(:right) { double('right') }
describe '.build' do
it 'creates a new instance of the token' do
expect(described_class.build('=~', left, right))
.to be_a(described_class)
end
end
describe '.type' do
it 'is an operator' do
expect(described_class.type).to eq :operator
end
end
describe '#evaluate' do
it 'returns false when left and right do not match' do
allow(left).to receive(:evaluate).and_return('my-string')
allow(right).to receive(:evaluate)
.and_return(Gitlab::UntrustedRegexp.new('something'))
operator = described_class.new(left, right)
expect(operator.evaluate).to eq false
end
it 'returns true when left and right match' do
allow(left).to receive(:evaluate).and_return('my-awesome-string')
allow(right).to receive(:evaluate)
.and_return(Gitlab::UntrustedRegexp.new('awesome.string$'))
operator = described_class.new(left, right)
expect(operator.evaluate).to eq true
end
it 'supports matching against a nil value' do
allow(left).to receive(:evaluate).and_return(nil)
allow(right).to receive(:evaluate)
.and_return(Gitlab::UntrustedRegexp.new('pattern'))
operator = described_class.new(left, right)
expect(operator.evaluate).to eq false
end
it 'supports multiline strings' do
allow(left).to receive(:evaluate).and_return <<~TEXT
My awesome contents
My-text-string!
TEXT
allow(right).to receive(:evaluate)
.and_return(Gitlab::UntrustedRegexp.new('text-string'))
operator = described_class.new(left, right)
expect(operator.evaluate).to eq true
end
it 'supports regexp flags' do
allow(left).to receive(:evaluate).and_return <<~TEXT
My AWESOME content
TEXT
allow(right).to receive(:evaluate)
.and_return(Gitlab::UntrustedRegexp.new('(?i)awesome'))
operator = described_class.new(left, right)
expect(operator.evaluate).to eq true
end
end
end
require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
describe '.build' do
it 'creates a new instance of the token' do
expect(described_class.build('/.*/'))
.to be_a(described_class)
end
it 'raises an error if pattern is invalid' do
expect { described_class.build('/ some ( thin/i') }
.to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError)
end
end
describe '.type' do
it 'is a value lexeme' do
expect(described_class.type).to eq :value
end
end
describe '.scan' do
it 'correctly identifies a pattern token' do
scanner = StringScanner.new('/pattern/')
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate)
.to eq Gitlab::UntrustedRegexp.new('pattern')
end
it 'is a greedy scanner for regexp boundaries' do
scanner = StringScanner.new('/some .* / pattern/')
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate)
.to eq Gitlab::UntrustedRegexp.new('some .* / pattern')
end
it 'does not allow to use an empty pattern' do
scanner = StringScanner.new(%(//))
token = described_class.scan(scanner)
expect(token).to be_nil
end
it 'support single flag' do
scanner = StringScanner.new('/pattern/i')
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate)
.to eq Gitlab::UntrustedRegexp.new('(?i)pattern')
end
it 'support multiple flags' do
scanner = StringScanner.new('/pattern/im')
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate)
.to eq Gitlab::UntrustedRegexp.new('(?im)pattern')
end
it 'does not support arbitrary flags' do
scanner = StringScanner.new('/pattern/x')
token = described_class.scan(scanner)
expect(token).to be_nil
end
end
describe '#evaluate' do
it 'returns a regular expression' do
regexp = described_class.new('/abc/')
expect(regexp.evaluate).to eq Gitlab::UntrustedRegexp.new('abc')
end
it 'raises error if evaluated regexp is not valid' do
allow(Gitlab::UntrustedRegexp).to receive(:valid?).and_return(true)
regexp = described_class.new('/invalid ( .*/')
expect { regexp.evaluate }
.to raise_error(Gitlab::Ci::Pipeline::Expression::RuntimeError)
end
end
end
......@@ -6,7 +6,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
end
describe '#tokens' do
it 'tokenss single value' do
it 'returns single value' do
tokens = described_class.new('$VARIABLE').tokens
expect(tokens).to be_one
......@@ -20,14 +20,14 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect(tokens).to all(be_an_instance_of(token_class))
end
it 'tokenss multiple values of the same token' do
it 'returns multiple values of the same token' do
tokens = described_class.new("$VARIABLE1 $VARIABLE2").tokens
expect(tokens.size).to eq 2
expect(tokens).to all(be_an_instance_of(token_class))
end
it 'tokenss multiple values with different tokens' do
it 'returns multiple values with different tokens' do
tokens = described_class.new('$VARIABLE "text" "value"').tokens
expect(tokens.size).to eq 3
......@@ -36,7 +36,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect(tokens.third.value).to eq '"value"'
end
it 'tokenss tokens and operators' do
it 'returns tokens and operators' do
tokens = described_class.new('$VARIABLE == "text"').tokens
expect(tokens.size).to eq 3
......
require 'spec_helper'
require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Parser do
describe '#tree' do
......
require 'spec_helper'
require 'fast_spec_helper'
require 'rspec-parameterized'
describe Gitlab::Ci::Pipeline::Expression::Statement do
subject do
......@@ -36,7 +37,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
'== "123"', # invalid left side
'"some string"', # only string provided
'$VAR ==', # invalid right side
'12345', # unknown syntax
'null', # missing lexemes
'' # empty statement
]
......@@ -44,7 +45,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
context "when expression grammar is #{syntax.inspect}" do
let(:text) { syntax }
it 'aises a statement error exception' do
it 'raises a statement error exception' do
expect { subject.parse_tree }
.to raise_error described_class::StatementError
end
......@@ -82,48 +83,66 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end
describe '#evaluate' do
statements = [
['$PRESENT_VARIABLE == "my variable"', true],
["$PRESENT_VARIABLE == 'my variable'", true],
['"my variable" == $PRESENT_VARIABLE', true],
['$PRESENT_VARIABLE == null', false],
['$EMPTY_VARIABLE == null', false],
['"" == $EMPTY_VARIABLE', true],
['$EMPTY_VARIABLE', ''],
['$UNDEFINED_VARIABLE == null', true],
['null == $UNDEFINED_VARIABLE', true],
['$PRESENT_VARIABLE', 'my variable'],
['$UNDEFINED_VARIABLE', nil]
]
statements.each do |expression, value|
context "when using expression `#{expression}`" do
let(:text) { expression }
it "evaluates to `#{value.inspect}`" do
expect(subject.evaluate).to eq value
end
using RSpec::Parameterized::TableSyntax
where(:expression, :value) do
'$PRESENT_VARIABLE == "my variable"' | true
'"my variable" == $PRESENT_VARIABLE' | true
'$PRESENT_VARIABLE == null' | false
'$EMPTY_VARIABLE == null' | false
'"" == $EMPTY_VARIABLE' | true
'$EMPTY_VARIABLE' | ''
'$UNDEFINED_VARIABLE == null' | true
'null == $UNDEFINED_VARIABLE' | true
'$PRESENT_VARIABLE' | 'my variable'
'$UNDEFINED_VARIABLE' | nil
"$PRESENT_VARIABLE =~ /var.*e$/" | true
"$PRESENT_VARIABLE =~ /^var.*/" | false
"$EMPTY_VARIABLE =~ /var.*/" | false
"$UNDEFINED_VARIABLE =~ /var.*/" | false
"$PRESENT_VARIABLE =~ /VAR.*/i" | true
end
with_them do
let(:text) { expression }
it "evaluates to `#{params[:value].inspect}`" do
expect(subject.evaluate).to eq value
end
end
end
describe '#truthful?' do
statements = [
['$PRESENT_VARIABLE == "my variable"', true],
["$PRESENT_VARIABLE == 'no match'", false],
['$UNDEFINED_VARIABLE == null', true],
['$PRESENT_VARIABLE', true],
['$UNDEFINED_VARIABLE', false],
['$EMPTY_VARIABLE', false]
]
statements.each do |expression, value|
context "when using expression `#{expression}`" do
let(:text) { expression }
it "returns `#{value.inspect}`" do
expect(subject.truthful?).to eq value
end
using RSpec::Parameterized::TableSyntax
where(:expression, :value) do
'$PRESENT_VARIABLE == "my variable"' | true
"$PRESENT_VARIABLE == 'no match'" | false
'$UNDEFINED_VARIABLE == null' | true
'$PRESENT_VARIABLE' | true
'$UNDEFINED_VARIABLE' | false
'$EMPTY_VARIABLE' | false
'$INVALID = 1' | false
"$PRESENT_VARIABLE =~ /var.*/" | true
"$UNDEFINED_VARIABLE =~ /var.*/" | false
end
with_them do
let(:text) { expression }
it "returns `#{params[:value].inspect}`" do
expect(subject.truthful?).to eq value
end
end
context 'when evaluating expression raises an error' do
let(:text) { '$PRESENT_VARIABLE' }
it 'returns false' do
allow(subject).to receive(:evaluate)
.and_raise(described_class::StatementError)
expect(subject.truthful?).to be_falsey
end
end
end
......
require 'spec_helper'
require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Token do
let(:value) { '$VARIABLE' }
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Preloader do
describe '.preload' do
it 'preloads the author of every pipeline commit' do
commit = double(:commit)
pipeline = double(:pipeline, commit: commit)
expect(commit)
.to receive(:lazy_author)
expect(pipeline)
.to receive(:number_of_warnings)
described_class.preload([pipeline])
end
end
end
......@@ -32,6 +32,12 @@ describe Gitlab::Database do
end
describe '.version' do
around do |example|
described_class.instance_variable_set(:@version, nil)
example.run
described_class.instance_variable_set(:@version, nil)
end
context "on mysql" do
it "extracts the version number" do
allow(described_class).to receive(:database_version)
......@@ -49,6 +55,14 @@ describe Gitlab::Database do
expect(described_class.version).to eq '9.4.4'
end
end
it 'memoizes the result' do
count = ActiveRecord::QueryRecorder
.new { 2.times { described_class.version } }
.count
expect(count).to eq(1)
end
end
describe '.join_lateral_supported?' do
......
......@@ -2247,66 +2247,42 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#checksum' do
shared_examples 'calculating checksum' do
it 'calculates the checksum for non-empty repo' do
expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4'
end
it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git'))
system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
chdir: storage_path,
out: '/dev/null',
err: '/dev/null')
it 'calculates the checksum for non-empty repo' do
expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4'
end
empty_repo = described_class.new('default', 'empty-repo.git', '')
it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git'))
expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
end
system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
chdir: storage_path,
out: '/dev/null',
err: '/dev/null')
it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do
FileUtils.rm_rf(File.join(storage_path, 'non-valid.git'))
empty_repo = described_class.new('default', 'empty-repo.git', '')
system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git),
chdir: SEED_STORAGE_PATH,
out: '/dev/null',
err: '/dev/null')
expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
end
File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0)
it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do
FileUtils.rm_rf(File.join(storage_path, 'non-valid.git'))
non_valid = described_class.new('default', 'non-valid.git', '')
system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git),
chdir: SEED_STORAGE_PATH,
out: '/dev/null',
err: '/dev/null')
expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
end
File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0)
it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do
broken_repo = described_class.new('default', 'a/path.git', '')
non_valid = described_class.new('default', 'non-valid.git', '')
expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
end
expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
end
context 'when calculate_checksum Gitaly feature is enabled' do
it_behaves_like 'calculating checksum'
end
context 'when calculate_checksum Gitaly feature is disabled', :disable_gitaly do
it_behaves_like 'calculating checksum'
describe 'when storage is broken', :broken_storage do
it 'raises a storage exception when storage is not available' do
broken_repo = described_class.new('broken', 'a/path.git', '')
expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible)
end
end
it "raises a Gitlab::Git::Repository::Failure error if the `popen` call to git returns a non-zero exit code" do
allow(repository).to receive(:popen).and_return(['output', nil])
it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do
broken_repo = described_class.new('default', 'a/path.git', '')
expect { repository.checksum }.to raise_error Gitlab::Git::Repository::ChecksumError
end
expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
end
end
......
......@@ -83,6 +83,10 @@ describe Gitlab::IncomingEmail do
it "returns reply key" do
expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
end
it 'does not match emails with extra bits' do
expect(described_class.key_from_address('somereplies+somekey@example.com.someotherdomain.com')).to be nil
end
end
context 'self.key_from_fallback_message_id' do
......
require 'spec_helper'
require 'fast_spec_helper'
require 'support/shared_examples/malicious_regexp_shared_examples'
describe Gitlab::UntrustedRegexp do
describe '.valid?' do
it 'returns true if regexp is valid' do
expect(described_class.valid?('/some ( thing/'))
.to be false
end
it 'returns true if regexp is invalid' do
expect(described_class.valid?('/some .* thing/'))
.to be true
end
end
describe '.fabricate' do
context 'when regexp is using /regexp/ scheme with flags' do
it 'fabricates regexp with a single flag' do
regexp = described_class.fabricate('/something/i')
expect(regexp).to eq described_class.new('(?i)something')
expect(regexp.scan('SOMETHING')).to be_one
end
it 'fabricates regexp with multiple flags' do
regexp = described_class.fabricate('/something/im')
expect(regexp).to eq described_class.new('(?im)something')
end
it 'fabricates regexp without flags' do
regexp = described_class.fabricate('/something/')
expect(regexp).to eq described_class.new('something')
end
end
context 'when regexp is a raw pattern' do
it 'raises an error' do
expect { described_class.fabricate('some .* thing') }
.to raise_error(RegexpError)
end
end
end
describe '#initialize' do
subject { described_class.new(pattern) }
......
......@@ -594,7 +594,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{project.full_name} project"
is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project.full_name
is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text project_member.invite_token
end
......
......@@ -1270,6 +1270,46 @@ describe Ci::Build do
end
end
describe '#playable?' do
context 'when build is a manual action' do
context 'when build has been skipped' do
subject { build_stubbed(:ci_build, :manual, status: :skipped) }
it { is_expected.not_to be_playable }
end
context 'when build has been canceled' do
subject { build_stubbed(:ci_build, :manual, status: :canceled) }
it { is_expected.to be_playable }
end
context 'when build is successful' do
subject { build_stubbed(:ci_build, :manual, status: :success) }
it { is_expected.to be_playable }
end
context 'when build has failed' do
subject { build_stubbed(:ci_build, :manual, status: :failed) }
it { is_expected.to be_playable }
end
context 'when build is a manual untriggered action' do
subject { build_stubbed(:ci_build, :manual, status: :manual) }
it { is_expected.to be_playable }
end
end
context 'when build is not a manual action' do
subject { build_stubbed(:ci_build, :success) }
it { is_expected.not_to be_playable }
end
end
describe 'project settings' do
describe '#allow_git_fetch' do
it 'return project allow_git_fetch configuration' do
......
......@@ -774,6 +774,33 @@ describe Ci::Pipeline, :mailer do
end
end
describe '#number_of_warnings' do
it 'returns the number of warnings' do
create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
expect(pipeline.number_of_warnings).to eq(1)
end
it 'supports eager loading of the number of warnings' do
pipeline2 = create(:ci_empty_pipeline, status: :created, project: project)
create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline2, name: 'rubocop')
pipelines = project.pipelines.to_a
pipelines.each(&:number_of_warnings)
# To run the queries we need to actually use the lazy objects, which we do
# by just sending "to_i" to them.
amount = ActiveRecord::QueryRecorder
.new { pipelines.each { |p| p.number_of_warnings.to_i } }
.count
expect(amount).to eq(1)
end
end
shared_context 'with some outdated pipelines' do
before do
create_pipeline(:canceled, 'ref', 'A', project)
......
......@@ -52,22 +52,98 @@ describe Commit do
end
end
describe '#author' do
describe '#author', :request_store do
it 'looks up the author in a case-insensitive way' do
user = create(:user, email: commit.author_email.upcase)
expect(commit.author).to eq(user)
end
it 'caches the author', :request_store do
it 'caches the author' do
user = create(:user, email: commit.author_email)
expect(User).to receive(:find_by_any_email).and_call_original
expect(commit.author).to eq(user)
key = "Commit:author:#{commit.author_email.downcase}"
expect(RequestStore.store[key]).to eq(user)
expect(RequestStore.store[key]).to eq(user)
expect(commit.author).to eq(user)
end
context 'using eager loading' do
let!(:alice) { create(:user, email: 'alice@example.com') }
let!(:bob) { create(:user, email: 'hunter2@example.com') }
let(:alice_commit) do
described_class.new(RepoHelpers.sample_commit, project).tap do |c|
c.author_email = 'alice@example.com'
end
end
let(:bob_commit) do
# The commit for Bob uses one of his alternative Emails, instead of the
# primary one.
described_class.new(RepoHelpers.sample_commit, project).tap do |c|
c.author_email = 'bob@example.com'
end
end
let(:eve_commit) do
described_class.new(RepoHelpers.sample_commit, project).tap do |c|
c.author_email = 'eve@example.com'
end
end
let!(:commits) { [alice_commit, bob_commit, eve_commit] }
before do
create(:email, user: bob, email: 'bob@example.com')
end
it 'executes only two SQL queries' do
recorder = ActiveRecord::QueryRecorder.new do
# Running this first ensures we don't run one query for every
# commit.
commits.each(&:lazy_author)
# This forces the execution of the SQL queries necessary to load the
# data.
commits.each { |c| c.author.try(:id) }
end
expect(recorder.count).to eq(2)
end
it "preloads the authors for Commits matching a user's primary Email" do
commits.each(&:lazy_author)
expect(alice_commit.author).to eq(alice)
end
it "preloads the authors for Commits using a User's alternative Email" do
commits.each(&:lazy_author)
expect(bob_commit.author).to eq(bob)
end
it 'sets the author to Nil if an author could not be found for a Commit' do
commits.each(&:lazy_author)
expect(eve_commit.author).to be_nil
end
it 'does not execute SQL queries once the authors are preloaded' do
commits.each(&:lazy_author)
commits.each { |c| c.author.try(:id) }
recorder = ActiveRecord::QueryRecorder.new do
alice_commit.author
bob_commit.author
eve_commit.author
end
expect(recorder.count).to be_zero
end
end
end
describe '#to_reference' do
......
......@@ -565,4 +565,10 @@ describe CommitStatus do
it_behaves_like 'commit status enqueued'
end
end
describe '#present' do
subject { commit_status.present }
it { is_expected.to be_a(CommitStatusPresenter) }
end
end
......@@ -266,6 +266,19 @@ describe Issuable do
end
end
describe '#time_estimate=' do
it 'coerces the value below Gitlab::Database::MAX_INT_VALUE' do
expect { issue.time_estimate = 100 }.to change { issue.time_estimate }.to(100)
expect { issue.time_estimate = Gitlab::Database::MAX_INT_VALUE + 100 }.to change { issue.time_estimate }.to(Gitlab::Database::MAX_INT_VALUE)
end
it 'skips coercion for not Integer values' do
expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil)
expect { issue.time_estimate = 'invalid time' }.not_to raise_error(StandardError)
expect { issue.time_estimate = 22.33 }.not_to raise_error(StandardError)
end
end
describe '#to_hook_data' do
let(:builder) { double }
......
require 'spec_helper'
describe Sortable do
describe '.order_by' do
let(:relation) { Group.all }
describe 'ordering by id' do
it 'ascending' do
expect(relation).to receive(:reorder).with(id: :asc)
relation.order_by('id_asc')
end
it 'descending' do
expect(relation).to receive(:reorder).with(id: :desc)
relation.order_by('id_desc')
end
end
describe 'ordering by created day' do
it 'ascending' do
expect(relation).to receive(:reorder).with(created_at: :asc)
relation.order_by('created_asc')
end
it 'descending' do
expect(relation).to receive(:reorder).with(created_at: :desc)
relation.order_by('created_desc')
end
it 'order by "date"' do
expect(relation).to receive(:reorder).with(created_at: :desc)
relation.order_by('created_date')
end
end
describe 'ordering by name' do
it 'ascending' do
expect(relation).to receive(:reorder).with("lower(name) asc")
relation.order_by('name_asc')
end
it 'descending' do
expect(relation).to receive(:reorder).with("lower(name) desc")
relation.order_by('name_desc')
end
end
describe 'ordering by Updated Time' do
it 'ascending' do
expect(relation).to receive(:reorder).with(updated_at: :asc)
relation.order_by('updated_asc')
end
it 'descending' do
expect(relation).to receive(:reorder).with(updated_at: :desc)
relation.order_by('updated_desc')
end
end
it 'does not call reorder in case of unrecognized ordering' do
expect(relation).not_to receive(:reorder)
relation.order_by('random_ordering')
end
end
describe 'sorting groups' do
def ordered_group_names(order)
Group.all.order_by(order).map(&:name)
end
let!(:ref_time) { Time.parse('2018-05-01 00:00:00') }
let!(:group1) { create(:group, name: 'aa', id: 1, created_at: ref_time - 15.seconds, updated_at: ref_time) }
let!(:group2) { create(:group, name: 'AAA', id: 2, created_at: ref_time - 10.seconds, updated_at: ref_time - 5.seconds) }
let!(:group3) { create(:group, name: 'BB', id: 3, created_at: ref_time - 5.seconds, updated_at: ref_time - 10.seconds) }
let!(:group4) { create(:group, name: 'bbb', id: 4, created_at: ref_time, updated_at: ref_time - 15.seconds) }
it 'sorts groups by id' do
expect(ordered_group_names('id_asc')).to eq(%w(aa AAA BB bbb))
expect(ordered_group_names('id_desc')).to eq(%w(bbb BB AAA aa))
end
it 'sorts groups by name via case-insentitive comparision' do
expect(ordered_group_names('name_asc')).to eq(%w(aa AAA BB bbb))
expect(ordered_group_names('name_desc')).to eq(%w(bbb BB AAA aa))
end
it 'sorts groups by created_at' do
expect(ordered_group_names('created_asc')).to eq(%w(aa AAA BB bbb))
expect(ordered_group_names('created_desc')).to eq(%w(bbb BB AAA aa))
expect(ordered_group_names('created_date')).to eq(%w(bbb BB AAA aa))
end
it 'sorts groups by updated_at' do
expect(ordered_group_names('updated_asc')).to eq(%w(bbb BB AAA aa))
expect(ordered_group_names('updated_desc')).to eq(%w(aa AAA BB bbb))
end
end
end
......@@ -78,4 +78,10 @@ describe GenericCommitStatus do
it { is_expected.not_to be_nil }
end
end
describe '#present' do
subject { generic_commit_status.present }
it { is_expected.to be_a(GenericCommitStatusPresenter) }
end
end
......@@ -1223,6 +1223,24 @@ describe User do
end
end
describe '#accept_pending_invitations!' do
let(:user) { create(:user, email: 'user@email.com') }
let!(:project_member_invite) { create(:project_member, :invited, invite_email: user.email) }
let!(:group_member_invite) { create(:group_member, :invited, invite_email: user.email) }
let!(:external_project_member_invite) { create(:project_member, :invited, invite_email: 'external@email.com') }
let!(:external_group_member_invite) { create(:group_member, :invited, invite_email: 'external@email.com') }
it 'accepts all the user members pending invitations and returns the accepted_members' do
accepted_members = user.accept_pending_invitations!
expect(accepted_members).to match_array([project_member_invite, group_member_invite])
expect(group_member_invite.reload).not_to be_invite
expect(project_member_invite.reload).not_to be_invite
expect(external_project_member_invite.reload).to be_invite
expect(external_group_member_invite.reload).to be_invite
end
end
describe '#all_emails' do
let(:user) { create(:user) }
......
......@@ -10,7 +10,7 @@ describe Ci::BuildPresenter do
end
it 'inherits from Gitlab::View::Presenter::Delegated' do
expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
expect(described_class.ancestors).to include(Gitlab::View::Presenter::Delegated)
end
describe '#initialize' do
......
require 'spec_helper'
describe CommitStatusPresenter do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
subject(:presenter) do
described_class.new(build)
end
it 'inherits from Gitlab::View::Presenter::Delegated' do
expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
end
end
......@@ -738,13 +738,16 @@ describe API::Groups do
describe "DELETE /groups/:id" do
context "when authenticated as user" do
it "removes group" do
delete api("/groups/#{group1.id}", user1)
Sidekiq::Testing.fake! do
expect { delete api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1)
end
expect(response).to have_gitlab_http_status(204)
expect(response).to have_gitlab_http_status(202)
end
it_behaves_like '412 response' do
let(:request) { api("/groups/#{group1.id}", user1) }
let(:success_status) { 202 }
end
it "does not remove a group if not an owner" do
......@@ -773,7 +776,7 @@ describe API::Groups do
it "removes any existing group" do
delete api("/groups/#{group2.id}", admin)
expect(response).to have_gitlab_http_status(204)
expect(response).to have_gitlab_http_status(202)
end
it "does not remove a non existing group" do
......
require "spec_helper"
describe API::Markdown do
RSpec::Matchers.define_negated_matcher :exclude, :include
describe "POST /markdown" do
let(:user) {} # No-op. It gets overwritten in the contexts below.
before do
post api("/markdown", user), params
end
shared_examples "rendered markdown text without GFM" do
it "renders markdown text" do
expect(response).to have_http_status(201)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash)
expect(json_response["html"]).to eq("<p>#{text}</p>")
end
end
shared_examples "404 Project Not Found" do
it "responses with 404 Not Found" do
expect(response).to have_http_status(404)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash)
expect(json_response["message"]).to eq("404 Project Not Found")
end
end
context "when arguments are invalid" do
context "when text is missing" do
let(:params) { {} }
it "responses with 400 Bad Request" do
expect(response).to have_http_status(400)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash)
expect(json_response["error"]).to eq("text is missing")
end
end
context "when project is not found" do
let(:params) { { text: "Hello world!", gfm: true, project: "Dummy project" } }
it_behaves_like "404 Project Not Found"
end
end
context "when arguments are valid" do
set(:project) { create(:project) }
set(:issue) { create(:issue, project: project) }
let(:text) { ":tada: Hello world! :100: #{issue.to_reference}" }
context "when not using gfm" do
context "without project" do
let(:params) { { text: text } }
it_behaves_like "rendered markdown text without GFM"
end
context "with project" do
let(:params) { { text: text, project: project.full_path } }
context "when not authorized" do
it_behaves_like "404 Project Not Found"
end
context "when authorized" do
let(:user) { project.owner }
it_behaves_like "rendered markdown text without GFM"
end
end
end
context "when using gfm" do
context "without project" do
let(:params) { { text: text, gfm: true } }
it "renders markdown text" do
expect(response).to have_http_status(201)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash)
expect(json_response["html"]).to include("Hello world!")
.and include('data-name="tada"')
.and include('data-name="100"')
.and include("#1")
.and exclude("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"")
.and exclude("#1</a>")
end
end
context "with project" do
let(:params) { { text: text, gfm: true, project: project.full_path } }
let(:user) { project.owner }
it "renders markdown text" do
expect(response).to have_http_status(201)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash)
expect(json_response["html"]).to include("Hello world!")
.and include('data-name="tada"')
.and include('data-name="100"')
.and include("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"")
.and include("#1</a>")
end
end
end
end
end
end
......@@ -458,9 +458,11 @@ describe API::V3::Groups do
describe "DELETE /groups/:id" do
context "when authenticated as user" do
it "removes group" do
delete v3_api("/groups/#{group1.id}", user1)
Sidekiq::Testing.fake! do
expect { delete v3_api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1)
end
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(202)
end
it "does not remove a group if not an owner" do
......@@ -489,7 +491,7 @@ describe API::V3::Groups do
it "removes any existing group" do
delete v3_api("/groups/#{group2.id}", admin)
expect(response).to have_gitlab_http_status(200)
expect(response).to have_gitlab_http_status(202)
end
it "does not remove a non existing group" do
......
......@@ -26,6 +26,13 @@ describe PipelineEntity do
expect(subject).to include :updated_at, :created_at
end
it 'excludes coverage data when disabled' do
entity = described_class
.represent(pipeline, request: request, disable_coverage: true)
expect(entity.as_json).not_to include(:coverage)
end
it 'contains details' do
expect(subject).to include :details
expect(subject[:details])
......
require 'spec_helper'
describe Keys::DestroyService do
let(:user) { create(:user) }
subject { described_class.new(user) }
it 'destroys a key' do
key = create(:key)
expect { subject.execute(key) }.to change(Key, :count).by(-1)
end
end
require 'timeout'
shared_examples 'malicious regexp' do
let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' }
let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' }
......
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