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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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