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: ...@@ -189,7 +189,7 @@ stages:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg <<: *use-pg
variables: variables:
CREATE_DB_USER: "true" SETUP_DB: "false"
script: script:
# Manually clone gitlab-test and only seed this project in # Manually clone gitlab-test and only seed this project in
# db/fixtures/development/04_project.rb thanks to SIZE=1 below # db/fixtures/development/04_project.rb thanks to SIZE=1 below
...@@ -233,7 +233,7 @@ stages: ...@@ -233,7 +233,7 @@ stages:
.migration-paths: &migration-paths .migration-paths: &migration-paths
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables: variables:
CREATE_DB_USER: "true" SETUP_DB: "false"
script: script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0 - git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD - git checkout -f FETCH_HEAD
...@@ -242,7 +242,7 @@ stages: ...@@ -242,7 +242,7 @@ stages:
- cp config/gitlab.yml.example config/gitlab.yml - cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu - bundle exec rake db:drop db:create db:schema:load db:seed_fu
- date - date
- git checkout $CI_COMMIT_SHA - git checkout -f $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS - bundle install $BUNDLE_INSTALL_FLAGS
- date - date
- . scripts/prepare_build.sh - . scripts/prepare_build.sh
......
...@@ -168,7 +168,7 @@ hits. They are not always necessary, but very convenient. ...@@ -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 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 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, Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
~issues, ~"merge requests", ~labels, and ~"container registry". ~issues, ~"merge requests", ~labels, and ~"container registry".
...@@ -296,7 +296,24 @@ any potential community contributor to @-mention per above. ...@@ -296,7 +296,24 @@ any potential community contributor to @-mention per above.
## Implement design & UI elements ## 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 ## Issue tracker
......
...@@ -5,7 +5,7 @@ import LoadingButton from '~/vue_shared/components/loading_button.vue'; ...@@ -5,7 +5,7 @@ import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue'; import CommitMessageField from './message_field.vue';
import Actions from './actions.vue'; import Actions from './actions.vue';
import SuccessMessage from './success_message.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 { export default {
components: { components: {
...@@ -70,7 +70,7 @@ export default { ...@@ -70,7 +70,7 @@ export default {
? this.$refs.formEl && this.$refs.formEl.offsetHeight ? this.$refs.formEl && this.$refs.formEl.offsetHeight
: this.$refs.compactEl && this.$refs.compactEl.offsetHeight; : this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING; this.componentHeight = elHeight;
}, },
enterTransition() { enterTransition() {
this.$nextTick(() => { this.$nextTick(() => {
...@@ -78,7 +78,7 @@ export default { ...@@ -78,7 +78,7 @@ export default {
? this.$refs.compactEl && this.$refs.compactEl.offsetHeight ? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
: this.$refs.formEl && this.$refs.formEl.offsetHeight; : this.$refs.formEl && this.$refs.formEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING; this.componentHeight = elHeight;
}); });
}, },
afterEndTransition() { afterEndTransition() {
......
...@@ -122,11 +122,11 @@ export default { ...@@ -122,11 +122,11 @@ export default {
<div <div
class="file" class="file"
:class="fileClass" :class="fileClass"
@click="clickFile"
role="button"
> >
<div <div
class="file-name" class="file-name"
@click="clickFile"
role="button"
> >
<span <span
class="ide-file-name str-truncated" class="ide-file-name str-truncated"
......
...@@ -5,8 +5,6 @@ export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33; ...@@ -5,8 +5,6 @@ export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750; export const MAX_WINDOW_HEIGHT_COMPACT = 750;
export const COMMIT_ITEM_PADDING = 32;
// Commit message textarea // Commit message textarea
export const MAX_TITLE_LENGTH = 50; export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72; export const MAX_BODY_LENGTH = 72;
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import tooltip from '../../../vue_shared/directives/tooltip'; import axios from '~/lib/utils/axios_utils';
import Icon from '../../../vue_shared/components/icon.vue'; import { dasherize } from '~/lib/utils/text_utility';
import { dasherize } from '../../../lib/utils/text_utility'; import { __ } from '~/locale';
import eventHub from '../../event_hub'; 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 { export default {
components: { components: {
...@@ -32,16 +42,10 @@ export default { ...@@ -32,16 +42,10 @@ export default {
required: true, required: true,
}, },
requestFinishedFor: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
return { return {
isDisabled: false, isDisabled: false,
linkRequested: '',
}; };
}, },
...@@ -51,19 +55,28 @@ export default { ...@@ -51,19 +55,28 @@ export default {
return `${actionIconDash} js-icon-${actionIconDash}`; return `${actionIconDash} js-icon-${actionIconDash}`;
}, },
}, },
watch: {
requestFinishedFor() {
if (this.requestFinishedFor === this.linkRequested) {
this.isDisabled = false;
}
},
},
methods: { methods: {
/**
* The request should not be handled here.
* However due to this component being used in several
* different apps it avoids repetition & complexity.
*
*/
onClickAction() { onClickAction() {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
eventHub.$emit('postAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true; 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" ...@@ -80,6 +93,6 @@ btn-transparent ci-action-icon-container ci-action-icon-wrapper"
data-container="body" data-container="body"
:disabled="isDisabled" :disabled="isDisabled"
> >
<icon :name="actionIcon" /> <icon :name="actionIcon"/>
</button> </button>
</template> </template>
...@@ -42,11 +42,6 @@ export default { ...@@ -42,11 +42,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
requestFinishedFor: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
...@@ -76,11 +71,15 @@ export default { ...@@ -76,11 +71,15 @@ export default {
e.stopPropagation(); e.stopPropagation();
}); });
}, },
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
},
}, },
}; };
</script> </script>
<template> <template>
<div class="ci-job-dropdown-container"> <div class="ci-job-dropdown-container dropdown">
<button <button
v-tooltip v-tooltip
type="button" type="button"
...@@ -110,7 +109,7 @@ export default { ...@@ -110,7 +109,7 @@ export default {
<job-component <job-component
:job="item" :job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
:request-finished-for="requestFinishedFor" @pipelineActionRequestComplete="pipelineActionRequestComplete"
/> />
</li> </li>
</ul> </ul>
......
...@@ -16,11 +16,6 @@ export default { ...@@ -16,11 +16,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
requestFinishedFor: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
...@@ -51,6 +46,10 @@ export default { ...@@ -51,6 +46,10 @@ export default {
return className; return className;
}, },
refreshPipelineGraph() {
this.$emit('refreshPipelineGraph');
},
}, },
}; };
</script> </script>
...@@ -74,7 +73,7 @@ export default { ...@@ -74,7 +73,7 @@ export default {
:key="stage.name" :key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:request-finished-for="requestFinishedFor" @refreshPipelineGraph="refreshPipelineGraph"
/> />
</ul> </ul>
</div> </div>
......
...@@ -46,11 +46,6 @@ export default { ...@@ -46,11 +46,6 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
requestFinishedFor: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
status() { status() {
...@@ -84,6 +79,11 @@ export default { ...@@ -84,6 +79,11 @@ export default {
return this.job.status && this.job.status.action && this.job.status.action.path; return this.job.status && this.job.status.action && this.job.status.action.path;
}, },
}, },
methods: {
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
},
},
}; };
</script> </script>
<template> <template>
...@@ -126,7 +126,7 @@ export default { ...@@ -126,7 +126,7 @@ export default {
:tooltip-text="status.action.title" :tooltip-text="status.action.title"
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
:request-finished-for="requestFinishedFor" @pipelineActionRequestComplete="pipelineActionRequestComplete"
/> />
</div> </div>
</template> </template>
...@@ -29,12 +29,6 @@ export default { ...@@ -29,12 +29,6 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
requestFinishedFor: {
type: String,
required: false,
default: '',
},
}, },
methods: { methods: {
...@@ -49,6 +43,10 @@ export default { ...@@ -49,6 +43,10 @@ export default {
buildConnnectorClass(index) { buildConnnectorClass(index) {
return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
}, },
pipelineActionRequestComplete() {
this.$emit('refreshPipelineGraph');
},
}, },
}; };
</script> </script>
...@@ -75,12 +73,13 @@ export default { ...@@ -75,12 +73,13 @@ export default {
v-if="job.size === 1" v-if="job.size === 1"
:job="job" :job="job"
css-class-job-name="build-content" css-class-job-name="build-content"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/> />
<dropdown-job-component <dropdown-job-component
v-if="job.size > 1" v-if="job.size > 1"
:job="job" :job="job"
:request-finished-for="requestFinishedFor" @pipelineActionRequestComplete="pipelineActionRequestComplete"
/> />
</li> </li>
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
import CommitComponent from '../../vue_shared/components/commit.vue'; import CommitComponent from '../../vue_shared/components/commit.vue';
import LoadingButton from '../../vue_shared/components/loading_button.vue'; import LoadingButton from '../../vue_shared/components/loading_button.vue';
import Icon from '../../vue_shared/components/icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
import { PIPELINES_TABLE } from '../constants';
/** /**
* Pipeline table row. * Pipeline table row.
...@@ -46,6 +47,7 @@ ...@@ -46,6 +47,7 @@
required: true, required: true,
}, },
}, },
pipelinesTable: PIPELINES_TABLE,
data() { data() {
return { return {
isRetrying: false, isRetrying: false,
...@@ -297,6 +299,7 @@ ...@@ -297,6 +299,7 @@
v-for="(stage, index) in pipeline.details.stages" v-for="(stage, index) in pipeline.details.stages"
:key="index"> :key="index">
<pipeline-stage <pipeline-stage
:type="$options.pipelinesTable"
:stage="stage" :stage="stage"
:update-dropdown="updateGraphDropdown" :update-dropdown="updateGraphDropdown"
/> />
......
...@@ -21,6 +21,7 @@ import Icon from '../../vue_shared/components/icon.vue'; ...@@ -21,6 +21,7 @@ import Icon from '../../vue_shared/components/icon.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import JobComponent from './graph/job_component.vue'; import JobComponent from './graph/job_component.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { PIPELINES_TABLE } from '../constants';
export default { export default {
components: { components: {
...@@ -44,6 +45,12 @@ export default { ...@@ -44,6 +45,12 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
type: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
...@@ -133,6 +140,16 @@ export default { ...@@ -133,6 +140,16 @@ export default {
isDropdownOpen() { isDropdownOpen() {
return this.$el.classList.contains('open'); 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> </script>
...@@ -151,6 +168,7 @@ export default { ...@@ -151,6 +168,7 @@ export default {
id="stageDropdown" id="stageDropdown"
aria-haspopup="true" aria-haspopup="true"
aria-expanded="false" aria-expanded="false"
ref="dropdown"
> >
<span <span
...@@ -188,6 +206,7 @@ export default { ...@@ -188,6 +206,7 @@ export default {
<job-component <job-component
:job="job" :job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/> />
</li> </li>
</ul> </ul>
......
// eslint-disable-next-line import/prefer-default-export
export const CANCEL_REQUEST = 'CANCEL_REQUEST'; export const CANCEL_REQUEST = 'CANCEL_REQUEST';
export const PIPELINES_TABLE = 'PIPELINES_TABLE';
...@@ -55,11 +55,13 @@ export default { ...@@ -55,11 +55,13 @@ export default {
eventHub.$on('postAction', this.postAction); eventHub.$on('postAction', this.postAction);
eventHub.$on('retryPipeline', this.postAction); eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable); eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('postAction', this.postAction); eventHub.$off('postAction', this.postAction);
eventHub.$off('retryPipeline', this.postAction); eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable); eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
}, },
destroyed() { destroyed() {
this.poll.stop(); this.poll.stop();
......
...@@ -25,30 +25,14 @@ export default () => { ...@@ -25,30 +25,14 @@ export default () => {
data() { data() {
return { return {
mediator, mediator,
requestFinishedFor: null,
}; };
}, },
created() {
eventHub.$on('postAction', this.postAction);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
},
methods: { methods: {
postAction(action) { requestRefreshPipelineGraph() {
// Click was made, reset this variable // When an action is clicked
this.requestFinishedFor = null; // (wether in the dropdown or in the main nodes, we refresh the big graph)
this.mediator.refreshPipeline()
this.mediator.service .catch(() => Flash(__('An error occurred while making the request.')));
.postAction(action)
.then(() => {
this.mediator.refreshPipeline();
this.requestFinishedFor = action;
})
.catch(() => {
this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.'));
});
}, },
}, },
render(createElement) { render(createElement) {
...@@ -56,7 +40,9 @@ export default () => { ...@@ -56,7 +40,9 @@ export default () => {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline, pipeline: this.mediator.store.state.pipeline,
requestFinishedFor: this.requestFinishedFor, },
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
}, },
}); });
}, },
......
...@@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts { ...@@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts {
super(); super();
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project')); 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 f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits')); Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds')); Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
...@@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts { ...@@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues')); Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards')); Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests')); 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 w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); 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')); Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project'); this.enabledHelp.push('.hidden-shortcut.project');
......
...@@ -279,251 +279,14 @@ ul.indent-list { ...@@ -279,251 +279,14 @@ ul.indent-list {
padding: 10px 0 0 30px; padding: 10px 0 0 30px;
} }
// Specific styles for tree list // Specific styles for tree list
@keyframes spin-avatar { @keyframes spin-avatar {
from { transform: rotate(0deg); } from { transform: rotate(0deg); }
to { transform: rotate(360deg); } 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 { .namespace-title {
.tooltip-inner { .tooltip-inner {
max-width: 350px; 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 @@ ...@@ -18,6 +18,10 @@
.group-row { .group-row {
@include basic-list-stats; @include basic-list-stats;
.description p {
margin-bottom: 0;
}
} }
.ldap-group-links { .ldap-group-links {
...@@ -237,3 +241,231 @@ ...@@ -237,3 +241,231 @@
overflow-y: unset; 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 @@ ...@@ -197,9 +197,21 @@
} }
&.assignee { &.assignee {
.author_link:hover { .author_link {
.author { display: block;
text-decoration: underline; padding-left: 42px;
position: relative;
&:hover {
.author {
text-decoration: underline;
}
}
.avatar {
left: 0;
position: absolute;
top: 0;
} }
} }
} }
......
...@@ -66,13 +66,9 @@ ...@@ -66,13 +66,9 @@
} }
} }
.btn-group { .btn-group.open .btn-default {
&.open { background-color: $white-normal;
.btn-default { border-color: $border-white-normal;
background-color: $white-normal;
border-color: $border-white-normal;
}
}
} }
.btn .text-center { .btn .text-center {
...@@ -361,16 +357,14 @@ ...@@ -361,16 +357,14 @@
&:not(:first-child) { &:not(:first-child) {
margin-left: 44px; margin-left: 44px;
.left-connector { .left-connector::before {
&::before { content: '';
content: ''; position: absolute;
position: absolute; top: 48%;
top: 48%; left: -44px;
left: -44px; border-top: 2px solid $border-color;
border-top: 2px solid $border-color; width: 44px;
width: 44px; height: 1px;
height: 1px;
}
} }
} }
} }
...@@ -386,22 +380,16 @@ ...@@ -386,22 +380,16 @@
&:last-child { &:last-child {
.build { .build {
// Remove right connecting horizontal line from first build in last stage // Remove right connecting horizontal line from first build in last stage
&:first-child { &:first-child::after {
&::after { border: 0;
border: 0;
}
} }
// Remove right curved connectors from all builds in last stage // Remove right curved connectors from all builds in last stage
&:not(:first-child) { &:not(:first-child)::after {
&::after { border: 0;
border: 0;
}
} }
// Remove opposite curve // Remove opposite curve
.curve { .curve::before {
&::before { display: none;
display: none;
}
} }
} }
} }
...@@ -409,16 +397,12 @@ ...@@ -409,16 +397,12 @@
&:first-child { &:first-child {
.build { .build {
// Remove left curved connectors from all builds in first stage // Remove left curved connectors from all builds in first stage
&:not(:first-child) { &:not(:first-child)::before {
&::before { border: 0;
border: 0;
}
} }
// Remove opposite curve // Remove opposite curve
.curve { .curve::after {
&::after { display: none;
display: none;
}
} }
} }
} }
......
...@@ -39,12 +39,15 @@ ...@@ -39,12 +39,15 @@
.ide-file-list { .ide-file-list {
flex: 1; flex: 1;
padding-left: $gl-padding;
padding-right: $gl-padding;
padding-bottom: $grid-size;
.file { .file {
cursor: pointer; cursor: pointer;
&.file-open { &.file-open {
background: $link-active-background; background: $white-normal;
} }
&.file-active { &.file-active {
...@@ -84,12 +87,11 @@ ...@@ -84,12 +87,11 @@
.ide-new-btn { .ide-new-btn {
display: none; display: none;
margin-right: -8px;
} }
&:hover, &:hover,
&:focus { &:focus {
background: $link-active-background; background: $white-normal;
.ide-new-btn { .ide-new-btn {
display: block; display: block;
...@@ -111,12 +113,11 @@ ...@@ -111,12 +113,11 @@
} }
} }
.file-name, .file-name {
.file-col-commit-message {
display: flex; display: flex;
overflow: visible; overflow: visible;
align-items: center; align-items: center;
padding: 6px 12px; width: 100%;
} }
.multi-file-loading-container { .multi-file-loading-container {
...@@ -306,8 +307,18 @@ ...@@ -306,8 +307,18 @@
} }
.preview-container { .preview-container {
height: 100%; flex-grow: 1;
overflow: auto; position: relative;
.md-previewer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: auto;
padding: $gl-padding;
}
.file-container { .file-container {
background-color: $gray-darker; background-color: $gray-darker;
...@@ -347,10 +358,6 @@ ...@@ -347,10 +358,6 @@
color: $diff-image-info-color; color: $diff-image-info-color;
} }
} }
.md-previewer {
padding: $gl-padding;
}
} }
.ide-mode-tabs { .ide-mode-tabs {
...@@ -501,7 +508,7 @@ ...@@ -501,7 +508,7 @@
align-items: center; align-items: center;
margin-bottom: 0; margin-bottom: 0;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding $gl-padding; padding: 12px 0;
} }
.multi-file-commit-panel-header-title { .multi-file-commit-panel-header-title {
...@@ -523,32 +530,31 @@ ...@@ -523,32 +530,31 @@
.multi-file-commit-list { .multi-file-commit-list {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: $gl-padding; padding: $grid-size 0;
margin-left: -$grid-size;
margin-right: -$grid-size;
min-height: 60px; 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 { .multi-file-commit-list-item {
display: flex;
padding: 0;
align-items: center;
border-radius: $border-radius-default;
.multi-file-discard-btn { .multi-file-discard-btn {
display: none; display: none;
margin-top: -2px; margin-top: -2px;
margin-left: auto; margin-left: auto;
margin-right: $grid-size;
color: $gl-link-color; color: $gl-link-color;
&:focus,
&:hover {
text-decoration: underline;
}
} }
&:hover { &:hover {
background: $white-normal;
.multi-file-discard-btn { .multi-file-discard-btn {
display: flex; display: flex;
} }
...@@ -584,25 +590,39 @@ ...@@ -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 { .multi-file-commit-list-path {
padding: $grid-size / 2; padding: 0;
padding-left: $grid-size;
background: none; background: none;
border: 0; border: 0;
text-align: left; text-align: left;
width: 100%; width: 100%;
min-width: 0;
&:hover,
&:focus {
outline: 0;
}
svg { svg {
min-width: 16px; min-width: 16px;
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
} }
&:hover,
&:focus {
outline: 0;
}
} }
.multi-file-commit-list-file-path { .multi-file-commit-list-file-path {
...@@ -619,12 +639,18 @@ ...@@ -619,12 +639,18 @@
.multi-file-commit-form { .multi-file-commit-form {
position: relative; position: relative;
padding: $gl-padding;
background-color: $white-light; background-color: $white-light;
border-top: 1px solid $white-dark;
border-left: 1px solid $white-dark; border-left: 1px solid $white-dark;
transition: all 0.3s ease; 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 { .btn {
font-size: $gl-font-size; font-size: $gl-font-size;
} }
...@@ -787,8 +813,9 @@ ...@@ -787,8 +813,9 @@
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
width: 100%;
min-height: 140px; min-height: 140px;
margin-left: $gl-padding;
margin-right: $gl-padding;
&.is-first { &.is-first {
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
...@@ -979,9 +1006,8 @@ ...@@ -979,9 +1006,8 @@
.ide-tree-header { .ide-tree-header {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 10px 0; margin-bottom: 8px;
margin-left: 10px; padding: 12px 0;
margin-right: 10px;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
.ide-new-btn { .ide-new-btn {
...@@ -1012,9 +1038,9 @@ ...@@ -1012,9 +1038,9 @@
.commit-form-slide-up-enter-active, .commit-form-slide-up-enter-active,
.commit-form-slide-up-leave-active { .commit-form-slide-up-leave-active {
position: absolute; position: absolute;
top: 16px; top: 0;
left: 16px; left: 0;
right: 16px; right: 0;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
......
...@@ -94,7 +94,7 @@ module Boards ...@@ -94,7 +94,7 @@ module Boards
def serialize_as_json(resource) def serialize_as_json(resource)
resource.as_json( 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, labels: true,
issue_endpoints: true, issue_endpoints: true,
include_full_project_path: board.group_board?, 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 class ConfirmationsController < Devise::ConfirmationsController
include AcceptsPendingInvitations
def almost_there def almost_there
flash[:notice] = nil flash[:notice] = nil
render layout: "devise_empty" render layout: "devise_empty"
...@@ -11,6 +13,8 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -11,6 +13,8 @@ class ConfirmationsController < Devise::ConfirmationsController
end end
def after_confirmation_path_for(resource_name, resource) def after_confirmation_path_for(resource_name, resource)
accept_pending_invitations
# incoming resource can either be a :user or an :email # incoming resource can either be a :user or an :email
if signed_in?(:user) if signed_in?(:user)
after_sign_in(resource) after_sign_in(resource)
......
...@@ -23,7 +23,7 @@ class Profiles::KeysController < Profiles::ApplicationController ...@@ -23,7 +23,7 @@ class Profiles::KeysController < Profiles::ApplicationController
def destroy def destroy
@key = current_user.keys.find(params[:id]) @key = current_user.keys.find(params[:id])
@key.destroy Keys::DestroyService.new(current_user).execute(@key)
respond_to do |format| respond_to do |format|
format.html { redirect_to profile_keys_url, status: 302 } format.html { redirect_to profile_keys_url, status: 302 }
......
...@@ -18,19 +18,12 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -18,19 +18,12 @@ class Projects::PipelinesController < Projects::ApplicationController
.page(params[:page]) .page(params[:page])
.per(30) .per(30)
@running_count = PipelinesFinder @running_count = limited_pipelines_count(project, 'running')
.new(project, scope: 'running').execute.count @pending_count = limited_pipelines_count(project, 'pending')
@finished_count = limited_pipelines_count(project, 'finished')
@pipelines_count = limited_pipelines_count(project)
@pending_count = PipelinesFinder Gitlab::Ci::Pipeline::Preloader.preload(@pipelines)
.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
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -41,7 +34,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -41,7 +34,7 @@ class Projects::PipelinesController < Projects::ApplicationController
pipelines: PipelineSerializer pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
.with_pagination(request, response) .with_pagination(request, response)
.represent(@pipelines), .represent(@pipelines, disable_coverage: true),
count: { count: {
all: @pipelines_count, all: @pipelines_count,
running: @running_count, running: @running_count,
...@@ -185,4 +178,10 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -185,4 +178,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def authorize_update_pipeline! def authorize_update_pipeline!
return access_denied! unless can?(current_user, :update_pipeline, @pipeline) return access_denied! unless can?(current_user, :update_pipeline, @pipeline)
end end
def limited_pipelines_count(project, scope = nil)
finder = PipelinesFinder.new(project, scope: scope)
view_context.limited_counter_with_delimiter(finder.execute)
end
end end
...@@ -11,7 +11,14 @@ module Projects ...@@ -11,7 +11,14 @@ module Projects
@hook = ProjectHook.new @hook = ProjectHook.new
# Services # 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 end
end end
......
class RegistrationsController < Devise::RegistrationsController class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify include Recaptcha::Verify
include AcceptsPendingInvitations
before_action :whitelist_query_limiting, only: [:destroy] before_action :whitelist_query_limiting, only: [:destroy]
...@@ -16,6 +17,7 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -16,6 +17,7 @@ class RegistrationsController < Devise::RegistrationsController
end end
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
accept_pending_invitations
super super
else else
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
...@@ -60,7 +62,7 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -60,7 +62,7 @@ class RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(user) 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?}") 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 end
def after_inactive_sign_up_path_for(resource) def after_inactive_sign_up_path_for(resource)
......
...@@ -184,7 +184,7 @@ module Ci ...@@ -184,7 +184,7 @@ module Ci
end end
def playable? def playable?
action? && (manual? || complete?) action? && (manual? || retryable?)
end end
def action? def action?
......
...@@ -406,7 +406,18 @@ module Ci ...@@ -406,7 +406,18 @@ module Ci
end end
def has_warnings? 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 end
def set_config_source def set_config_source
......
...@@ -224,8 +224,34 @@ class Commit ...@@ -224,8 +224,34 @@ class Commit
Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message) Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
end 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 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 end
request_cache(:author) { author_email.downcase } request_cache(:author) { author_email.downcase }
......
...@@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base
include HasStatus include HasStatus
include Importable include Importable
include AfterCommitQueue include AfterCommitQueue
include Presentable
self.table_name = 'ci_builds' self.table_name = 'ci_builds'
......
...@@ -12,8 +12,8 @@ module Sortable ...@@ -12,8 +12,8 @@ module Sortable
scope :order_created_asc, -> { reorder(created_at: :asc) } scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) } scope :order_updated_desc, -> { reorder(updated_at: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc) } scope :order_updated_asc, -> { reorder(updated_at: :asc) }
scope :order_name_asc, -> { reorder(name: :asc) } scope :order_name_asc, -> { reorder("lower(name) asc") }
scope :order_name_desc, -> { reorder(name: :desc) } scope :order_name_desc, -> { reorder("lower(name) desc") }
end end
module ClassMethods module ClassMethods
......
...@@ -53,6 +53,10 @@ module TimeTrackable ...@@ -53,6 +53,10 @@ module TimeTrackable
Gitlab::TimeTrackingFormatter.output(time_estimate) Gitlab::TimeTrackingFormatter.output(time_estimate)
end end
def time_estimate=(val)
val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val)
end
private private
def touchable? def touchable?
......
...@@ -997,7 +997,7 @@ class Project < ActiveRecord::Base ...@@ -997,7 +997,7 @@ class Project < ActiveRecord::Base
available_services_names = Service.available_services_names - exceptions 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) service = find_service(services, service_name)
if service if service
...@@ -1014,6 +1014,14 @@ class Project < ActiveRecord::Base ...@@ -1014,6 +1014,14 @@ class Project < ActiveRecord::Base
end end
end end
end end
available_services.reject do |service|
disabled_services.include?(service.to_param)
end
end
def disabled_services
[]
end end
def find_or_initialize_service(name) def find_or_initialize_service(name)
......
...@@ -860,6 +860,16 @@ class User < ActiveRecord::Base ...@@ -860,6 +860,16 @@ class User < ActiveRecord::Base
confirmed? && !temp_oauth_email? confirmed? && !temp_oauth_email?
end 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 def all_emails
all_emails = [] all_emails = []
all_emails << email unless temp_oauth_email? all_emails << email unless temp_oauth_email?
......
module Ci module Ci
class BuildPresenter < Gitlab::View::Presenter::Delegated class BuildPresenter < CommitStatusPresenter
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 erased_by_user? def erased_by_user?
# Build can be erased through API, therefore it does not have # Build can be erased through API, therefore it does not have
# `erased_by` user assigned in that case. # `erased_by` user assigned in that case.
...@@ -44,14 +33,6 @@ module Ci ...@@ -44,14 +33,6 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}" "#{subject.name} - #{detailed_status.status_tooltip}"
end end
def callout_failure_message
CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
end
def recoverable?
failed? && !unrecoverable?
end
private private
def tooltip_for_badge def tooltip_for_badge
...@@ -61,9 +42,5 @@ module Ci ...@@ -61,9 +42,5 @@ module Ci
def detailed_status def detailed_status
@detailed_status ||= subject.detailed_status(user) @detailed_status ||= subject.detailed_status(user)
end end
def unrecoverable?
script_failure? || missing_dependency_failure?
end
end 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 ...@@ -4,7 +4,11 @@ class PipelineEntity < Grape::Entity
expose :id expose :id
expose :user, using: UserEntity expose :user, using: UserEntity
expose :active?, as: :active 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 :source
expose :created_at, :updated_at expose :created_at, :updated_at
......
...@@ -2,7 +2,7 @@ module Keys ...@@ -2,7 +2,7 @@ module Keys
class BaseService class BaseService
attr_accessor :user, :params attr_accessor :user, :params
def initialize(user, params) def initialize(user, params = {})
@user, @params = user, params @user, @params = user, params
@ip_address = @params.delete(:ip_address) @ip_address = @params.delete(:ip_address)
end 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 ...@@ -2,14 +2,14 @@ module Lfs
class UnlockFileService < BaseService class UnlockFileService < BaseService
def execute def execute
unless can?(current_user, :push_code, project) unless can?(current_user, :push_code, project)
raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions' raise Gitlab::GitAccess::UnauthorizedError, _('You have no permissions')
end end
unlock_file unlock_file
rescue Gitlab::GitAccess::UnauthorizedError => ex rescue Gitlab::GitAccess::UnauthorizedError => ex
error(ex.message, 403) error(ex.message, 403)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
error('Lock not found', 404) error(_('Lock not found'), 404)
rescue => ex rescue => ex
error(ex.message, 500) error(ex.message, 500)
end end
...@@ -24,9 +24,9 @@ module Lfs ...@@ -24,9 +24,9 @@ module Lfs
success(lock: lock, http_status: :ok) success(lock: lock, http_status: :ok)
elsif forced 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 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
end end
......
...@@ -5,6 +5,7 @@ module Milestones ...@@ -5,6 +5,7 @@ module Milestones
def initialize(parent, user, params = {}) def initialize(parent, user, params = {})
@parent, @current_user, @params = parent, user, params.dup @parent, @current_user, @params = parent, user, params.dup
super
end end
end end
end end
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
%tr %tr
%td.shortcut %td.shortcut
.key g .key g
.key e .key v
%td %td
Go to the project's activity feed Go to the project's activity feed
%tr %tr
...@@ -172,6 +172,18 @@ ...@@ -172,6 +172,18 @@
.key m .key m
%td %td
Go to merge requests 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 %tr
%td.shortcut %td.shortcut
.key g .key g
......
...@@ -212,7 +212,7 @@ ...@@ -212,7 +212,7 @@
- if project_nav_tab? :clusters - if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project) - show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do = 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 %span
= _('Kubernetes') = _('Kubernetes')
- if show_cluster_hint - if show_cluster_hint
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
by by
= link_to member.created_by.name, user_url(member.created_by) = link_to member.created_by.name, user_url(member.created_by)
to join the 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}. #{member_source.model_name.singular} as #{member.human_access}.
%p %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 = ActiveSupport::Deprecation.new('11.0', 'GitLab')
deprecator.behavior = -> (message, callstack) { 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: ...@@ -33,6 +33,7 @@ following locations:
- [Jobs](jobs.md) - [Jobs](jobs.md)
- [Keys](keys.md) - [Keys](keys.md)
- [Labels](labels.md) - [Labels](labels.md)
- [Markdown](markdown.md)
- [Merge Requests](merge_requests.md) - [Merge Requests](merge_requests.md)
- [Project milestones](milestones.md) - [Project milestones](milestones.md)
- [Group milestones](group_milestones.md) - [Group milestones](group_milestones.md)
......
...@@ -487,6 +487,9 @@ Parameters: ...@@ -487,6 +487,9 @@ Parameters:
- `id` (required) - The ID or path of a user group - `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 ## Search for group
Get all groups that match your string in their name or path. 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: ...@@ -530,6 +530,16 @@ Below you can find supported syntax reference:
`$STAGING` value needs to a string, with length higher than zero. `$STAGING` value needs to a string, with length higher than zero.
Variable that contains only whitespace characters is not an empty variable. 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 ### Unsupported predefined variables
Because GitLab evaluates variables before creating jobs, we do not support a Because GitLab evaluates variables before creating jobs, we do not support a
......
...@@ -344,10 +344,11 @@ job: ...@@ -344,10 +344,11 @@ job:
kubernetes: active kubernetes: active
``` ```
Example of using variables expressions: Examples of using variables expressions:
```yaml ```yaml
deploy: deploy:
script: cap staging deploy
only: only:
refs: refs:
- branches - branches
...@@ -356,6 +357,16 @@ deploy: ...@@ -356,6 +357,16 @@ deploy:
- $STAGING - $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]. Learn more about variables expressions on [a separate page][variables-expressions].
## `tags` ## `tags`
......
...@@ -37,12 +37,13 @@ import state from './state'; ...@@ -37,12 +37,13 @@ import state from './state';
Vue.use(Vuex); Vue.use(Vuex);
export default new Vuex.Store({ export const createStore = () => new Vuex.Store({
actions, actions,
getters, getters,
mutations, mutations,
state, state,
}); });
export default createStore();
``` ```
### `state.js` ### `state.js`
...@@ -320,10 +321,11 @@ In order to write unit tests for those components, we need to include the store ...@@ -320,10 +321,11 @@ In order to write unit tests for those components, we need to include the store
```javascript ```javascript
//component_spec.js //component_spec.js
import Vue from 'vue'; import Vue from 'vue';
import store from './store'; import { createStore } from './store';
import component from './component.vue' import component from './component.vue'
describe('component', () => { describe('component', () => {
let store;
let vm; let vm;
let Component; let Component;
...@@ -340,6 +342,8 @@ describe('component', () => { ...@@ -340,6 +342,8 @@ describe('component', () => {
name: 'Foo', name: 'Foo',
age: '30', age: '30',
}; };
store = createStore();
// populate the store // populate the store
store.dispatch('addUser', user); store.dispatch('addUser', user);
......
...@@ -15,9 +15,9 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so ...@@ -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 you should disable these mechanisms before downgrading and you should provide
alternative authentication methods to your users. 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 so if you downgrade to the Community Edition, you'll come across the following
error: error:
...@@ -30,20 +30,31 @@ column if you didn't intend it to be used for storing the inheritance class or o ...@@ -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.) 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 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 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** **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** **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 ### Secret variables environment scopes
......
...@@ -64,6 +64,13 @@ If you are running GitLab within a Docker container, you can run the backup from ...@@ -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 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: Example output:
``` ```
...@@ -601,6 +608,34 @@ If there is a GitLab version mismatch between your backup tar file and the insta ...@@ -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 version of GitLab, the restore command will abort with an error. Install the
[correct GitLab version](https://packages.gitlab.com/gitlab/) and try again. [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 ## 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. 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 ...@@ -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 **Fork** button. Soon you should have a project under your namespace with the
necessary files. 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 ## Setup your own cluster on Google Kubernetes Engine
If you do not already have a Google Cloud account, create one at 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 + ?' ...@@ -46,15 +46,19 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| Keyboard Shortcut | Description | | Keyboard Shortcut | Description |
| ----------------- | ----------- | | ----------------- | ----------- |
| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page | | <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>f</kbd> | Go to files |
| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits | | <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>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>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>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>s</kbd> | Go to snippets |
| <kbd>g</kbd> + <kbd>w</kbd> | Go to wiki |
| <kbd>t</kbd> | Go to finding file | | <kbd>t</kbd> | Go to finding file |
| <kbd>i</kbd> | New issue | | <kbd>i</kbd> | New issue |
......
...@@ -8,14 +8,15 @@ module API ...@@ -8,14 +8,15 @@ module API
PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
use GrapeLogging::Middleware::RequestLogger, insert_before Grape::Middleware::Error,
logger: Logger.new(LOG_FILENAME), GrapeLogging::Middleware::RequestLogger,
formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, logger: Logger.new(LOG_FILENAME),
include: [ formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
GrapeLogging::Loggers::FilterParameters.new, include: [
GrapeLogging::Loggers::ClientEnv.new, GrapeLogging::Loggers::FilterParameters.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new GrapeLogging::Loggers::ClientEnv.new,
] Gitlab::GrapeLogging::Loggers::UserLogger.new
]
allow_access_with_scope :api allow_access_with_scope :api
prefix :api prefix :api
...@@ -139,6 +140,7 @@ module API ...@@ -139,6 +140,7 @@ module API
mount ::API::Keys mount ::API::Keys
mount ::API::Labels mount ::API::Labels
mount ::API::Lint mount ::API::Lint
mount ::API::Markdown
mount ::API::Members mount ::API::Members
mount ::API::MergeRequestDiffs mount ::API::MergeRequestDiffs
mount ::API::MergeRequests mount ::API::MergeRequests
......
...@@ -167,8 +167,10 @@ module API ...@@ -167,8 +167,10 @@ module API
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285') Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
destroy_conditionally!(group) do |group| destroy_conditionally!(group) do |group|
::Groups::DestroyService.new(group, current_user).execute ::Groups::DestroyService.new(group, current_user).async_execute
end end
accepted!
end end
desc 'Get a list of projects in this group.' do 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 ...@@ -131,8 +131,9 @@ module API
delete ":id" do delete ":id" do
group = find_group!(params[:id]) group = find_group!(params[:id])
authorize! :admin_group, group authorize! :admin_group, group
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285') ::Groups::DestroyService.new(group, current_user).async_execute
present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
accepted!
end end
desc 'Get a list of projects in this group.' do desc 'Get a list of projects in this group.' do
......
...@@ -73,7 +73,7 @@ module Banzai ...@@ -73,7 +73,7 @@ module Banzai
# #
# Note that while the key might exist, its value could be nil! # Note that while the key might exist, its value could be nil!
def validate def validate
needs :project needs :project unless skip_project_check?
end end
# Iterates over all <a> and text() nodes in a document. # Iterates over all <a> and text() nodes in a document.
......
...@@ -42,9 +42,9 @@ module Banzai ...@@ -42,9 +42,9 @@ module Banzai
end end
def self.transform_context(context) def self.transform_context(context)
context.merge( context[:only_path] = true unless context.key?(:only_path)
only_path: true,
context.merge(
# EmojiFilter # EmojiFilter
asset_host: Gitlab::Application.config.asset_host, asset_host: Gitlab::Application.config.asset_host,
asset_root: Gitlab.config.gitlab.base_url 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 ...@@ -5,15 +5,17 @@ module Gitlab
class Lexer class Lexer
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
SyntaxError = Class.new(Expression::ExpressionError)
LEXEMES = [ LEXEMES = [
Expression::Lexeme::Variable, Expression::Lexeme::Variable,
Expression::Lexeme::String, Expression::Lexeme::String,
Expression::Lexeme::Pattern,
Expression::Lexeme::Null, Expression::Lexeme::Null,
Expression::Lexeme::Equals Expression::Lexeme::Equals,
Expression::Lexeme::Matches
].freeze ].freeze
SyntaxError = Class.new(Statement::StatementError)
MAX_TOKENS = 100 MAX_TOKENS = 100
def initialize(statement, max_tokens: MAX_TOKENS) def initialize(statement, max_tokens: MAX_TOKENS)
......
...@@ -3,15 +3,16 @@ module Gitlab ...@@ -3,15 +3,16 @@ module Gitlab
module Pipeline module Pipeline
module Expression module Expression
class Statement class Statement
StatementError = Class.new(StandardError) StatementError = Class.new(Expression::ExpressionError)
GRAMMAR = [ GRAMMAR = [
%w[variable],
%w[variable equals string], %w[variable equals string],
%w[variable equals variable], %w[variable equals variable],
%w[variable equals null], %w[variable equals null],
%w[string equals variable], %w[string equals variable],
%w[null equals variable], %w[null equals variable],
%w[variable] %w[variable matches pattern]
].freeze ].freeze
def initialize(statement, variables = {}) def initialize(statement, variables = {})
...@@ -35,11 +36,13 @@ module Gitlab ...@@ -35,11 +36,13 @@ module Gitlab
def truthful? def truthful?
evaluate.present? evaluate.present?
rescue Expression::ExpressionError
false
end end
def valid? def valid?
parse_tree.is_a?(Lexeme::Base) parse_tree.is_a?(Lexeme::Base)
rescue StatementError rescue Expression::ExpressionError
false false
end end
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 ...@@ -43,7 +43,7 @@ module Gitlab
end end
def self.version def self.version
database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1] @version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end end
def self.join_lateral_supported? def self.join_lateral_supported?
......
...@@ -1572,14 +1572,12 @@ module Gitlab ...@@ -1572,14 +1572,12 @@ module Gitlab
end end
def checksum def checksum
gitaly_migrate(:calculate_checksum, # The exists? RPC is much cheaper, so we perform this request first
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| raise NoRepository, "Repository does not exists" unless exists?
if is_enabled
gitaly_repository_client.calculate_checksum gitaly_repository_client.calculate_checksum
else rescue GRPC::NotFound
calculate_checksum_by_shelling_out raise NoRepository # Guard against data races.
end
end
end end
private private
...@@ -2478,36 +2476,6 @@ module Gitlab ...@@ -2478,36 +2476,6 @@ module Gitlab
rev_parse_target(ref).oid rev_parse_target(ref).oid
end 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) def build_git_cmd(*args)
object_directories = alternate_object_directories.join(File::PATH_SEPARATOR) object_directories = alternate_object_directories.join(File::PATH_SEPARATOR)
......
...@@ -57,7 +57,7 @@ module Gitlab ...@@ -57,7 +57,7 @@ module Gitlab
regex = Regexp.escape(wildcard_address) regex = Regexp.escape(wildcard_address)
regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)') regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)')
Regexp.new(regex).freeze Regexp.new(/\A#{regex}\z/).freeze
end end
end end
end end
......
...@@ -9,7 +9,9 @@ module Gitlab ...@@ -9,7 +9,9 @@ module Gitlab
# there is a strict limit on total execution time. See the RE2 documentation # there is a strict limit on total execution time. See the RE2 documentation
# at https://github.com/google/re2/wiki/Syntax for more details. # at https://github.com/google/re2/wiki/Syntax for more details.
class UntrustedRegexp class UntrustedRegexp
delegate :===, to: :regexp require_dependency 're2'
delegate :===, :source, to: :regexp
def initialize(pattern, multiline: false) def initialize(pattern, multiline: false)
if multiline if multiline
...@@ -35,6 +37,10 @@ module Gitlab ...@@ -35,6 +37,10 @@ module Gitlab
RE2.Replace(text, regexp, rewrite) RE2.Replace(text, regexp, rewrite)
end end
def ==(other)
self.source == other.source
end
# Handles regular expressions with the preferred RE2 library where possible # Handles regular expressions with the preferred RE2 library where possible
# via UntustedRegex. Falls back to Ruby's built-in regular expression library # via UntustedRegex. Falls back to Ruby's built-in regular expression library
# when the syntax would be invalid in RE2. # when the syntax would be invalid in RE2.
...@@ -48,6 +54,24 @@ module Gitlab ...@@ -48,6 +54,24 @@ module Gitlab
Regexp.new(pattern) Regexp.new(pattern)
end 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 private
attr_reader :regexp attr_reader :regexp
......
...@@ -8,6 +8,7 @@ task setup_postgresql: :environment do ...@@ -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/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/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/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') require Rails.root.join('db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb')
NamespacesProjectsPathLowerIndexes.new.up NamespacesProjectsPathLowerIndexes.new.up
...@@ -18,5 +19,6 @@ task setup_postgresql: :environment do ...@@ -18,5 +19,6 @@ task setup_postgresql: :environment do
IndexRedirectRoutesPathForLike.new.up IndexRedirectRoutesPathForLike.new.up
AddIndexOnNamespacesLowerName.new.up AddIndexOnNamespacesLowerName.new.up
UsersNameLowerIndex.new.up UsersNameLowerIndex.new.up
ProjectNameLowerIndex.new.up
AddPathIndexToRedirectRoutes.new.up AddPathIndexToRedirectRoutes.new.up
end end
...@@ -11,7 +11,7 @@ module QA ...@@ -11,7 +11,7 @@ module QA
expect(page).to have_content('This is a merge request') expect(page).to have_content('This is a merge request')
expect(page).to have_content('Great feature') 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 end
end end
#!/bin/bash #!/bin/bash
mysql --user=root --host=mysql <<EOF 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'@'%'; CREATE USER IF NOT EXISTS 'gitlab'@'%';
GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%'; GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
FLUSH PRIVILEGES; FLUSH PRIVILEGES;
......
#!/bin/bash #!/bin/bash
psql -h postgres -U postgres postgres <<EOF psql -h postgres -U postgres postgres <<EOF
DROP DATABASE IF EXISTS gitlabhq_test;
CREATE DATABASE gitlabhq_test;
CREATE USER gitlab; CREATE USER gitlab;
GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gitlab;
EOF EOF
...@@ -49,20 +49,8 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml ...@@ -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 cp config/redis.shared_state.yml.example config/redis.shared_state.yml
sed -i 's/localhost/redis/g' 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 if [ "$SETUP_DB" != "false" ]; then
bundle exec rake db:drop db:create db:schema:load db:migrate setup_db
elif getent hosts postgres || getent hosts mysql; then
if [ "$GITLAB_DATABASE" = "mysql" ]; then setup_db_user_only
bundle exec rake add_limits_mysql
fi
fi 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