Commit 3effb9ac authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee-2018-07-05' into 'master'

CE upstream - 2018-07-05 12:27 UTC

Closes charts/gitlab#565 and #4266

See merge request gitlab-org/gitlab-ee!6393
parents fde933c2 c98a9fce
...@@ -375,10 +375,10 @@ package-and-qa: ...@@ -375,10 +375,10 @@ package-and-qa:
<<: *single-script-job-variables <<: *single-script-job-variables
SCRIPT_NAME: trigger-build-docs SCRIPT_NAME: trigger-build-docs
environment: environment:
name: review-docs/$CI_COMMIT_REF_NAME name: review-docs/$CI_COMMIT_REF_SLUG
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693 # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup on_stop: review-docs-cleanup
# Trigger a manual docs build in gitlab-docs only on non docs-only branches. # Trigger a manual docs build in gitlab-docs only on non docs-only branches.
...@@ -404,9 +404,8 @@ review-docs-deploy: ...@@ -404,9 +404,8 @@ review-docs-deploy:
- gem install gitlab --no-ri --no-rdoc - gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME deploy - ./$SCRIPT_NAME deploy
only: only:
- /(^docs[\/-].*|.*-docs$)/ - /(^docs[\/-].*|.*-docs$)/@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ce - /(^docs[\/-].*|.*-docs$)/@gitlab-org/gitlab-ee
- branches@gitlab-org/gitlab-ee
<<: *except-qa <<: *except-qa
# Cleanup remote environment of gitlab-docs # Cleanup remote environment of gitlab-docs
...@@ -414,7 +413,7 @@ review-docs-cleanup: ...@@ -414,7 +413,7 @@ review-docs-cleanup:
<<: *review-docs <<: *review-docs
stage: post-cleanup stage: post-cleanup
environment: environment:
name: review-docs/$CI_COMMIT_REF_NAME name: review-docs/$CI_COMMIT_REF_SLUG
action: stop action: stop
when: manual when: manual
script: script:
...@@ -432,11 +431,9 @@ cloud-native-image: ...@@ -432,11 +431,9 @@ cloud-native-image:
variables: variables:
GIT_DEPTH: "1" GIT_DEPTH: "1"
cache: {} cache: {}
before_script:
- gem install gitlab --no-rdoc --no-ri
- chmod 755 ./scripts/trigger-build-cloud-native
script: script:
- ./scripts/trigger-build-cloud-native - gem install gitlab --no-ri --no-rdoc
- ./trigger-build cng
only: only:
- tags@gitlab-org/gitlab-ce - tags@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ee - tags@gitlab-org/gitlab-ee
......
...@@ -47,7 +47,7 @@ gem 'omniauth-google-oauth2', '~> 0.5.3' ...@@ -47,7 +47,7 @@ gem 'omniauth-google-oauth2', '~> 0.5.3'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2' gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.3.0'
gem 'omniauth-twitter', '~> 1.4' gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.3' gem 'omniauth-authentiq', '~> 0.3.3'
......
...@@ -597,7 +597,7 @@ GEM ...@@ -597,7 +597,7 @@ GEM
omniauth-saml (1.10.0) omniauth-saml (1.10.0)
omniauth (~> 1.3, >= 1.3.2) omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7) ruby-saml (~> 1.7)
omniauth-shibboleth (1.2.1) omniauth-shibboleth (1.3.0)
omniauth (>= 1.0.0) omniauth (>= 1.0.0)
omniauth-twitter (1.4.0) omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1) omniauth-oauth (~> 1.1)
...@@ -1138,7 +1138,7 @@ DEPENDENCIES ...@@ -1138,7 +1138,7 @@ DEPENDENCIES
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2) omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0) omniauth-shibboleth (~> 1.3.0)
omniauth-twitter (~> 1.4) omniauth-twitter (~> 1.4)
omniauth_crowd (~> 2.2.0) omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12) org-ruby (~> 0.9.12)
......
...@@ -601,7 +601,7 @@ GEM ...@@ -601,7 +601,7 @@ GEM
omniauth-saml (1.10.0) omniauth-saml (1.10.0)
omniauth (~> 1.3, >= 1.3.2) omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7) ruby-saml (~> 1.7)
omniauth-shibboleth (1.2.1) omniauth-shibboleth (1.3.0)
omniauth (>= 1.0.0) omniauth (>= 1.0.0)
omniauth-twitter (1.4.0) omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1) omniauth-oauth (~> 1.1)
...@@ -1148,7 +1148,7 @@ DEPENDENCIES ...@@ -1148,7 +1148,7 @@ DEPENDENCIES
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2) omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0) omniauth-shibboleth (~> 1.3.0)
omniauth-twitter (~> 1.4) omniauth-twitter (~> 1.4)
omniauth_crowd (~> 2.2.0) omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12) org-ruby (~> 0.9.12)
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import _ from 'underscore'; import _ from 'underscore';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip'; import Tooltip from '~/vue_shared/directives/tooltip';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
ClipboardButton, ClipboardButton,
EditButton, EditButton,
Icon, Icon,
FileIcon,
}, },
directives: { directives: {
Tooltip, Tooltip,
...@@ -139,18 +141,20 @@ export default { ...@@ -139,18 +141,20 @@ export default {
:name="collapseIcon" :name="collapseIcon"
:size="16" :size="16"
aria-hidden="true" aria-hidden="true"
class="diff-toggle-caret" class="diff-toggle-caret append-right-5"
@click.stop="handleToggle" @click.stop="handleToggle"
/> />
<a <a
ref="titleWrapper" ref="titleWrapper"
:href="titleLink" :href="titleLink"
class="append-right-4"
> >
<i <file-icon
:class="`fa-${icon}`" :file-name="filePath"
class="fa fa-fw" :size="18"
aria-hidden="true" aria-hidden="true"
></i> css-classes="js-file-icon append-right-5"
/>
<span v-if="diffFile.renamedFile"> <span v-if="diffFile.renamedFile">
<strong <strong
v-tooltip v-tooltip
......
...@@ -24,19 +24,21 @@ export default { ...@@ -24,19 +24,21 @@ export default {
...mapGetters(['commit']), ...mapGetters(['commit']),
parallelDiffLines() { parallelDiffLines() {
return this.diffLines.map(line => { return this.diffLines.map(line => {
const parallelLine = Object.assign({}, line);
if (line.left) { if (line.left) {
Object.assign(line, { left: trimFirstCharOfLineContent(line.left) }); parallelLine.left = trimFirstCharOfLineContent(line.left);
} else { } else {
Object.assign(line, { left: { type: EMPTY_CELL_TYPE } }); parallelLine.left = { type: EMPTY_CELL_TYPE };
} }
if (line.right) { if (line.right) {
Object.assign(line, { right: trimFirstCharOfLineContent(line.right) }); parallelLine.right = trimFirstCharOfLineContent(line.right);
} else { } else {
Object.assign(line, { right: { type: EMPTY_CELL_TYPE } }); parallelLine.right = { type: EMPTY_CELL_TYPE };
} }
return line; return parallelLine;
}); });
}, },
diffLinesLength() { diffLinesLength() {
......
...@@ -155,18 +155,21 @@ export function addContextLines(options) { ...@@ -155,18 +155,21 @@ export function addContextLines(options) {
} }
} }
export function trimFirstCharOfLineContent(line) { /**
if (!line.richText) { * Trims the first char of the `richText` property when it's either a space or a diff symbol.
return line; * @param {Object} line
} * @returns {Object}
*/
const firstChar = line.richText.charAt(0); export function trimFirstCharOfLineContent(line = {}) {
const parsedLine = Object.assign({}, line);
if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
Object.assign(line, { if (line.richText) {
richText: line.richText.substring(1), const firstChar = parsedLine.richText.charAt(0);
});
if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
parsedLine.richText = line.richText.substring(1);
}
} }
return line; return parsedLine;
} }
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash'; import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service'; import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue'; import GraphGroup from './graph_group.vue';
...@@ -13,6 +15,7 @@ export default { ...@@ -13,6 +15,7 @@ export default {
Graph, Graph,
GraphGroup, GraphGroup,
EmptyState, EmptyState,
Icon,
}, },
props: { props: {
hasMetrics: { hasMetrics: {
...@@ -80,6 +83,14 @@ export default { ...@@ -80,6 +83,14 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
environmentsEndpoint: {
type: String,
required: true,
},
currentEnvironmentName: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -96,6 +107,7 @@ export default { ...@@ -96,6 +107,7 @@ export default {
this.service = new MonitoringService({ this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint, metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint, deploymentEndpoint: this.deploymentEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
}); });
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged); eventHub.$on('hoverChanged', this.hoverChanged);
...@@ -122,7 +134,11 @@ export default { ...@@ -122,7 +134,11 @@ export default {
this.service this.service
.getDeploymentData() .getDeploymentData()
.then(data => this.store.storeDeploymentData(data)) .then(data => this.store.storeDeploymentData(data))
.catch(() => new Flash('Error getting deployment information.')), .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))),
this.service
.getEnvironmentsData()
.then((data) => this.store.storeEnvironmentsData(data))
.catch(() => Flash(s__('Metrics|There was an error getting environments information.'))),
]) ])
.then(() => { .then(() => {
if (this.store.groups.length < 1) { if (this.store.groups.length < 1) {
...@@ -155,8 +171,41 @@ export default { ...@@ -155,8 +171,41 @@ export default {
<template> <template>
<div <div
v-if="!showEmptyState" v-if="!showEmptyState"
class="prometheus-graphs" class="prometheus-graphs prepend-top-10"
> >
<div class="environments d-flex align-items-center">
{{ s__('Metrics|Environment') }}
<div class="dropdown prepend-left-10">
<button
class="dropdown-menu-toggle"
data-toggle="dropdown"
type="button"
>
<span>
{{ currentEnvironmentName }}
</span>
<icon
name="chevron-down"
/>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
<li
v-for="environment in store.environmentsData"
:key="environment.latest.id"
>
<a
:href="environment.latest.metrics_path"
:class="{ 'is-active': environment.latest.name == currentEnvironmentName }"
class="dropdown-item"
>
{{ environment.latest.name }}
</a>
</li>
</ul>
</div>
</div>
</div>
<graph-group <graph-group
v-for="(groupData, index) in store.groups" v-for="(groupData, index) in store.groups"
:key="index" :key="index"
......
import axios from '../../lib/utils/axios_utils'; import axios from '../../lib/utils/axios_utils';
import statusCodes from '../../lib/utils/http_status'; import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils'; import { backOff } from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
const MAX_REQUESTS = 3; const MAX_REQUESTS = 3;
...@@ -23,9 +24,10 @@ function backOffRequest(makeRequestCallback) { ...@@ -23,9 +24,10 @@ function backOffRequest(makeRequestCallback) {
} }
export default class MonitoringService { export default class MonitoringService {
constructor({ metricsEndpoint, deploymentEndpoint }) { constructor({ metricsEndpoint, deploymentEndpoint, environmentsEndpoint }) {
this.metricsEndpoint = metricsEndpoint; this.metricsEndpoint = metricsEndpoint;
this.deploymentEndpoint = deploymentEndpoint; this.deploymentEndpoint = deploymentEndpoint;
this.environmentsEndpoint = environmentsEndpoint;
} }
getGraphsData() { getGraphsData() {
...@@ -33,7 +35,7 @@ export default class MonitoringService { ...@@ -33,7 +35,7 @@ export default class MonitoringService {
.then(resp => resp.data) .then(resp => resp.data)
.then((response) => { .then((response) => {
if (!response || !response.data) { if (!response || !response.data) {
throw new Error('Unexpected metrics data response from prometheus endpoint'); throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
} }
return response.data; return response.data;
}); });
...@@ -47,9 +49,20 @@ export default class MonitoringService { ...@@ -47,9 +49,20 @@ export default class MonitoringService {
.then(resp => resp.data) .then(resp => resp.data)
.then((response) => { .then((response) => {
if (!response || !response.deployments) { if (!response || !response.deployments) {
throw new Error('Unexpected deployment data response from prometheus endpoint'); throw new Error(s__('Metrics|Unexpected deployment data response from prometheus endpoint'));
} }
return response.deployments; return response.deployments;
}); });
} }
getEnvironmentsData() {
return axios.get(this.environmentsEndpoint)
.then(resp => resp.data)
.then((response) => {
if (!response || !response.environments) {
throw new Error(s__('Metrics|There was an error fetching the environments data, please try again'));
}
return response.environments;
});
}
} }
...@@ -24,6 +24,7 @@ export default class MonitoringStore { ...@@ -24,6 +24,7 @@ export default class MonitoringStore {
constructor() { constructor() {
this.groups = []; this.groups = [];
this.deploymentData = []; this.deploymentData = [];
this.environmentsData = [];
} }
storeMetrics(groups = []) { storeMetrics(groups = []) {
...@@ -37,6 +38,10 @@ export default class MonitoringStore { ...@@ -37,6 +38,10 @@ export default class MonitoringStore {
this.deploymentData = deploymentData; this.deploymentData = deploymentData;
} }
storeEnvironmentsData(environmentsData = []) {
this.environmentsData = environmentsData;
}
getMetricsCount() { getMetricsCount() {
return this.groups.reduce((count, group) => count + group.metrics.length, 0); return this.groups.reduce((count, group) => count + group.metrics.length, 0);
} }
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import imageDiffHelper from '~/image_diff/helpers/index'; import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
export default { export default {
components: { components: {
DiffFileHeader, DiffFileHeader,
SkeletonLoadingContainer, SkeletonLoadingContainer,
},
props: {
discussion: {
type: Object,
required: true,
}, },
}, props: {
data() { discussion: {
return { type: Object,
error: false, required: true,
}; },
},
computed: {
...mapState({
noteableData: state => state.notes.noteableData,
}),
hasTruncatedDiffLines() {
return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
}, },
isDiscussionsExpanded() { data() {
return true; // TODO: @fatihacet - Fix this. return {
error: false,
};
}, },
isCollapsed() { computed: {
return this.diffFile.collapsed || false; ...mapState({
}, noteableData: state => state.notes.noteableData,
isImageDiff() { }),
return !this.diffFile.text; hasTruncatedDiffLines() {
}, return this.discussion.truncatedDiffLines &&
diffFileClass() { this.discussion.truncatedDiffLines.length !== 0;
const { text } = this.diffFile; },
return text ? 'text-file' : 'js-image-file'; isDiscussionsExpanded() {
}, return true; // TODO: @fatihacet - Fix this.
diffFile() { },
return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true }); isCollapsed() {
}, return this.diffFile.collapsed || false;
imageDiffHtml() { },
return this.discussion.imageDiffHtml; isImageDiff() {
}, return !this.diffFile.text;
currentUser() { },
return this.noteableData.current_user; diffFileClass() {
}, const { text } = this.diffFile;
userColorScheme() { return text ? 'text-file' : 'js-image-file';
return window.gon.user_color_scheme; },
}, diffFile() {
normalizedDiffLines() { return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true });
const lines = this.discussion.truncatedDiffLines || []; },
imageDiffHtml() {
return this.discussion.imageDiffHtml;
},
currentUser() {
return this.noteableData.current_user;
},
userColorScheme() {
return window.gon.user_color_scheme;
},
normalizedDiffLines() {
if (this.discussion.truncatedDiffLines) {
return this.discussion.truncatedDiffLines.map(line =>
trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)),
);
}
return lines.map(line => trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line))); return [];
},
}, },
}, mounted() {
mounted() { if (this.isImageDiff) {
if (this.isImageDiff) { const canCreateNote = false;
const canCreateNote = false; const renderCommentBadge = true;
const renderCommentBadge = true; imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge); } else if (!this.hasTruncatedDiffLines) {
} else if (!this.hasTruncatedDiffLines) { this.fetchDiff();
this.fetchDiff(); }
}
},
methods: {
...mapActions(['fetchDiscussionDiffLines']),
rowTag(html) {
return html.outerHTML ? 'tr' : 'template';
}, },
fetchDiff() { methods: {
this.error = false; ...mapActions(['fetchDiscussionDiffLines']),
this.fetchDiscussionDiffLines(this.discussion) rowTag(html) {
.then(this.highlight) return html.outerHTML ? 'tr' : 'template';
.catch(() => { },
this.error = true; fetchDiff() {
}); this.error = false;
this.fetchDiscussionDiffLines(this.discussion)
.then(this.highlight)
.catch(() => {
this.error = true;
});
},
}, },
}, };
};
</script> </script>
<template> <template>
......
...@@ -20,6 +20,7 @@ export default class ShortcutsNavigation extends Shortcuts { ...@@ -20,6 +20,7 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes')); Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments')); Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
Mousetrap.bind('g l', () => findAndFollowLink('.shortcuts-metrics'));
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');
......
...@@ -29,8 +29,8 @@ ...@@ -29,8 +29,8 @@
methods: { methods: {
isValid(form) { isValid(form) {
return !form || return !form ||
form.find('.js-vue-markdown-field').length || form.find('.js-vue-markdown-field').length &&
$(this.$el).closest('form') === form[0]; $(this.$el).closest('form')[0] === form[0];
}, },
previewMarkdownTab(event, form) { previewMarkdownTab(event, form) {
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
} }
svg { svg {
vertical-align: text-bottom; vertical-align: middle;
} }
} }
......
...@@ -384,6 +384,23 @@ ...@@ -384,6 +384,23 @@
} }
} }
.prometheus-graphs {
.environments {
.dropdown-menu-toggle {
svg {
position: absolute;
right: 5%;
top: 25%;
}
}
.dropdown-menu-toggle,
.dropdown-menu {
width: 240px;
}
}
}
.environments-actions { .environments-actions {
.external-url, .external-url,
.monitoring-url, .monitoring-url,
......
...@@ -191,6 +191,22 @@ ...@@ -191,6 +191,22 @@
} }
} }
.initialize-with-readme-setting {
.form-check {
margin-bottom: 10px;
.option-title {
font-weight: $gl-font-weight-normal;
display: inline-block;
color: $gl-text-color;
}
.option-description {
color: $project-option-descr-color;
}
}
}
.nested-settings { .nested-settings {
padding-left: 20px; padding-left: 20px;
} }
......
...@@ -122,6 +122,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -122,6 +122,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
end end
def metrics_redirect
environment = project.default_environment
if environment
redirect_to environment_metrics_path(environment)
else
render :empty
end
end
def metrics def metrics
# Currently, this acts as a hint to load the metrics details into the cache # Currently, this acts as a hint to load the metrics details into the cache
# if they aren't there already # if they aren't there already
......
...@@ -349,6 +349,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -349,6 +349,7 @@ class ProjectsController < Projects::ApplicationController
:visibility_level, :visibility_level,
:template_name, :template_name,
:merge_method, :merge_method,
:initialize_with_readme,
project_feature_attributes: %i[ project_feature_attributes: %i[
builds_access_level builds_access_level
......
...@@ -87,8 +87,7 @@ class Group < Namespace ...@@ -87,8 +87,7 @@ class Group < Namespace
end end
def public_or_visible_to_user(user) def public_or_visible_to_user(user)
where( where('id IN (?) OR namespaces.visibility_level IN (?)',
'id IN (?) OR namespaces.visibility_level IN (?)',
user.authorized_groups.select(:id), user.authorized_groups.select(:id),
Gitlab::VisibilityLevel.levels_for_user(user)) Gitlab::VisibilityLevel.levels_for_user(user))
end end
......
...@@ -1792,6 +1792,15 @@ class Project < ActiveRecord::Base ...@@ -1792,6 +1792,15 @@ class Project < ActiveRecord::Base
end end
end end
def default_environment
production_first = "(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC"
environments
.with_state(:available)
.reorder(production_first)
.first
end
def secret_variables_for(ref:, environment: nil) def secret_variables_for(ref:, environment: nil)
# EE would use the environment # EE would use the environment
if protected_for?(ref) if protected_for?(ref)
......
...@@ -25,6 +25,8 @@ class DiffFileEntity < Grape::Entity ...@@ -25,6 +25,8 @@ class DiffFileEntity < Grape::Entity
expose :can_modify_blob do |diff_file| expose :can_modify_blob do |diff_file|
merge_request = options[:merge_request] merge_request = options[:merge_request]
next unless diff_file.blob
if merge_request&.source_project && current_user if merge_request&.source_project && current_user
can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch) can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
else else
...@@ -108,6 +110,7 @@ class DiffFileEntity < Grape::Entity ...@@ -108,6 +110,7 @@ class DiffFileEntity < Grape::Entity
project = merge_request.target_project project = merge_request.target_project
next unless project next unless project
next unless diff_file.content_sha
project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path)) project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
end end
...@@ -125,6 +128,8 @@ class DiffFileEntity < Grape::Entity ...@@ -125,6 +128,8 @@ class DiffFileEntity < Grape::Entity
end end
expose :context_lines_path, if: -> (diff_file, _) { diff_file.text? } do |diff_file| expose :context_lines_path, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
next unless diff_file.content_sha
project_blob_diff_path(diff_file.repository.project, tree_join(diff_file.content_sha, diff_file.file_path)) project_blob_diff_path(diff_file.repository.project, tree_join(diff_file.content_sha, diff_file.file_path))
end end
......
...@@ -3,7 +3,7 @@ class DiscussionEntity < Grape::Entity ...@@ -3,7 +3,7 @@ class DiscussionEntity < Grape::Entity
include NotesHelper include NotesHelper
expose :id, :reply_id expose :id, :reply_id
expose :position, if: -> (d, _) { d.diff_discussion? } expose :position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? }
expose :line_code, if: -> (d, _) { d.diff_discussion? } expose :line_code, if: -> (d, _) { d.diff_discussion? }
expose :expanded?, as: :expanded expose :expanded?, as: :expanded
expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? } expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? }
......
...@@ -4,6 +4,8 @@ module Projects ...@@ -4,6 +4,8 @@ module Projects
def initialize(user, params) def initialize(user, params)
@current_user, @params = user, params.dup @current_user, @params = user, params.dup
@skip_wiki = @params.delete(:skip_wiki)
@initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme))
end end
def execute def execute
...@@ -13,7 +15,6 @@ module Projects ...@@ -13,7 +15,6 @@ module Projects
forked_from_project_id = params.delete(:forked_from_project_id) forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data) import_data = params.delete(:import_data)
@skip_wiki = params.delete(:skip_wiki)
@project = Project.new(params) @project = Project.new(params)
...@@ -104,6 +105,8 @@ module Projects ...@@ -104,6 +105,8 @@ module Projects
setup_authorizations setup_authorizations
current_user.invalidate_personal_projects_count current_user.invalidate_personal_projects_count
create_readme if @initialize_with_readme
end end
# Refresh the current user's authorizations inline (so they can access the # Refresh the current user's authorizations inline (so they can access the
...@@ -118,6 +121,17 @@ module Projects ...@@ -118,6 +121,17 @@ module Projects
end end
end end
def create_readme
commit_attrs = {
branch_name: 'master',
commit_message: 'Initial commit',
file_path: 'README.md',
file_content: "# #{@project.name}\n\n#{@project.description}"
}
Files::CreateService.new(@project, current_user, commit_attrs).execute
end
def skip_wiki? def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki !@project.feature_available?(:wiki, current_user) || @skip_wiki
end end
......
# frozen_string_literal: true
class AttachmentUploader < GitlabUploader class AttachmentUploader < GitlabUploader
include RecordsUploads::Concern include RecordsUploads::Concern
include ObjectStorage::Concern include ObjectStorage::Concern
......
# frozen_string_literal: true
class AvatarUploader < GitlabUploader class AvatarUploader < GitlabUploader
include UploaderHelper include UploaderHelper
include RecordsUploads::Concern include RecordsUploads::Concern
......
# frozen_string_literal: true
class FaviconUploader < AttachmentUploader class FaviconUploader < AttachmentUploader
EXTENSION_WHITELIST = %w[png ico].freeze EXTENSION_WHITELIST = %w[png ico].freeze
......
# frozen_string_literal: true
class FileMover class FileMover
attr_reader :secret, :file_name, :model, :update_field attr_reader :secret, :file_name, :model, :update_field
......
# frozen_string_literal: true
# This class breaks the actual CarrierWave concept. # This class breaks the actual CarrierWave concept.
# Every uploader should use a base_dir that is model agnostic so we can build # Every uploader should use a base_dir that is model agnostic so we can build
# back URLs from base_dir-relative paths saved in the `Upload` model. # back URLs from base_dir-relative paths saved in the `Upload` model.
...@@ -117,7 +119,7 @@ class FileUploader < GitlabUploader ...@@ -117,7 +119,7 @@ class FileUploader < GitlabUploader
end end
def markdown_link def markdown_link
markdown = "[#{markdown_name}](#{secure_url})" markdown = +"[#{markdown_name}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous? markdown.prepend("!") if image_or_video? || dangerous?
markdown markdown
end end
......
# frozen_string_literal: true
class GitlabUploader < CarrierWave::Uploader::Base class GitlabUploader < CarrierWave::Uploader::Base
class_attribute :options class_attribute :options
......
# frozen_string_literal: true
class JobArtifactUploader < GitlabUploader class JobArtifactUploader < GitlabUploader
extend Workhorse::UploadPath extend Workhorse::UploadPath
include ObjectStorage::Concern include ObjectStorage::Concern
......
# frozen_string_literal: true
class LegacyArtifactUploader < GitlabUploader class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath extend Workhorse::UploadPath
include ObjectStorage::Concern include ObjectStorage::Concern
......
# frozen_string_literal: true
class LfsObjectUploader < GitlabUploader class LfsObjectUploader < GitlabUploader
extend Workhorse::UploadPath extend Workhorse::UploadPath
include ObjectStorage::Concern include ObjectStorage::Concern
......
# frozen_string_literal: true
class NamespaceFileUploader < FileUploader class NamespaceFileUploader < FileUploader
# Re-Override # Re-Override
def self.root def self.root
......
# frozen_string_literal: true
require 'fog/aws' require 'fog/aws'
require 'carrierwave/storage/fog' require 'carrierwave/storage/fog'
......
# frozen_string_literal: true
class PersonalFileUploader < FileUploader class PersonalFileUploader < FileUploader
# Re-Override # Re-Override
def self.root def self.root
......
# frozen_string_literal: true
module RecordsUploads module RecordsUploads
module Concern module Concern
extend ActiveSupport::Concern extend ActiveSupport::Concern
......
# frozen_string_literal: true
# Extra methods for uploader # Extra methods for uploader
module UploaderHelper module UploaderHelper
IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze
......
# frozen_string_literal: true
module Workhorse module Workhorse
module UploadPath module UploadPath
def workhorse_upload_path def workhorse_upload_path
......
# frozen_string_literal: true
class AbstractPathValidator < ActiveModel::EachValidator class AbstractPathValidator < ActiveModel::EachValidator
extend Gitlab::EncodingHelper extend Gitlab::EncodingHelper
......
# frozen_string_literal: true
class CertificateFingerprintValidator < ActiveModel::EachValidator class CertificateFingerprintValidator < ActiveModel::EachValidator
FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/.freeze FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/.freeze
......
# frozen_string_literal: true
# UrlValidator # UrlValidator
# #
# Custom validator for private keys. # Custom validator for private keys.
......
# frozen_string_literal: true
# UrlValidator # UrlValidator
# #
# Custom validator for private keys. # Custom validator for private keys.
......
# frozen_string_literal: true
# ClusterNameValidator # ClusterNameValidator
# #
# Custom validator for ClusterName. # Custom validator for ClusterName.
......
# frozen_string_literal: true
# ColorValidator # ColorValidator
# #
# Custom validator for web color codes. It requires the leading hash symbol and # Custom validator for web color codes. It requires the leading hash symbol and
......
# frozen_string_literal: true
# CronTimezoneValidator # CronTimezoneValidator
# #
# Custom validator for CronTimezone. # Custom validator for CronTimezone.
......
# frozen_string_literal: true
# CronValidator # CronValidator
# #
# Custom validator for Cron. # Custom validator for Cron.
......
# frozen_string_literal: true
# DurationValidator # DurationValidator
# #
# Validate the format conforms with ChronicDuration # Validate the format conforms with ChronicDuration
......
# frozen_string_literal: true
class EmailValidator < ActiveModel::EachValidator class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
record.errors.add(attribute, :invalid) unless value =~ Devise.email_regexp record.errors.add(attribute, :invalid) unless value =~ Devise.email_regexp
......
# frozen_string_literal: true
class KeyRestrictionValidator < ActiveModel::EachValidator class KeyRestrictionValidator < ActiveModel::EachValidator
FORBIDDEN = -1 FORBIDDEN = -1
......
# frozen_string_literal: true
# LineCodeValidator # LineCodeValidator
# #
# Custom validator for GitLab line codes. # Custom validator for GitLab line codes.
......
# frozen_string_literal: true
# NamespaceNameValidator # NamespaceNameValidator
# #
# Custom validator for GitLab namespace name strings. # Custom validator for GitLab namespace name strings.
......
# frozen_string_literal: true
class NamespacePathValidator < AbstractPathValidator class NamespacePathValidator < AbstractPathValidator
extend Gitlab::EncodingHelper extend Gitlab::EncodingHelper
......
# frozen_string_literal: true
class ProjectPathValidator < AbstractPathValidator class ProjectPathValidator < AbstractPathValidator
extend Gitlab::EncodingHelper extend Gitlab::EncodingHelper
......
# frozen_string_literal: true
# PublicUrlValidator # PublicUrlValidator
# #
# Custom validator for URLs. This validator works like UrlValidator but # Custom validator for URLs. This validator works like UrlValidator but
......
# frozen_string_literal: true
class TopLevelGroupValidator < ActiveModel::EachValidator class TopLevelGroupValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
if value&.subgroup? if value&.subgroup?
......
# frozen_string_literal: true
# UrlValidator # UrlValidator
# #
# Custom validator for URLs. # Custom validator for URLs.
......
# frozen_string_literal: true
# VariableDuplicatesValidator # VariableDuplicatesValidator
# #
# This validator is designed for especially the following condition # This validator is designed for especially the following condition
...@@ -22,8 +24,8 @@ class VariableDuplicatesValidator < ActiveModel::EachValidator ...@@ -22,8 +24,8 @@ class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_duplicates(record, attribute, values) def validate_duplicates(record, attribute, values)
duplicates = values.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first) duplicates = values.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first)
if duplicates.any? if duplicates.any?
error_message = "have duplicate values (#{duplicates.join(", ")})" error_message = +"have duplicate values (#{duplicates.join(", ")})"
error_message += " for #{values.first.send(options[:scope])} scope" if options[:scope] # rubocop:disable GitlabSecurity/PublicSend error_message << " for #{values.first.send(options[:scope])} scope" if options[:scope] # rubocop:disable GitlabSecurity/PublicSend
record.errors.add(attribute, error_message) record.errors.add(attribute, error_message)
end end
end end
......
...@@ -179,6 +179,12 @@ ...@@ -179,6 +179,12 @@
%kbd e %kbd e
%td %td
Go to environments Go to environments
%tr
%td.shortcut
%kbd g
%kbd l
%td
Go to metrics
%tr %tr
%td.shortcut %td.shortcut
%kbd g %kbd g
......
...@@ -196,7 +196,7 @@ ...@@ -196,7 +196,7 @@
- if project_nav_tab? :operations - if project_nav_tab? :operations
= nav_link(controller: [:environments, :clusters, :user, :gcp]) do = nav_link(controller: [:environments, :clusters, :user, :gcp]) do
= link_to project_environments_path(@project), class: 'shortcuts-operations' do = link_to metrics_project_environments_path(@project), class: 'shortcuts-operations' do
.nav-icon-container .nav-icon-container
= sprite_icon('cloud-gear') = sprite_icon('cloud-gear')
%span.nav-item-name %span.nav-item-name
...@@ -204,14 +204,19 @@ ...@@ -204,14 +204,19 @@
%ul.sidebar-sub-level-items %ul.sidebar-sub-level-items
= nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
= link_to project_environments_path(@project) do = link_to metrics_project_environments_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('Operations') = _('Operations')
%li.divider.fly-out-top-item %li.divider.fly-out-top-item
- if project_nav_tab? :environments - if project_nav_tab? :environments
= nav_link(controller: :environments) do = nav_link(controller: :environments, action: [:metrics, :metrics_redirect]) do
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do = link_to metrics_project_environments_path(@project), title: _('Metrics'), class: 'shortcuts-metrics' do
%span
= _('Metrics')
= nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
= link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments' do
%span %span
= _('Environments') = _('Environments')
......
...@@ -40,5 +40,15 @@ ...@@ -40,5 +40,15 @@
= link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer' = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer'
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
.form-group.row.initialize-with-readme-setting
%div{ :class => "col-sm-12" }
.form-check
= check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input'
= label_tag 'project[initialize_with_readme]', class: 'form-check-label' do
.option-title
%strong Initialize repository with a README
.option-description
Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
.modal{ id: "revoke-modal-#{token.id}" } .modal{ id: "revoke-modal-#{token.id}", tabindex: -1 }
.modal-dialog .modal-dialog
.modal-content .modal-content
.modal-header .modal-header
......
- page_title _("Metrics")
.row
.col-sm-12
.svg-content
= image_tag 'illustrations/operations_metrics_empty.svg'
.row.empty-environments
.col-sm-12.text-center
%h4
= s_('Metrics|No deployed environments')
.state-description
= s_('Metrics|Check out the CI/CD documentation on deploying to an environment')
.prepend-top-10
= link_to s_("Metrics|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success'
...@@ -2,15 +2,9 @@ ...@@ -2,15 +2,9 @@
- page_title "Metrics for environment", @environment.name - page_title "Metrics for environment", @environment.name
.prometheus-container{ class: container_class } .prometheus-container{ class: container_class }
.top-area
.row
.col-sm-6
%h3
Environment:
= link_to @environment.name, environment_path(@environment)
#prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'), #prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'),
"clusters-path": project_clusters_path(@project), "clusters-path": project_clusters_path(@project),
"current-environment-name": @environment.name,
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'), "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'), "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'), "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
...@@ -18,6 +12,7 @@ ...@@ -18,6 +12,7 @@
"empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'), "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json), "metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json),
"deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json), "deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json),
"environments-endpoint": project_environments_path(@project, format: :json),
"project-path": project_path(@project), "project-path": project_path(@project),
"tags-path": project_tags_path(@project), "tags-path": project_tags_path(@project),
"has-metrics": "#{@environment.has_metrics?}" } } "has-metrics": "#{@environment.has_metrics?}" } }
...@@ -3,34 +3,34 @@ ...@@ -3,34 +3,34 @@
.d-none.d-sm-block .d-none.d-sm-block
- if can?(current_user, :update_project_snippet, @snippet) - if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_project_snippet_path(@project, @snippet), class: "btn btn-grouped" do = link_to edit_project_snippet_path(@project, @snippet), class: "btn btn-grouped" do
Edit = _('Edit')
- if can?(current_user, :update_project_snippet, @snippet) - if can?(current_user, :update_project_snippet, @snippet)
= link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do = link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-inverted btn-remove", title: _('Delete Snippet') do
Delete = _('Delete')
- if can?(current_user, :create_project_snippet, @project) - if can?(current_user, :create_project_snippet, @project)
= link_to new_project_snippet_path(@project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do = link_to new_project_snippet_path(@project), class: 'btn btn-grouped btn-inverted btn-create', title: _("New snippet") do
New snippet = _('New snippet')
- if @snippet.submittable_as_spam_by?(current_user) - if @snippet.submittable_as_spam_by?(current_user)
= link_to 'Submit as spam', mark_as_spam_project_snippet_path(@project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' = link_to _('Submit as spam'), mark_as_spam_project_snippet_path(@project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: _('Submit as spam')
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.d-block.d-sm-none.dropdown .d-block.d-sm-none.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
Options = _('Options')
= icon('caret-down') = icon('caret-down')
.dropdown-menu.dropdown-menu-full-width .dropdown-menu.dropdown-menu-full-width
%ul %ul
- if can?(current_user, :create_project_snippet, @project) - if can?(current_user, :create_project_snippet, @project)
%li %li
= link_to new_project_snippet_path(@project), title: "New snippet" do = link_to new_project_snippet_path(@project), title: _("New snippet") do
New snippet = _('New snippet')
- if can?(current_user, :update_project_snippet, @snippet) - if can?(current_user, :update_project_snippet, @snippet)
%li %li
= link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do = link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: _("Are you sure?") }, title: _('Delete Snippet') do
Delete = _('Delete')
- if can?(current_user, :update_project_snippet, @snippet) - if can?(current_user, :update_project_snippet, @snippet)
%li %li
= link_to edit_project_snippet_path(@project, @snippet) do = link_to edit_project_snippet_path(@project, @snippet) do
Edit = _('Edit')
- if @snippet.submittable_as_spam_by?(current_user) - if @snippet.submittable_as_spam_by?(current_user)
%li %li
= link_to 'Submit as spam', mark_as_spam_project_snippet_path(@project, @snippet), method: :post = link_to _('Submit as spam'), mark_as_spam_project_snippet_path(@project, @snippet), method: :post
- add_to_breadcrumbs "Snippets", project_snippets_path(@project) - add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title @snippet.to_reference - breadcrumb_title @snippet.to_reference
- page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets" - page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
%h3.page-title %h3.page-title
Edit Snippet = _("Edit Snippet")
%hr %hr
= render "shared/snippets/form", url: project_snippet_path(@project, @snippet) = render "shared/snippets/form", url: project_snippet_path(@project, @snippet)
- page_title "Snippets" - page_title _("Snippets")
- if current_user - if current_user
.top-area .top-area
...@@ -7,6 +7,6 @@ ...@@ -7,6 +7,6 @@
.nav-controls .nav-controls
- if can?(current_user, :create_project_snippet, @project) - if can?(current_user, :create_project_snippet, @project)
= link_to "New snippet", new_project_snippet_path(@project), class: "btn btn-new", title: "New snippet" = link_to _("New snippet"), new_project_snippet_path(@project), class: "btn btn-new", title: _("New snippet")
= render 'snippets/snippets' = render 'snippets/snippets'
- add_to_breadcrumbs "Snippets", project_snippets_path(@project) - add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title "New" - breadcrumb_title _("New")
- page_title "New Snippets" - page_title _("New Snippets")
%h3.page-title %h3.page-title
New Snippet = _('New Snippet')
%hr %hr
= render "shared/snippets/form", url: project_snippets_path(@project, @snippet) = render "shared/snippets/form", url: project_snippets_path(@project, @snippet)
- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout - @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout
- add_to_breadcrumbs "Snippets", project_snippets_path(@project) - add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title @snippet.to_reference - breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets" - page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
= render 'shared/snippets/header' = render 'shared/snippets/header'
......
---
title: Do not use '-f' with 'rm' in gitlab-basics docs
merge_request: 18027
author: Elias Werberich
type: changed
---
title: Add option to add README when creating a project
merge_request: 20335
author:
type: added
---
title: Fixed bug when editing a comment in an issue,the preview mode is toggled in
the main textarea
merge_request: 20112
author: Constance Okoghenun
type: fixed
---
title: Add environment dropdown for the metrics page
merge_request: 19833
author:
type: changed
---
title: Improves performance of mr code, by fixing the state being mutated outside
of the store in the util function trimFirstCharOfLineContent and in map operations.
Avoids map operation in an empty array. Adds specs to the trimFirstCharOfLineContent
function
merge_request: 20380
author: filipa
type: performance
---
title: Close revoke deploy token modal on escape keypress
merge_request: 20347
author: George Tsiolis
type: changed
---
title: Enable frozen string in apps/validators/*.rb
merge_request: 20382
author: gfyoung
type: other
---
title: Enable frozen string in apps/validators/*.rb
merge_request: 20220
author: gfyoung
type: other
...@@ -265,6 +265,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -265,6 +265,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
collection do collection do
get :metrics, action: :metrics_redirect
get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ } get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
end end
......
...@@ -43,7 +43,19 @@ exclude shibboleth URLs from rewriting, add "RewriteCond %{REQUEST_URI} !/Shibbo ...@@ -43,7 +43,19 @@ exclude shibboleth URLs from rewriting, add "RewriteCond %{REQUEST_URI} !/Shibbo
RequestHeader set X_FORWARDED_PROTO 'https' RequestHeader set X_FORWARDED_PROTO 'https'
``` ```
1. Edit /etc/gitlab/gitlab.rb configuration file, your shibboleth attributes should be in form of "HTTP_ATTRIBUTE" and you should adjust them to your need and environment. Add any other configuration you need. 1. Edit /etc/gitlab/gitlab.rb configuration file to enable OmniAuth and add
Shibboleth as an OmniAuth provider. User attributes will be sent from the
Apache reverse proxy to GitLab as headers with the names from the Shibboleth
attribute mapping. Therefore the values of the `args` hash
should be in the form of `"HTTP_ATTRIBUTE"`. The keys in the hash are arguments
to the [OmniAuth::Strategies::Shibboleth class](https://github.com/toyokazu/omniauth-shibboleth/blob/master/lib/omniauth/strategies/shibboleth.rb)
and are documented by the [omniauth-shibboleth gem](https://github.com/toyokazu/omniauth-shibboleth)
(take care to note the version of the gem packaged with GitLab). If some of
your users appear to be authenticated by Shibboleth and Apache, but GitLab
rejects their account with a URI that contains "e-mail is invalid" then your
Shibboleth Identity Provider or Attribute Authority may be asserting multiple
e-mail addresses. In this instance, you might consider setting the
`multi_values` argument to `first`.
File should look like this: File should look like this:
``` ```
...@@ -58,14 +70,15 @@ gitlab_rails['omniauth_block_auto_created_users'] = false ...@@ -58,14 +70,15 @@ gitlab_rails['omniauth_block_auto_created_users'] = false
gitlab_rails['omniauth_enabled'] = true gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_providers'] = [ gitlab_rails['omniauth_providers'] = [
{ {
"name" => 'shibboleth', "name" => "'shibboleth"',
"args" => { "label" => "Text for Login Button",
"shib_session_id_field" => "HTTP_SHIB_SESSION_ID", "args" => {
"shib_session_id_field" => "HTTP_SHIB_SESSION_ID",
"shib_application_id_field" => "HTTP_SHIB_APPLICATION_ID", "shib_application_id_field" => "HTTP_SHIB_APPLICATION_ID",
"uid_field" => 'HTTP_EPPN', "uid_field" => 'HTTP_EPPN',
"name_field" => 'HTTP_CN', "name_field" => 'HTTP_CN',
"info_fields" => { "email" => 'HTTP_MAIL'} "info_fields" => { "email" => 'HTTP_MAIL'}
} }
} }
] ]
......
...@@ -247,6 +247,7 @@ module Gitlab ...@@ -247,6 +247,7 @@ module Gitlab
lines = highlighted_diff_lines lines = highlighted_diff_lines
return if lines.empty? return if lines.empty?
return if blob.nil?
last_line = lines.last last_line = lines.last
......
...@@ -429,7 +429,7 @@ module Gitlab ...@@ -429,7 +429,7 @@ module Gitlab
def self.count_stack def self.count_stack
return unless RequestStore.active? return unless RequestStore.active?
stack_string = caller.drop(1).join("\n") stack_string = Gitlab::Profiler.clean_backtrace(caller).drop(1).join("\n")
RequestStore.store[:stack_counter] ||= Hash.new RequestStore.store[:stack_counter] ||= Hash.new
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-01 16:35+1000\n" "POT-Creation-Date: 2018-07-01 21:24+1000\n"
"PO-Revision-Date: 2018-07-01 16:35+1000\n" "PO-Revision-Date: 2018-07-01 21:24+1000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -1815,6 +1815,9 @@ msgstr "" ...@@ -1815,6 +1815,9 @@ msgstr ""
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
msgid "Delete Snippet"
msgstr ""
msgid "Delete list" msgid "Delete list"
msgstr "" msgstr ""
...@@ -2036,6 +2039,9 @@ msgstr "" ...@@ -2036,6 +2039,9 @@ msgstr ""
msgid "Edit Pipeline Schedule %{id}" msgid "Edit Pipeline Schedule %{id}"
msgstr "" msgstr ""
msgid "Edit Snippet"
msgstr ""
msgid "Edit files in the editor and commit changes here" msgid "Edit files in the editor and commit changes here"
msgstr "" msgstr ""
...@@ -2978,6 +2984,9 @@ msgstr "" ...@@ -2978,6 +2984,9 @@ msgstr ""
msgid "Nav|Sign out and sign in with a different account" msgid "Nav|Sign out and sign in with a different account"
msgstr "" msgstr ""
msgid "New"
msgstr ""
msgid "New Identity" msgid "New Identity"
msgstr "" msgstr ""
...@@ -2998,6 +3007,12 @@ msgstr "" ...@@ -2998,6 +3007,12 @@ msgstr ""
msgid "New Pipeline Schedule" msgid "New Pipeline Schedule"
msgstr "" msgstr ""
msgid "New Snippet"
msgstr ""
msgid "New Snippets"
msgstr ""
msgid "New branch" msgid "New branch"
msgstr "" msgstr ""
...@@ -4301,6 +4316,9 @@ msgstr "" ...@@ -4301,6 +4316,9 @@ msgstr ""
msgid "Subgroups" msgid "Subgroups"
msgstr "" msgstr ""
msgid "Submit as spam"
msgstr ""
msgid "Subscribe" msgid "Subscribe"
msgstr "" msgstr ""
......
...@@ -22,14 +22,6 @@ module QA ...@@ -22,14 +22,6 @@ module QA
element :squash_checkbox element :squash_checkbox
end end
def rebase!
click_element :mr_rebase_button
wait(reload: false) do
has_text?('Fast-forward merge without a merge commit')
end
end
def fast_forward_possible? def fast_forward_possible?
!has_text?('Fast-forward merge is not possible') !has_text?('Fast-forward merge is not possible')
end end
...@@ -40,7 +32,35 @@ module QA ...@@ -40,7 +32,35 @@ module QA
has_selector?('.accept-merge-request') has_selector?('.accept-merge-request')
end end
def rebase!
# The rebase button is disabled on load
wait do
has_css?(element_selector_css(:mr_rebase_button))
end
# The rebase button is enabled via JS
wait(reload: false) do
!first(element_selector_css(:mr_rebase_button)).disabled?
end
click_element :mr_rebase_button
wait(reload: false) do
has_text?('Fast-forward merge without a merge commit')
end
end
def merge! def merge!
# The merge button is disabled on load
wait do
has_css?(element_selector_css(:merge_button))
end
# The merge button is enabled via JS
wait(reload: false) do
!first(element_selector_css(:merge_button)).disabled?
end
click_element :merge_button click_element :merge_button
wait(reload: false) do wait(reload: false) do
...@@ -49,10 +69,16 @@ module QA ...@@ -49,10 +69,16 @@ module QA
end end
def mark_to_squash def mark_to_squash
wait(reload: true) do # The squash checkbox is disabled on load
wait do
has_css?(element_selector_css(:squash_checkbox)) has_css?(element_selector_css(:squash_checkbox))
end end
# The squash checkbox is enabled via JS
wait(reload: false) do
!first(element_selector_css(:squash_checkbox)).disabled?
end
click_element :squash_checkbox click_element :squash_checkbox
end end
end end
......
...@@ -24,6 +24,8 @@ module QA ...@@ -24,6 +24,8 @@ module QA
merge_request.visit! merge_request.visit!
expect(page).to have_text('to be squashed')
Page::MergeRequest::Show.perform do |merge_request_page| Page::MergeRequest::Show.perform do |merge_request_page|
merge_request_page.mark_to_squash merge_request_page.mark_to_squash
merge_request_page.merge! merge_request_page.merge!
......
...@@ -16,18 +16,14 @@ end ...@@ -16,18 +16,14 @@ end
GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze
# #
# Truncate the remote docs branch name if it's more than 63 characters # Truncate the remote docs branch name otherwise we hit the filesystem
# otherwise we hit the filesystem limit and the directory name where # limit and the directory name where NGINX serves the site won't match
# NGINX serves the site won't match the branch name. # the branch name.
# #
def docs_branch def docs_branch
# The maximum string length a file can have on a filesystem (ext4) # The maximum string length a file can have on a filesystem (ext4)
# is 63 characters. Let's use something smaller to be 100% sure. # is 63 characters. CI_ENVIRONMENT_SLUG is limited to 24 characters.
max = 42 ENV["CI_ENVIRONMENT_SLUG"]
# Prefix the remote branch with the slug of the project in order
# to avoid name conflicts in the rare case the branch name already
# exists in the docs repo and truncate to max length.
"#{slug}-#{ENV["CI_ENVIRONMENT_SLUG"]}"[0...max]
end end
# #
......
...@@ -277,6 +277,25 @@ describe Projects::EnvironmentsController do ...@@ -277,6 +277,25 @@ describe Projects::EnvironmentsController do
end end
end end
describe 'GET #metrics_redirect' do
let(:project) { create(:project) }
it 'redirects to environment if it exists' do
environment = create(:environment, name: 'production', project: project)
get :metrics_redirect, namespace_id: project.namespace, project_id: project
expect(response).to redirect_to(environment_metrics_path(environment))
end
it 'redirects to empty page if no environment exists' do
get :metrics_redirect, namespace_id: project.namespace, project_id: project
expect(response).to be_ok
expect(response).to render_template 'empty'
end
end
describe 'GET #metrics' do describe 'GET #metrics' do
before do before do
allow(controller).to receive(:environment).and_return(environment) allow(controller).to receive(:environment).and_return(environment)
......
...@@ -63,6 +63,14 @@ describe "User comments on issue", :js do ...@@ -63,6 +63,14 @@ describe "User comments on issue", :js do
page.within(".current-note-edit-form") do page.within(".current-note-edit-form") do
fill_in("note[note]", with: comment) fill_in("note[note]", with: comment)
find('textarea').send_keys [:control, :shift, 'p']
expect(page).to have_selector('.current-note-edit-form .md-preview-holder')
expect(page.find('.current-note-edit-form .md-preview-holder p')).to have_content(comment)
end
expect(page).to have_selector('.new-note .note-textarea')
page.within(".current-note-edit-form") do
click_button("Save comment") click_button("Save comment")
end end
......
...@@ -48,6 +48,15 @@ describe 'New project' do ...@@ -48,6 +48,15 @@ describe 'New project' do
end end
end end
context 'Readme selector' do
it 'shows the initialize with Readme checkbox' do
visit new_project_path
expect(page).to have_css('input#project_initialize_with_readme')
expect(page).to have_content('Initialize repository with a README')
end
end
context 'Namespace selector' do context 'Namespace selector' do
context 'with user namespace' do context 'with user namespace' do
before do before do
......
require 'rails_helper'
describe 'Repository Settings > User sees revoke deploy token modal', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:role) { :developer }
let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) }
before do
project.add_role(user, role)
sign_in(user)
visit(project_settings_repository_path(project))
click_link('Revoke')
end
it 'shows the revoke deploy token modal' do
expect(page).to have_content('You are about to revoke')
end
it 'closes the revoke deploy token modal with escape keypress' do
find('.modal.show').send_keys(:escape)
expect(page).not_to have_content('You are about to revoke')
end
end
...@@ -110,6 +110,14 @@ describe 'User uses shortcuts', :js do ...@@ -110,6 +110,14 @@ describe 'User uses shortcuts', :js do
end end
context 'when navigating to the Operations pages' do context 'when navigating to the Operations pages' do
it 'redirects to the Metrics page' do
find('body').native.send_key('g')
find('body').native.send_key('l')
expect(page).to have_active_navigation('Operations')
expect(page).to have_active_sub_navigation('Metrics')
end
it 'redirects to the Environments page' do it 'redirects to the Environments page' do
find('body').native.send_key('g') find('body').native.send_key('g')
find('body').native.send_key('e') find('body').native.send_key('e')
......
...@@ -280,11 +280,11 @@ describe('diff_file_header', () => { ...@@ -280,11 +280,11 @@ describe('diff_file_header', () => {
}); });
}); });
it('displays an icon in the title', () => { it('displays an file icon in the title', () => {
vm = mountComponent(Component, props); vm = mountComponent(Component, props);
expect(vm.$el.querySelector('svg.js-file-icon use').getAttribute('xlink:href')).toContain(
const icon = vm.$el.querySelector(`i[class="fa fa-fw fa-${vm.icon}"]`); 'ruby',
expect(icon).not.toBe(null); );
}); });
describe('file paths', () => { describe('file paths', () => {
......
...@@ -176,4 +176,35 @@ describe('DiffsStoreUtils', () => { ...@@ -176,4 +176,35 @@ describe('DiffsStoreUtils', () => {
expect(linesWithReferences[1].metaData.newPos).toEqual(3); expect(linesWithReferences[1].metaData.newPos).toEqual(3);
}); });
}); });
describe('trimFirstCharOfLineContent', () => {
it('trims the line when it starts with a space', () => {
expect(utils.trimFirstCharOfLineContent({ richText: ' diff' })).toEqual({ richText: 'diff' });
});
it('trims the line when it starts with a +', () => {
expect(utils.trimFirstCharOfLineContent({ richText: '+diff' })).toEqual({ richText: 'diff' });
});
it('trims the line when it starts with a -', () => {
expect(utils.trimFirstCharOfLineContent({ richText: '-diff' })).toEqual({ richText: 'diff' });
});
it('does not trims the line when it starts with a letter', () => {
expect(utils.trimFirstCharOfLineContent({ richText: 'diff' })).toEqual({ richText: 'diff' });
});
it('does not modify the provided object', () => {
const lineObj = {
richText: ' diff',
};
utils.trimFirstCharOfLineContent(lineObj);
expect(lineObj).toEqual({ richText: ' diff' });
});
it('handles a undefined or null parameter', () => {
expect(utils.trimFirstCharOfLineContent()).toEqual({});
});
});
}); });
...@@ -2,7 +2,7 @@ import Vue from 'vue'; ...@@ -2,7 +2,7 @@ import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue'; import Dashboard from '~/monitoring/components/dashboard.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { metricsGroupsAPIResponse, mockApiEndpoint } from './mock_data'; import { metricsGroupsAPIResponse, mockApiEndpoint, environmentData } from './mock_data';
describe('Dashboard', () => { describe('Dashboard', () => {
let DashboardComponent; let DashboardComponent;
...@@ -20,6 +20,8 @@ describe('Dashboard', () => { ...@@ -20,6 +20,8 @@ describe('Dashboard', () => {
emptyLoadingSvgPath: '/path/to/loading.svg', emptyLoadingSvgPath: '/path/to/loading.svg',
emptyNoDataSvgPath: '/path/to/no-data.svg', emptyNoDataSvgPath: '/path/to/no-data.svg',
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
environmentsEndpoint: '/root/hello-prometheus/environments/35',
currentEnvironmentName: 'production',
}; };
beforeEach(() => { beforeEach(() => {
...@@ -50,7 +52,7 @@ describe('Dashboard', () => { ...@@ -50,7 +52,7 @@ describe('Dashboard', () => {
mock.restore(); mock.restore();
}); });
it('shows up a loading state', (done) => { it('shows up a loading state', done => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true }, propsData: { ...propsData, hasMetrics: true },
...@@ -62,7 +64,7 @@ describe('Dashboard', () => { ...@@ -62,7 +64,7 @@ describe('Dashboard', () => {
}); });
}); });
it('hides the legend when showLegend is false', (done) => { it('hides the legend when showLegend is false', done => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showLegend: false }, propsData: { ...propsData, hasMetrics: true, showLegend: false },
...@@ -76,7 +78,7 @@ describe('Dashboard', () => { ...@@ -76,7 +78,7 @@ describe('Dashboard', () => {
}); });
}); });
it('hides the group panels when showPanels is false', (done) => { it('hides the group panels when showPanels is false', done => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showPanels: false }, propsData: { ...propsData, hasMetrics: true, showPanels: false },
...@@ -89,5 +91,40 @@ describe('Dashboard', () => { ...@@ -89,5 +91,40 @@ describe('Dashboard', () => {
done(); done();
}); });
}); });
it('renders the dropdown with a number of environments', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showPanels: false },
});
component.store.storeEnvironmentsData(environmentData);
setTimeout(() => {
const dropdownMenuEnvironments = component.$el.querySelectorAll('.dropdown-menu ul li a');
expect(dropdownMenuEnvironments.length).toEqual(component.store.environmentsData.length);
done();
});
});
it('renders the dropdown with a single is-active element', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showPanels: false },
});
component.store.storeEnvironmentsData(environmentData);
setTimeout(() => {
const dropdownIsActiveElement = component.$el.querySelectorAll(
'.dropdown-menu ul li a.is-active',
);
expect(dropdownIsActiveElement.length).toEqual(1);
expect(dropdownIsActiveElement[0].textContent.trim()).toEqual(
component.currentEnvironmentName,
);
done();
});
});
}); });
}); });
...@@ -6542,3 +6542,44 @@ export function convertDatesMultipleSeries(multipleSeries) { ...@@ -6542,3 +6542,44 @@ export function convertDatesMultipleSeries(multipleSeries) {
}); });
return convertedMultiple; return convertedMultiple;
} }
export const environmentData = [
{
name: 'production',
size: 1,
latest: {
id: 34,
name: 'production',
state: 'available',
external_url: 'http://root-autodevops-deploy.my-fake-domain.com',
environment_type: null,
stop_action: false,
metrics_path: '/root/hello-prometheus/environments/34/metrics',
environment_path: '/root/hello-prometheus/environments/34',
stop_path: '/root/hello-prometheus/environments/34/stop',
terminal_path: '/root/hello-prometheus/environments/34/terminal',
folder_path: '/root/hello-prometheus/environments/folders/production',
created_at: '2018-06-29T16:53:38.301Z',
updated_at: '2018-06-29T16:57:09.825Z',
},
},
{
name: 'review',
size: 1,
latest: {
id: 35,
name: 'review/noop-branch',
state: 'available',
external_url: 'http://root-autodevops-deploy-review-noop-branc-die93w.my-fake-domain.com',
environment_type: 'review',
stop_action: true,
metrics_path: '/root/hello-prometheus/environments/35/metrics',
environment_path: '/root/hello-prometheus/environments/35',
stop_path: '/root/hello-prometheus/environments/35/stop',
terminal_path: '/root/hello-prometheus/environments/35/terminal',
folder_path: '/root/hello-prometheus/environments/folders/review',
created_at: '2018-07-03T18:39:41.702Z',
updated_at: '2018-07-03T18:44:54.010Z',
},
},
];
...@@ -51,7 +51,7 @@ describe('Markdown field header component', () => { ...@@ -51,7 +51,7 @@ describe('Markdown field header component', () => {
spyOn(vm, '$emit'); spyOn(vm, '$emit');
$(document).triggerHandler('markdown-preview:show', [ $(document).triggerHandler('markdown-preview:show', [
$('<form><textarea class="markdown-area"></textarea></textarea></form>'), $('<form><div class="js-vue-markdown-field"><textarea class="markdown-area"></textarea></div></form>'),
]); ]);
expect(vm.$emit).not.toHaveBeenCalled(); expect(vm.$emit).not.toHaveBeenCalled();
......
...@@ -2631,6 +2631,28 @@ describe Project do ...@@ -2631,6 +2631,28 @@ describe Project do
end end
end end
describe '#default_environment' do
let(:project) { create(:project) }
it 'returns production environment when it exists' do
production = create(:environment, name: "production", project: project)
create(:environment, name: 'staging', project: project)
expect(project.default_environment).to eq(production)
end
it 'returns first environment when no production environment exists' do
create(:environment, name: 'staging', project: project)
create(:environment, name: 'foo', project: project)
expect(project.default_environment).to eq(project.environments.first)
end
it 'returns nil when no available environment exists' do
expect(project.default_environment).to be_nil
end
end
describe '#secret_variables_for' do describe '#secret_variables_for' do
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -25,6 +25,20 @@ describe DiffFileEntity do ...@@ -25,6 +25,20 @@ describe DiffFileEntity do
:context_lines_path :context_lines_path
) )
end end
# Converted diff files from GitHub import does not contain blob file
# and content sha.
context 'when diff file does not have a blob and content sha' do
it 'exposes some attributes as nil' do
allow(diff_file).to receive(:content_sha).and_return(nil)
allow(diff_file).to receive(:blob).and_return(nil)
expect(subject[:context_lines_path]).to be_nil
expect(subject[:view_path]).to be_nil
expect(subject[:highlighted_diff_lines]).to be_nil
expect(subject[:can_modify_blob]).to be_nil
end
end
end end
context 'when there is no merge request' do context 'when there is no merge request' do
......
...@@ -36,6 +36,25 @@ describe DiscussionEntity do ...@@ -36,6 +36,25 @@ describe DiscussionEntity do
) )
end end
context 'when is LegacyDiffDiscussion' do
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:discussion) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
it 'exposes correct attributes' do
expect(subject.keys.sort).to include(
:diff_discussion,
:expanded,
:id,
:individual_note,
:notes,
:discussion_path,
:for_commit,
:commit_id
)
end
end
context 'when diff file is present' do context 'when diff file is present' do
let(:note) { create(:diff_note_on_merge_request) } let(:note) { create(:diff_note_on_merge_request) }
......
...@@ -12,15 +12,15 @@ describe NotificationRecipientService do ...@@ -12,15 +12,15 @@ describe NotificationRecipientService do
def create_watcher def create_watcher
watcher = create(:user) watcher = create(:user)
create(:notification_setting, project: project, user: watcher, level: :watch) create(:notification_setting, source: project, user: watcher, level: :watch)
other_projects.each do |other_project| other_projects.each do |other_project|
create(:notification_setting, project: other_project, user: watcher, level: :watch) create(:notification_setting, source: other_project, user: watcher, level: :watch)
end end
end end
it 'avoids N+1 queries', :request_store do it 'avoids N+1 queries', :request_store do
Gitlab::GitalyClient.allow_n_plus_1_calls { create_watcher } create_watcher
service.build_new_note_recipients(note) service.build_new_note_recipients(note)
...@@ -28,7 +28,7 @@ describe NotificationRecipientService do ...@@ -28,7 +28,7 @@ describe NotificationRecipientService do
service.build_new_note_recipients(note) service.build_new_note_recipients(note)
end end
Gitlab::GitalyClient.allow_n_plus_1_calls { create_watcher } create_watcher
expect { service.build_new_note_recipients(note) }.not_to exceed_query_limit(control_count) expect { service.build_new_note_recipients(note) }.not_to exceed_query_limit(control_count)
end end
......
...@@ -236,6 +236,18 @@ describe Projects::CreateService, '#execute' do ...@@ -236,6 +236,18 @@ describe Projects::CreateService, '#execute' do
end end
end end
context 'when readme initialization is requested' do
it 'creates README.md' do
opts[:initialize_with_readme] = '1'
project = create_project(user, opts)
expect(project.repository.commit_count).to be(1)
expect(project.repository.readme.name).to eql('README.md')
expect(project.repository.readme.data).to include('# GitLab')
end
end
context 'when there is an active service template' do context 'when there is an active service template' do
before do before do
create(:service, project: nil, template: true, active: true) create(:service, project: nil, template: true, active: true)
......
...@@ -61,7 +61,7 @@ describe ProjectCacheWorker do ...@@ -61,7 +61,7 @@ describe ProjectCacheWorker do
expect_any_instance_of(Project).not_to receive(:update_repository_size) expect_any_instance_of(Project).not_to receive(:update_repository_size)
expect_any_instance_of(Project).not_to receive(:update_commit_count) expect_any_instance_of(Project).not_to receive(:update_commit_count)
subject.perform(project.id) worker.perform(project.id, %w(readme))
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment