Commit 68f33e42 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 43404-pipelines-commit

* master: (124 commits)
  Double-check next value for internal ids.
  Resolve "skeleton placeholder on diff has white background"
  Replace the `project/commits/comments.feature` spinach test with an rspec analog
  Set ENV['IN_MEMORY_APPLICATION_SETTINGS'] to 'true in spec/db/production/settings_spec.rb
  Update docs on `.gitlab-ci.yml` and variables policy
  Add note about support of variables policy to docs
  Revert and fix
  corrected git cheat sheet url
  Bump parser gem to 2.5.1.0 to remove warning in rspec
  Update invalid test application_settings_controller_spec.rb
  Use stable docker image instead of latest
  Handle cancelled request
  Stop polling and fetch the table
  [ci skip] Updates table when stage is clicked
  Backport GitLab domain helper methods
  use jquery object in togglePopover
  Fix other listeners
  Fix the metrics link being not rendering the html
  Fix feature_highlight listener
  Update custom issue tracker docs
  ...
parents cb8fa2a0 ddd7a81d
...@@ -721,7 +721,7 @@ codequality: ...@@ -721,7 +721,7 @@ codequality:
tags: [] tags: []
before_script: [] before_script: []
services: services:
- docker:dind - docker:stable-dind
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
DOCKER_DRIVER: overlay2 DOCKER_DRIVER: overlay2
......
...@@ -590,7 +590,7 @@ GEM ...@@ -590,7 +590,7 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (0.9.6) os (0.9.6)
parallel (1.12.1) parallel (1.12.1)
parser (2.5.0.5) parser (2.5.1.0)
ast (~> 2.4.0) ast (~> 2.4.0)
parslet (1.5.0) parslet (1.5.0)
blankslate (~> 2.0) blankslate (~> 2.0)
......
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore';
import { import {
getSelector, getSelector,
togglePopover,
inserted, inserted,
mouseenter,
mouseleave,
} from './feature_highlight_helper'; } from './feature_highlight_helper';
import {
togglePopover,
mouseenter,
debouncedMouseleave,
} from '../shared/popover';
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const $selector = $(getSelector(id)); const $selector = $(getSelector(id));
const $parent = $selector.parent(); const $parent = $selector.parent();
const $popoverContent = $parent.siblings('.feature-highlight-popover-content'); const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
const hideOnScroll = togglePopover.bind($selector, false); const hideOnScroll = togglePopover.bind($selector, false);
const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
$selector $selector
// Setup popover // Setup popover
...@@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { ...@@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
`, `,
}) })
.on('mouseenter', mouseenter) .on('mouseenter', mouseenter)
.on('mouseleave', debouncedMouseleave) .on('mouseleave', debouncedMouseleave(debounceTimeout))
.on('inserted.bs.popover', inserted) .on('inserted.bs.popover', inserted)
.on('show.bs.popover', () => { .on('show.bs.popover', () => {
window.addEventListener('scroll', hideOnScroll); window.addEventListener('scroll', hideOnScroll, { once: true });
})
.on('hide.bs.popover', () => {
window.removeEventListener('scroll', hideOnScroll);
}) })
// Display feature highlight // Display feature highlight
.removeAttr('disabled'); .removeAttr('disabled');
......
...@@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils'; ...@@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils';
import { __ } from '../locale'; import { __ } from '../locale';
import Flash from '../flash'; import Flash from '../flash';
import LazyLoader from '../lazy_loader'; import LazyLoader from '../lazy_loader';
import { togglePopover } from '../shared/popover';
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
export function togglePopover(show) {
const isAlreadyShown = this.hasClass('js-popover-show');
if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
return false;
}
this.popover(show ? 'show' : 'hide');
this.toggleClass('disable-animation js-popover-show', show);
return true;
}
export function dismiss(highlightId) { export function dismiss(highlightId) {
axios.post(this.attr('data-dismiss-endpoint'), { axios.post(this.attr('data-dismiss-endpoint'), {
feature_name: highlightId, feature_name: highlightId,
...@@ -27,23 +17,6 @@ export function dismiss(highlightId) { ...@@ -27,23 +17,6 @@ export function dismiss(highlightId) {
this.hide(); this.hide();
} }
export function mouseleave() {
if (!$('.popover:hover').length > 0) {
const $featureHighlight = $(this);
togglePopover.call($featureHighlight, false);
}
}
export function mouseenter() {
const $featureHighlight = $(this);
const showedPopover = togglePopover.call($featureHighlight, true);
if (showedPopover) {
$('.popover')
.on('mouseleave', mouseleave.bind($featureHighlight));
}
}
export function inserted() { export function inserted() {
const popoverId = this.getAttribute('aria-describedby'); const popoverId = this.getAttribute('aria-describedby');
const highlightId = this.dataset.highlight; const highlightId = this.dataset.highlight;
......
import $ from 'jquery'; import $ from 'jquery';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import flash from './flash'; import flash from './flash';
import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover';
export default class Milestone { export default class Milestone {
constructor() { constructor() {
...@@ -43,4 +44,25 @@ export default class Milestone { ...@@ -43,4 +44,25 @@ export default class Milestone {
.catch(() => flash('Error loading milestone tab')); .catch(() => flash('Error loading milestone tab'));
} }
} }
static initDeprecationMessage() {
const deprecationMesssageContainer = document.querySelector('.js-milestone-deprecation-message');
if (!deprecationMesssageContainer) return;
const deprecationMessage = deprecationMesssageContainer.querySelector('.js-milestone-deprecation-message-template').innerHTML;
const $popover = $('.js-popover-link', deprecationMesssageContainer);
const hideOnScroll = togglePopover.bind($popover, false);
$popover.popover({
content: deprecationMessage,
html: true,
placement: 'bottom',
})
.on('mouseenter', mouseenter)
.on('mouseleave', debouncedMouseleave())
.on('show.bs.popover', () => {
window.addEventListener('scroll', hideOnScroll, { once: true });
});
}
} }
...@@ -6,4 +6,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -6,4 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
new Milestone(); // eslint-disable-line no-new new Milestone(); // eslint-disable-line no-new
new Sidebar(); // eslint-disable-line no-new new Sidebar(); // eslint-disable-line no-new
new MountMilestoneSidebar(); // eslint-disable-line no-new new MountMilestoneSidebar(); // eslint-disable-line no-new
Milestone.initDeprecationMessage();
}); });
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
import Milestone from '~/milestone';
document.addEventListener('DOMContentLoaded', initMilestonesShow); document.addEventListener('DOMContentLoaded', () => {
initMilestonesShow();
Milestone.initDeprecationMessage();
});
import initNotes from '~/init_notes'; import initNotes from '~/init_notes';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import LineHighlighter from '../../../../line_highlighter'; import LineHighlighter from '~/line_highlighter';
import BlobViewer from '../../../../blob/viewer'; import BlobViewer from '~/blob/viewer';
import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
initNotes(); initNotes();
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
snippetEmbed();
}); });
import LineHighlighter from '../../../line_highlighter'; import LineHighlighter from '~/line_highlighter';
import BlobViewer from '../../../blob/viewer'; import BlobViewer from '~/blob/viewer';
import ZenMode from '../../../zen_mode'; import ZenMode from '~/zen_mode';
import initNotes from '../../../init_notes'; import initNotes from '~/init_notes';
import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
initNotes(); initNotes();
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
snippetEmbed();
}); });
<script> <script>
import $ from 'jquery';
/** /**
* Renders each stage of the pipeline mini graph. * Renders each stage of the pipeline mini graph.
...@@ -13,8 +12,11 @@ ...@@ -13,8 +12,11 @@
* 3. Merge request widget * 3. Merge request widget
* 4. Commit widget * 4. Commit widget
*/ */
import axios from '../../lib/utils/axios_utils';
import $ from 'jquery';
import Flash from '../../flash'; import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils';
import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue'; 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 tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -82,6 +84,7 @@ ...@@ -82,6 +84,7 @@
methods: { methods: {
onClickStage() { onClickStage() {
if (!this.isDropdownOpen()) { if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
this.isLoading = true; this.isLoading = true;
this.fetchJobs(); this.fetchJobs();
} }
......
// eslint-disable-next-line import/prefer-default-export
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
...@@ -7,6 +7,7 @@ import SvgBlankState from '../components/blank_state.vue'; ...@@ -7,6 +7,7 @@ import SvgBlankState from '../components/blank_state.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import PipelinesTableComponent from '../components/pipelines_table.vue'; import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { CANCEL_REQUEST } from '../constants';
export default { export default {
components: { components: {
...@@ -52,34 +53,58 @@ export default { ...@@ -52,34 +53,58 @@ export default {
}); });
eventHub.$on('postAction', this.postAction); eventHub.$on('postAction', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('postAction', this.postAction); eventHub.$off('postAction', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
}, },
destroyed() { destroyed() {
this.poll.stop(); this.poll.stop();
}, },
methods: { methods: {
updateTable() {
// Cancel ongoing request
if (this.isMakingRequest) {
this.service.cancelationSource.cancel(CANCEL_REQUEST);
}
// Stop polling
this.poll.stop();
// Update the table
return this.getPipelines()
.then(() => this.poll.restart());
},
fetchPipelines() { fetchPipelines() {
if (!this.isMakingRequest) { if (!this.isMakingRequest) {
this.isLoading = true; this.isLoading = true;
this.service.getPipelines(this.requestData) this.getPipelines();
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
} }
}, },
getPipelines() {
return this.service.getPipelines(this.requestData)
.then(response => this.successCallback(response))
.catch((error) => this.errorCallback(error));
},
setCommonData(pipelines) { setCommonData(pipelines) {
this.store.storePipelines(pipelines); this.store.storePipelines(pipelines);
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = true; this.updateGraphDropdown = true;
this.hasMadeRequest = true; this.hasMadeRequest = true;
// In case the previous polling request returned an error, we need to reset it
if (this.hasError) {
this.hasError = false;
}
}, },
errorCallback() { errorCallback(error) {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
this.hasMadeRequest = true; this.hasMadeRequest = true;
this.isLoading = false;
if (error && error.message && error.message !== CANCEL_REQUEST) {
this.hasError = true;
this.updateGraphDropdown = false;
}
}, },
setIsMakingRequest(isMakingRequest) { setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
......
...@@ -19,8 +19,13 @@ export default class PipelinesService { ...@@ -19,8 +19,13 @@ export default class PipelinesService {
getPipelines(data = {}) { getPipelines(data = {}) {
const { scope, page } = data; const { scope, page } = data;
const CancelToken = axios.CancelToken;
this.cancelationSource = CancelToken.source();
return axios.get(this.endpoint, { return axios.get(this.endpoint, {
params: { scope, page }, params: { scope, page },
cancelToken: this.cancelationSource.token,
}); });
} }
......
import $ from 'jquery';
import _ from 'underscore';
export function togglePopover(show) {
const isAlreadyShown = this.hasClass('js-popover-show');
if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
return false;
}
this.popover(show ? 'show' : 'hide');
this.toggleClass('disable-animation js-popover-show', show);
return true;
}
export function mouseleave() {
if (!$('.popover:hover').length > 0) {
const $popover = $(this);
togglePopover.call($popover, false);
}
}
export function mouseenter() {
const $popover = $(this);
const showedPopover = togglePopover.call($popover, true);
if (showedPopover) {
$('.popover').on('mouseleave', mouseleave.bind($popover));
}
}
export function debouncedMouseleave(debounceTimeout = 300) {
return _.debounce(mouseleave, debounceTimeout);
}
import { visitUrl } from './lib/utils/url_utility';
/** /**
* Helper function that finds the href of the fiven selector and updates the location. * Helper function that finds the href of the fiven selector and updates the location.
* *
* @param {String} selector * @param {String} selector
*/ */
export default (selector) => { export default function findAndFollowLink(selector) {
const link = document.querySelector(selector).getAttribute('href'); const element = document.querySelector(selector);
const link = element && element.getAttribute('href');
if (link) { if (link) {
window.location = link; visitUrl(link);
} }
}; }
export default () => {
const { protocol, host, pathname } = location;
const shareBtn = document.querySelector('.js-share-btn');
const embedBtn = document.querySelector('.js-embed-btn');
const snippetUrlArea = document.querySelector('.js-snippet-url-area');
const embedAction = document.querySelector('.js-embed-action');
const url = `${protocol}//${host + pathname}`;
shareBtn.addEventListener('click', () => {
shareBtn.classList.add('is-active');
embedBtn.classList.remove('is-active');
snippetUrlArea.value = url;
embedAction.innerText = 'Share';
});
embedBtn.addEventListener('click', () => {
embedBtn.classList.add('is-active');
shareBtn.classList.remove('is-active');
const scriptTag = `<script src="${url}.js"></script>`;
snippetUrlArea.value = scriptTag;
embedAction.innerText = 'Embed';
});
};
...@@ -146,8 +146,8 @@ export default { ...@@ -146,8 +146,8 @@ export default {
</p> </p>
<p <p
v-if="shouldShowMemoryGraph" v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info"> class="usage-info js-usage-info"
{{ memoryChangeMessage }} v-html="memoryChangeMessage">
</p> </p>
<p <p
v-if="shouldShowLoadFailure" v-if="shouldShowLoadFailure"
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
lines: { lines: {
type: Number, type: Number,
required: false, required: false,
default: 6, default: 3,
}, },
}, },
computed: { computed: {
......
...@@ -37,7 +37,11 @@ ...@@ -37,7 +37,11 @@
/* /*
* Code highlight * Code highlight
*/ */
@import "highlight/**/*"; @import "highlight/dark";
@import "highlight/monokai";
@import "highlight/solarized_dark";
@import "highlight/solarized_light";
@import "highlight/white";
/* /*
* Styles for JS behaviors. * Styles for JS behaviors.
......
...@@ -187,12 +187,9 @@ a { ...@@ -187,12 +187,9 @@ a {
animation: fadeInFull $fade-in-duration 1; animation: fadeInFull $fade-in-duration 1;
} }
.animation-container { .animation-container {
background: $repo-editor-grey;
height: 40px; height: 40px;
overflow: hidden; overflow: hidden;
position: relative;
&.animation-container-small { &.animation-container-small {
height: 12px; height: 12px;
...@@ -205,60 +202,43 @@ a { ...@@ -205,60 +202,43 @@ a {
} }
} }
&::before { [class^="skeleton-line-"] {
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: blockTextShine;
animation-timing-function: linear;
background-image: $repo-editor-linear-gradient;
background-repeat: no-repeat;
background-size: 800px 45px;
content: ' ';
display: block;
height: 100%;
position: relative; position: relative;
} background-color: $theme-gray-100;
div {
background: $white-light;
height: 6px;
left: 0;
position: absolute;
right: 0;
}
.skeleton-line-1 {
left: 0;
top: 8px;
}
.skeleton-line-2 {
left: 150px;
top: 0;
height: 10px; height: 10px;
} overflow: hidden;
.skeleton-line-3 { &:not(:last-of-type) {
left: 0; margin-bottom: 4px;
top: 23px; }
}
.skeleton-line-4 { &::after {
left: 0; content: ' ';
top: 38px; display: block;
animation: blockTextShine 1s linear infinite forwards;
background-repeat: no-repeat;
background-size: cover;
background-image: linear-gradient(
to right,
$theme-gray-100 0%,
$theme-gray-50 20%,
$theme-gray-100 40%,
$theme-gray-100 100%
);
height: 10px;
}
} }
}
.skeleton-line-5 { $skeleton-line-widths: (
left: 200px; 156px,
top: 28px; 235px,
height: 10px; 200px,
} );
.skeleton-line-6 { @for $count from 1 through length($skeleton-line-widths) {
top: 14px; .skeleton-line-#{$count} {
left: 230px; width: nth($skeleton-line-widths, $count);
height: 10px;
} }
} }
......
.banner-callout { .banner-callout {
display: flex; display: flex;
position: relative; position: relative;
flex-wrap: wrap; align-items: start;
.banner-close { .banner-close {
position: absolute; position: absolute;
...@@ -16,10 +16,25 @@ ...@@ -16,10 +16,25 @@
} }
.banner-graphic { .banner-graphic {
margin: 20px auto; margin: 0 $gl-padding $gl-padding 0;
} }
&.banner-non-empty-state { &.banner-non-empty-state {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
@media (max-width: $screen-xs-max) {
justify-content: center;
flex-direction: column;
align-items: center;
.banner-title,
.banner-buttons {
text-align: center;
}
.banner-graphic {
margin-left: $gl-padding;
}
}
} }
...@@ -422,25 +422,43 @@ ...@@ -422,25 +422,43 @@
} }
} }
.btn-link.btn-secondary-hover-link { .btn-link {
color: $gl-text-color-secondary; padding: 0;
background-color: transparent;
color: $blue-600;
font-weight: normal;
border-radius: 0;
border-color: transparent;
&:hover, &:hover,
&:active, &:active,
&:focus { &:focus {
color: $gl-link-color; color: $blue-800;
text-decoration: none; text-decoration: underline;
background-color: transparent;
border-color: transparent;
} }
}
.btn-link.btn-primary-hover-link { &.btn-secondary-hover-link {
color: inherit; color: $gl-text-color-secondary;
&:hover, &:hover,
&:active, &:active,
&:focus { &:focus {
color: $gl-link-color; color: $gl-link-color;
text-decoration: none; text-decoration: none;
}
}
&.btn-primary-hover-link {
color: inherit;
&:hover,
&:active,
&:focus {
color: $gl-link-color;
text-decoration: none;
}
} }
} }
......
...@@ -481,7 +481,8 @@ ...@@ -481,7 +481,8 @@
.dropdown-menu-selectable { .dropdown-menu-selectable {
li { li {
a { a,
button {
padding: 8px 40px; padding: 8px 40px;
position: relative; position: relative;
......
...@@ -29,8 +29,10 @@ ...@@ -29,8 +29,10 @@
} }
.snippet-title { .snippet-title {
font-size: 24px; color: $gl-text-color;
font-size: 2em;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
min-height: $header-height;
} }
.snippet-edited-ago { .snippet-edited-ago {
...@@ -46,3 +48,26 @@ ...@@ -46,3 +48,26 @@
.snippet-scope-menu .btn-new { .snippet-scope-menu .btn-new {
margin-top: 15px; margin-top: 15px;
} }
.snippet-embed-input {
height: 35px;
}
.embed-snippet {
padding-right: 0;
padding-top: $gl-padding;
.form-control {
cursor: auto;
width: 101%;
margin-left: -1px;
}
.embed-toggle-list li button {
padding: 8px 40px;
}
.embed-toggle {
height: 35px;
}
}
...@@ -713,20 +713,6 @@ $color-high-score: $green-400; ...@@ -713,20 +713,6 @@ $color-high-score: $green-400;
$color-average-score: $orange-400; $color-average-score: $orange-400;
$color-low-score: $red-400; $color-low-score: $red-400;
/*
Repo editor
*/
$repo-editor-grey: #f6f7f9;
$repo-editor-grey-darker: #e9ebee;
$repo-editor-linear-gradient: linear-gradient(
to right,
$repo-editor-grey 0%,
$repo-editor-grey-darker,
20%,
$repo-editor-grey 40%,
$repo-editor-grey 100%
);
/* /*
Performance Bar Performance Bar
*/ */
......
/* https://github.com/aahan/pygments-github-style */
/*
* White Syntax Colors
*/
$white-code-color: $gl-text-color;
$white-highlight: #fafe3d;
$white-pre-hll-bg: #f8eec7;
$white-hll-bg: #f8f8f8;
$white-over-bg: #ded7fc;
$white-expanded-border: #e0e0e0;
$white-expanded-bg: #f7f7f7;
$white-c: #998;
$white-err: #a61717;
$white-err-bg: #e3d2d2;
$white-cm: #998;
$white-cp: #999;
$white-c1: #998;
$white-cs: #999;
$white-gd: $black;
$white-gd-bg: #fdd;
$white-gd-x: $black;
$white-gd-x-bg: #faa;
$white-gr: #a00;
$white-gh: #999;
$white-gi: $black;
$white-gi-bg: #dfd;
$white-gi-x: $black;
$white-gi-x-bg: #afa;
$white-go: #888;
$white-gp: #555;
$white-gu: #800080;
$white-gt: #a00;
$white-kt: #458;
$white-m: #099;
$white-s: #d14;
$white-n: #333;
$white-na: teal;
$white-nb: #0086b3;
$white-nc: #458;
$white-no: teal;
$white-ni: purple;
$white-ne: #900;
$white-nf: #900;
$white-nn: #555;
$white-nt: navy;
$white-nv: teal;
$white-w: #bbb;
$white-mf: #099;
$white-mh: #099;
$white-mi: #099;
$white-mo: #099;
$white-sb: #d14;
$white-sc: #d14;
$white-sd: #d14;
$white-s2: #d14;
$white-se: #d14;
$white-sh: #d14;
$white-si: #d14;
$white-sx: #d14;
$white-sr: #009926;
$white-s1: #d14;
$white-ss: #990073;
$white-bp: #999;
$white-vc: teal;
$white-vg: teal;
$white-vi: teal;
$white-il: #099;
$white-gc-color: #999;
$white-gc-bg: #eaf2f5;
@mixin matchLine {
color: $black-transparent;
background-color: $gray-light;
}
.code.white { .code.white {
// Line numbers @import "white_base";
.line-numbers,
.diff-line-num {
background-color: $gray-light;
}
.diff-line-num,
.diff-line-num a {
color: $black-transparent;
}
// Code itself
pre.code,
.diff-line-num {
border-color: $white-normal;
}
&,
pre.code,
.line_holder .line_content {
background-color: $white-light;
color: $white-code-color;
}
// Diff line
.line_holder {
&.match .line_content {
@include matchLine;
}
.diff-line-num {
&.old {
background-color: $line-number-old;
border-color: $line-removed-dark;
a {
color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
}
}
&.new {
background-color: $line-number-new;
border-color: $line-added-dark;
a {
color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
}
}
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $white-over-bg;
border-color: darken($white-over-bg, 5%);
a {
color: darken($white-over-bg, 15%);
}
}
&.hll:not(.empty-cell) {
background-color: $line-number-select;
border-color: $line-select-yellow-dark;
}
}
&:not(.diff-expanded) + .diff-expanded,
&.diff-expanded + .line_holder:not(.diff-expanded) {
> .diff-line-num,
> .line_content {
border-top: 1px solid $white-expanded-border;
}
}
&.diff-expanded {
> .diff-line-num,
> .line_content {
background: $white-expanded-bg;
border-color: $white-expanded-bg;
}
}
.line_content {
&.old {
background-color: $line-removed;
&::before {
color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
}
span.idiff {
background-color: $line-removed-dark;
}
}
&.new {
background-color: $line-added;
&::before {
color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
}
span.idiff {
background-color: $line-added-dark;
}
}
&.match {
@include matchLine;
}
&.hll:not(.empty-cell) {
background-color: $line-select-yellow;
}
}
}
// highlight line via anchor
pre .hll {
background-color: $white-pre-hll-bg !important;
}
// Search result highlight
span.highlight_word {
background-color: $white-highlight !important;
}
// Links to URLs, emails, or dependencies
.line a {
color: $white-nb;
}
.hll { background-color: $white-hll-bg; }
.c { color: $white-c; font-style: italic; }
.err { color: $white-err; background-color: $white-err-bg; }
.k { font-weight: $gl-font-weight-bold; }
.o { font-weight: $gl-font-weight-bold; }
.cm { color: $white-cm; font-style: italic; }
.cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $white-c1; font-style: italic; }
.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
.gd {
color: $white-gd;
background-color: $white-gd-bg;
.x {
color: $white-gd-x;
background-color: $white-gd-x-bg;
}
}
.ge { font-style: italic; }
.gr { color: $white-gr; }
.gh { color: $white-gh; }
.gi {
color: $white-gi;
background-color: $white-gi-bg;
.x {
color: $white-gi-x;
background-color: $white-gi-x-bg;
}
}
.go { color: $white-go; }
.gp { color: $white-gp; }
.gs { font-weight: $gl-font-weight-bold; }
.gu { color: $white-gu; font-weight: $gl-font-weight-bold; }
.gt { color: $white-gt; }
.kc { font-weight: $gl-font-weight-bold; }
.kd { font-weight: $gl-font-weight-bold; }
.kn { font-weight: $gl-font-weight-bold; }
.kp { font-weight: $gl-font-weight-bold; }
.kr { font-weight: $gl-font-weight-bold; }
.kt { color: $white-kt; font-weight: $gl-font-weight-bold; }
.m { color: $white-m; }
.s { color: $white-s; }
.n { color: $white-n; }
.na { color: $white-na; }
.nb { color: $white-nb; }
.nc { color: $white-nc; font-weight: $gl-font-weight-bold; }
.no { color: $white-no; }
.ni { color: $white-ni; }
.ne { color: $white-ne; font-weight: $gl-font-weight-bold; }
.nf { color: $white-nf; font-weight: $gl-font-weight-bold; }
.nn { color: $white-nn; }
.nt { color: $white-nt; }
.nv { color: $white-nv; }
.ow { font-weight: $gl-font-weight-bold; }
.w { color: $white-w; }
.mf { color: $white-mf; }
.mh { color: $white-mh; }
.mi { color: $white-mi; }
.mo { color: $white-mo; }
.sb { color: $white-sb; }
.sc { color: $white-sc; }
.sd { color: $white-sd; }
.s2 { color: $white-s2; }
.se { color: $white-se; }
.sh { color: $white-sh; }
.si { color: $white-si; }
.sx { color: $white-sx; }
.sr { color: $white-sr; }
.s1 { color: $white-s1; }
.ss { color: $white-ss; }
.bp { color: $white-bp; }
.vc { color: $white-vc; }
.vg { color: $white-vg; }
.vi { color: $white-vi; }
.il { color: $white-il; }
.gc { color: $white-gc-color; background-color: $white-gc-bg; }
} }
/* https://github.com/aahan/pygments-github-style */
/*
* White Syntax Colors
*/
$white-code-color: $gl-text-color;
$white-highlight: #fafe3d;
$white-pre-hll-bg: #f8eec7;
$white-hll-bg: #f8f8f8;
$white-over-bg: #ded7fc;
$white-expanded-border: #e0e0e0;
$white-expanded-bg: #f7f7f7;
$white-c: #998;
$white-err: #a61717;
$white-err-bg: #e3d2d2;
$white-cm: #998;
$white-cp: #999;
$white-c1: #998;
$white-cs: #999;
$white-gd: $black;
$white-gd-bg: #fdd;
$white-gd-x: $black;
$white-gd-x-bg: #faa;
$white-gr: #a00;
$white-gh: #999;
$white-gi: $black;
$white-gi-bg: #dfd;
$white-gi-x: $black;
$white-gi-x-bg: #afa;
$white-go: #888;
$white-gp: #555;
$white-gu: #800080;
$white-gt: #a00;
$white-kt: #458;
$white-m: #099;
$white-s: #d14;
$white-n: #333;
$white-na: teal;
$white-nb: #0086b3;
$white-nc: #458;
$white-no: teal;
$white-ni: purple;
$white-ne: #900;
$white-nf: #900;
$white-nn: #555;
$white-nt: navy;
$white-nv: teal;
$white-w: #bbb;
$white-mf: #099;
$white-mh: #099;
$white-mi: #099;
$white-mo: #099;
$white-sb: #d14;
$white-sc: #d14;
$white-sd: #d14;
$white-s2: #d14;
$white-se: #d14;
$white-sh: #d14;
$white-si: #d14;
$white-sx: #d14;
$white-sr: #009926;
$white-s1: #d14;
$white-ss: #990073;
$white-bp: #999;
$white-vc: teal;
$white-vg: teal;
$white-vi: teal;
$white-il: #099;
$white-gc-color: #999;
$white-gc-bg: #eaf2f5;
@mixin matchLine {
color: $black-transparent;
background-color: $gray-light;
}
// Line numbers
.line-numbers,
.diff-line-num {
background-color: $gray-light;
}
.diff-line-num,
.diff-line-num a {
color: $black-transparent;
}
// Code itself
pre.code,
.diff-line-num {
border-color: $white-normal;
}
&,
pre.code,
.line_holder .line_content {
background-color: $white-light;
color: $white-code-color;
}
// Diff line
.line_holder {
&.match .line_content {
@include matchLine;
}
.diff-line-num {
&.old {
background-color: $line-number-old;
border-color: $line-removed-dark;
a {
color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
}
}
&.new {
background-color: $line-number-new;
border-color: $line-added-dark;
a {
color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
}
}
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $white-over-bg;
border-color: darken($white-over-bg, 5%);
a {
color: darken($white-over-bg, 15%);
}
}
&.hll:not(.empty-cell) {
background-color: $line-number-select;
border-color: $line-select-yellow-dark;
}
}
&:not(.diff-expanded) + .diff-expanded,
&.diff-expanded + .line_holder:not(.diff-expanded) {
> .diff-line-num,
> .line_content {
border-top: 1px solid $white-expanded-border;
}
}
&.diff-expanded {
> .diff-line-num,
> .line_content {
background: $white-expanded-bg;
border-color: $white-expanded-bg;
}
}
.line_content {
&.old {
background-color: $line-removed;
&::before {
color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
}
span.idiff {
background-color: $line-removed-dark;
}
}
&.new {
background-color: $line-added;
&::before {
color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
}
span.idiff {
background-color: $line-added-dark;
}
}
&.match {
@include matchLine;
}
&.hll:not(.empty-cell) {
background-color: $line-select-yellow;
}
}
}
// highlight line via anchor
pre .hll {
background-color: $white-pre-hll-bg !important;
}
// Search result highlight
span.highlight_word {
background-color: $white-highlight !important;
}
// Links to URLs, emails, or dependencies
.line a {
color: $white-nb;
}
.hll { background-color: $white-hll-bg; }
.c { color: $white-c; font-style: italic; }
.err { color: $white-err; background-color: $white-err-bg; }
.k { font-weight: $gl-font-weight-bold; }
.o { font-weight: $gl-font-weight-bold; }
.cm { color: $white-cm; font-style: italic; }
.cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $white-c1; font-style: italic; }
.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
.gd {
color: $white-gd;
background-color: $white-gd-bg;
.x {
color: $white-gd-x;
background-color: $white-gd-x-bg;
}
}
.ge { font-style: italic; }
.gr { color: $white-gr; }
.gh { color: $white-gh; }
.gi {
color: $white-gi;
background-color: $white-gi-bg;
.x {
color: $white-gi-x;
background-color: $white-gi-x-bg;
}
}
.go { color: $white-go; }
.gp { color: $white-gp; }
.gs { font-weight: $gl-font-weight-bold; }
.gu { color: $white-gu; font-weight: $gl-font-weight-bold; }
.gt { color: $white-gt; }
.kc { font-weight: $gl-font-weight-bold; }
.kd { font-weight: $gl-font-weight-bold; }
.kn { font-weight: $gl-font-weight-bold; }
.kp { font-weight: $gl-font-weight-bold; }
.kr { font-weight: $gl-font-weight-bold; }
.kt { color: $white-kt; font-weight: $gl-font-weight-bold; }
.m { color: $white-m; }
.s { color: $white-s; }
.n { color: $white-n; }
.na { color: $white-na; }
.nb { color: $white-nb; }
.nc { color: $white-nc; font-weight: $gl-font-weight-bold; }
.no { color: $white-no; }
.ni { color: $white-ni; }
.ne { color: $white-ne; font-weight: $gl-font-weight-bold; }
.nf { color: $white-nf; font-weight: $gl-font-weight-bold; }
.nn { color: $white-nn; }
.nt { color: $white-nt; }
.nv { color: $white-nv; }
.ow { font-weight: $gl-font-weight-bold; }
.w { color: $white-w; }
.mf { color: $white-mf; }
.mh { color: $white-mh; }
.mi { color: $white-mi; }
.mo { color: $white-mo; }
.sb { color: $white-sb; }
.sc { color: $white-sc; }
.sd { color: $white-sd; }
.s2 { color: $white-s2; }
.se { color: $white-se; }
.sh { color: $white-sh; }
.si { color: $white-si; }
.sx { color: $white-sx; }
.sr { color: $white-sr; }
.s1 { color: $white-s1; }
.ss { color: $white-ss; }
.bp { color: $white-bp; }
.vc { color: $white-vc; }
.vg { color: $white-vg; }
.vi { color: $white-vi; }
.il { color: $white-il; }
.gc { color: $white-gc-color; background-color: $white-gc-bg; }
...@@ -194,3 +194,38 @@ ...@@ -194,3 +194,38 @@
.issuable-row { .issuable-row {
background-color: $white-light; background-color: $white-light;
} }
.milestone-deprecation-message {
.popover {
padding: 0;
}
.popover-content {
padding: 0;
}
}
.milestone-popover-body {
padding: $gl-padding-8;
background-color: $gray-light;
}
.milestone-popover-footer {
padding: $gl-padding-8 $gl-padding;
border-top: 1px solid $white-dark;
}
.milestone-popover-instructions-list {
padding-left: 2em;
> li {
padding-left: 1em;
}
}
@media (max-width: $screen-xs-max) {
.milestone-banner-text,
.milestone-banner-link {
display: inline;
}
}
@import "framework/variables";
.gitlab-embed-snippets {
@import "highlight/embedded";
@import "framework/images";
$border-style: 1px solid $border-color;
font-family: $regular_font;
font-size: $gl-font-size;
line-height: $code_line_height;
color: $gl-text-color;
margin: 20px;
font-weight: 200;
.gl-snippet-icon {
display: inline-block;
background: url(asset_path('ext_snippet_icons/ext_snippet_icons.png')) no-repeat;
overflow: hidden;
text-align: left;
width: 16px;
height: 16px;
background-size: cover;
&.gl-snippet-icon-doc_code { background-position: 0 0; }
&.gl-snippet-icon-doc_text { background-position: 0 -16px; }
&.gl-snippet-icon-download { background-position: 0 -32px; }
}
.blob-viewer {
background-color: $white-light;
text-align: left;
}
.file-content.code {
border: $border-style;
border-radius: 0 0 4px 4px;
display: flex;
box-shadow: none;
margin: 0;
padding: 0;
table-layout: fixed;
.blob-content {
overflow-x: auto;
pre {
padding: 10px;
border: 0;
border-radius: 0;
font-family: $monospace_font;
font-size: $code_font_size;
line-height: $code_line_height;
margin: 0;
overflow: auto;
overflow-y: hidden;
white-space: pre;
word-wrap: normal;
border-left: $border-style;
}
}
.line-numbers {
padding: 10px;
text-align: right;
float: left;
.diff-line-num {
font-family: $monospace_font;
display: block;
font-size: $code_font_size;
min-height: $code_line_height;
white-space: nowrap;
color: $black-transparent;
min-width: 30px;
}
.diff-line-num:hover {
color: $almost-black;
cursor: pointer;
}
}
}
.file-title-flex-parent {
display: flex;
align-items: center;
justify-content: space-between;
background-color: $gray-light;
border: $border-style;
border-bottom: 0;
padding: $gl-padding-top $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
.file-header-content {
.file-title-name {
font-weight: $gl-font-weight-bold;
}
.gitlab-embedded-snippets-title {
text-decoration: none;
color: $gl-text-color;
&:hover {
text-decoration: underline;
}
}
.gitlab-logo {
display: inline-block;
padding-left: 5px;
text-decoration: none;
color: $gl-text-color-secondary;
.logo-text {
background: image_url('ext_snippet_icons/logo.png') no-repeat left center;
background-size: 18px;
font-weight: $gl-font-weight-normal;
padding-left: 24px;
}
}
}
img,
.gl-snippet-icon {
display: inline-block;
vertical-align: middle;
}
}
.btn-group {
a.btn {
background-color: $white-light;
text-decoration: none;
padding: 7px 9px;
border: $border-style;
border-right: 0;
&:hover {
background-color: $white-normal;
border-color: $border-white-normal;
text-decoration: none;
}
&:first-child {
border-radius: 3px 0 0 3px;
}
&:last-child {
border-radius: 0 3px 3px 0;
border-right: $border-style;
}
}
}
}
...@@ -57,22 +57,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -57,22 +57,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def application_setting_params def application_setting_params
params[:application_setting] ||= {} params[:application_setting] ||= {}
import_sources = params[:application_setting][:import_sources]
if import_sources.nil? if params[:application_setting].key?(:enabled_oauth_sign_in_sources)
params[:application_setting][:import_sources] = [] enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources)
else enabled_oauth_sign_in_sources&.delete("")
import_sources.map! do |source|
source.to_str
end
end
enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources) params[:application_setting][:disabled_oauth_sign_in_sources] =
AuthHelper.button_based_providers.map(&:to_s) -
params[:application_setting][:disabled_oauth_sign_in_sources] = Array(enabled_oauth_sign_in_sources)
AuthHelper.button_based_providers.map(&:to_s) - end
Array(enabled_oauth_sign_in_sources)
params[:application_setting][:import_sources]&.delete("")
params[:application_setting][:restricted_visibility_levels]&.delete("") params[:application_setting][:restricted_visibility_levels]&.delete("")
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
......
...@@ -17,6 +17,10 @@ module SnippetsActions ...@@ -17,6 +17,10 @@ module SnippetsActions
end end
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables
def js_request?
request.format.js?
end
private private
def convert_line_endings(content) def convert_line_endings(content)
......
...@@ -5,6 +5,8 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -5,6 +5,8 @@ class Projects::SnippetsController < Projects::ApplicationController
include SnippetsActions include SnippetsActions
include RendersBlob include RendersBlob
skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
before_action :check_snippets_available! before_action :check_snippets_available!
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
...@@ -71,6 +73,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -71,6 +73,7 @@ class Projects::SnippetsController < Projects::ApplicationController
format.json do format.json do
render_blob_json(blob) render_blob_json(blob)
end end
format.js { render 'shared/snippets/show'}
end end
end end
......
...@@ -6,6 +6,8 @@ class SnippetsController < ApplicationController ...@@ -6,6 +6,8 @@ class SnippetsController < ApplicationController
include RendersBlob include RendersBlob
include PreviewMarkdown include PreviewMarkdown
skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet # Allow read snippet
...@@ -77,6 +79,8 @@ class SnippetsController < ApplicationController ...@@ -77,6 +79,8 @@ class SnippetsController < ApplicationController
format.json do format.json do
render_blob_json(blob) render_blob_json(blob)
end end
format.js { render 'shared/snippets/show' }
end end
end end
......
...@@ -74,10 +74,12 @@ module ApplicationSettingsHelper ...@@ -74,10 +74,12 @@ module ApplicationSettingsHelper
css_class = 'btn' css_class = 'btn'
css_class << ' active' unless disabled css_class << ' active' unless disabled
checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]'
name = Gitlab::Auth::OAuth::Provider.label_for(source)
label_tag(checkbox_name, class: css_class) do label_tag(checkbox_name, class: css_class) do
check_box_tag(checkbox_name, source, !disabled, check_box_tag(checkbox_name, source, !disabled,
autocomplete: 'off') + Gitlab::Auth::OAuth::Provider.label_for(source) autocomplete: 'off',
id: name.tr(' ', '_')) + name
end end
end end
end end
......
...@@ -43,6 +43,10 @@ module IconsHelper ...@@ -43,6 +43,10 @@ module IconsHelper
content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
end end
def external_snippet_icon(name)
content_tag(:span, "", class: "gl-snippet-icon gl-snippet-icon-#{name}")
end
def audit_icon(names, options = {}) def audit_icon(names, options = {})
case names case names
when "standard" when "standard"
......
...@@ -101,4 +101,39 @@ module SnippetsHelper ...@@ -101,4 +101,39 @@ module SnippetsHelper
# Return snippet with chunk array # Return snippet with chunk array
{ snippet_object: snippet, snippet_chunks: snippet_chunks } { snippet_object: snippet, snippet_chunks: snippet_chunks }
end end
def snippet_embed
"<script src=\"#{url_for(only_path: false, overwrite_params: nil)}.js\"></script>"
end
def embedded_snippet_raw_button
blob = @snippet.blob
return if blob.empty? || blob.raw_binary? || blob.stored_externally?
snippet_raw_url = if @snippet.is_a?(PersonalSnippet)
raw_snippet_url(@snippet)
else
raw_project_snippet_url(@snippet.project, @snippet)
end
link_to external_snippet_icon('doc_code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw'
end
def embedded_snippet_download_button
download_url = if @snippet.is_a?(PersonalSnippet)
raw_snippet_url(@snippet, inline: false)
else
raw_project_snippet_url(@snippet.project, @snippet, inline: false)
end
link_to external_snippet_icon('download'), download_url, class: 'btn', target: '_blank', title: 'Download', rel: 'noopener noreferrer'
end
def public_snippet?
if @snippet.project_id?
can?(nil, :read_project_snippet, @snippet)
else
can?(nil, :read_personal_snippet, @snippet)
end
end
end end
...@@ -23,9 +23,12 @@ class InternalId < ActiveRecord::Base ...@@ -23,9 +23,12 @@ class InternalId < ActiveRecord::Base
# #
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
# As such, the increment is atomic and safe to be called concurrently. # As such, the increment is atomic and safe to be called concurrently.
def increment_and_save! #
# If a `maximum_iid` is passed in, this overrides the incremented value if it's
# greater than that. This can be used to correct the increment value if necessary.
def increment_and_save!(maximum_iid)
lock! lock!
self.last_value = (last_value || 0) + 1 self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max
save! save!
last_value last_value
end end
...@@ -89,7 +92,16 @@ class InternalId < ActiveRecord::Base ...@@ -89,7 +92,16 @@ class InternalId < ActiveRecord::Base
# and increment its last value # and increment its last value
# #
# Note this will acquire a ROW SHARE lock on the InternalId record # Note this will acquire a ROW SHARE lock on the InternalId record
(lookup || create_record).increment_and_save!
# Note we always calculate the maximum iid present here and
# pass it in to correct the InternalId entry if it's last_value is off.
#
# This can happen in a transition phase where both `AtomicInternalId` and
# `NonatomicInternalId` code runs (e.g. during a deploy).
#
# This is subject to be cleaned up with the 10.8 release:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/45389.
(lookup || create_record).increment_and_save!(maximum_iid)
end end
end end
...@@ -115,11 +127,15 @@ class InternalId < ActiveRecord::Base ...@@ -115,11 +127,15 @@ class InternalId < ActiveRecord::Base
InternalId.create!( InternalId.create!(
**scope, **scope,
usage: usage_value, usage: usage_value,
last_value: init.call(subject) || 0 last_value: maximum_iid
) )
end end
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
lookup lookup
end end
def maximum_iid
@maximum_iid ||= init.call(subject) || 0
end
end end
end end
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
- if omniauth_enabled? && button_based_providers.any? - if omniauth_enabled? && button_based_providers.any?
.form-group .form-group
= f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2' = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
= hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]'
.col-sm-10 .col-sm-10
.btn-group{ data: { toggle: 'buttons' } } .btn-group{ data: { toggle: 'buttons' } }
- oauth_providers_checkboxes.each do |source| - oauth_providers_checkboxes.each do |source|
......
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
.form-group .form-group
= f.label :import_sources, class: 'control-label col-sm-2' = f.label :import_sources, class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
= hidden_field_tag 'application_setting[import_sources][]'
- import_sources_checkboxes('import-sources-help').each do |source| - import_sources_checkboxes('import-sources-help').each do |source|
.checkbox= source .checkbox= source
%span.help-block#import-sources-help %span.help-block#import-sources-help
......
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
GitLab GitLab
%span.pull-right %span.pull-right
= Gitlab::VERSION = Gitlab::VERSION
= "(#{Gitlab::REVISION})"
%p %p
GitLab Shell GitLab Shell
%span.pull-right %span.pull-right
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
- else - else
%p %p
Download the Google Authenticator application from App Store or Google Play Store and scan this code. Download the Google Authenticator application from App Store or Google Play Store and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}. More information is available in the #{link_to('documentation', help_page_path('user/profile/account/two_factor_authentication'))}.
.row.append-bottom-10 .row.append-bottom-10
.col-md-4 .col-md-4
= raw @qr_code = raw @qr_code
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- render_error = viewer.render_error - render_error = viewer.render_error
- rich_type = viewer.type == :rich ? viewer.partial_name : nil - rich_type = viewer.type == :rich ? viewer.partial_name : nil
- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?) - load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?)
- external_embed = local_assigns.fetch(:external_embed, false)
- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async - viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async
.blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) } .blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) }
...@@ -9,6 +10,8 @@ ...@@ -9,6 +10,8 @@
= render 'projects/blob/render_error', viewer: viewer = render 'projects/blob/render_error', viewer: viewer
- elsif load_async - elsif load_async
= render viewer.loading_partial_path, viewer: viewer = render viewer.loading_partial_path, viewer: viewer
- elsif external_embed
= render 'projects/blob/viewers/highlight_embed', blob: viewer.blob
- else - else
- viewer.prepare! - viewer.prepare!
......
.file-content.code.js-syntax-highlight
.line-numbers
- if blob.data.present?
- blob.data.each_line.each_with_index do |_, index|
%span.diff-line-num= index + 1
.blob-content{ data: { blob_id: blob.id } }
= highlight(blob.path, blob.data, repository: nil, plain: blob.no_highlighting?)
.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } .js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20.prepend-top-10{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
.banner-graphic .banner-graphic
= custom_icon('icon_autodevops') = custom_icon('icon_autodevops')
.prepend-top-10.prepend-left-10.append-bottom-10 .banner-body.prepend-left-10.append-bottom-10
%h5= s_('AutoDevOps|Auto DevOps (Beta)') %h5.banner-title= s_('AutoDevOps|Auto DevOps (Beta)')
%p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
%p %p
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
.prepend-top-10 .banner-buttons
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout' = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout'
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
......
.banner-callout.compact.milestone-deprecation-message.js-milestone-deprecation-message.prepend-top-20
.banner-graphic= image_tag 'illustrations/milestone_removing-page.svg'
.banner-body.prepend-left-10.append-right-10
%h5.banner-title.prepend-top-0= _('This page will be removed in a future release.')
%p.milestone-banner-text= _('Use group milestones to manage issues from multiple projects in the same milestone.')
= button_tag _('Promote these project milestones into a group milestone.'), class: 'btn btn-link js-popover-link text-align-left milestone-banner-link'
.milestone-banner-buttons.prepend-top-20= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-default', target: '_blank'
%template.js-milestone-deprecation-message-template
.milestone-popover-body
%ol.milestone-popover-instructions-list.append-bottom-0
%li= _('Click any <strong>project name</strong> in the project list below to navigate to the project milestone.').html_safe
%li= _('Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.').html_safe
.milestone-popover-footer= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-link prepend-left-0', target: '_blank'
- page_title @milestone.title - page_title milestone.title
- @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title) - @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- group = local_assigns[:group] - group = local_assigns[:group]
- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone?
.detail-page-header .detail-page-header
%a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" } %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
...@@ -31,21 +32,23 @@ ...@@ -31,21 +32,23 @@
- else - else
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
= render 'shared/milestones/deprecation_message' if is_dynamic_milestone
.detail-page-description.milestone-detail .detail-page-description.milestone-detail
%h2.title %h2.title
= markdown_field(milestone, :title) = markdown_field(milestone, :title)
- if @milestone.group_milestone? && @milestone.description.present? - if milestone.group_milestone? && milestone.description.present?
%div %div
.description .description
.wiki .wiki
= markdown_field(@milestone, :description) = markdown_field(milestone, :description)
- if milestone.complete?(current_user) && milestone.active? - if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default .alert.alert-success.prepend-top-default
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.' - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg} %span All issues for this milestone are closed. #{close_msg}
- if @milestone.legacy_group_milestone? || @milestone.dashboard_milestone? - if is_dynamic_milestone
.table-holder .table-holder
%table.table %table.table
%thead %thead
...@@ -68,7 +71,7 @@ ...@@ -68,7 +71,7 @@
Open Open
%td %td
= ms.expires_at = ms.expires_at
- elsif @milestone.group_milestone? - elsif milestone.group_milestone?
%br %br
View View
= link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title) = link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title)
......
- blob = @snippet.blob
.gitlab-embed-snippets
.js-file-title.file-title-flex-parent
.file-header-content
= external_snippet_icon('doc_text')
%strong.file-title-name
%a.gitlab-embedded-snippets-title{ href: url_for(only_path: false, overwrite_params: nil) }
= blob.name
%small
= number_to_human_size(blob.raw_size)
%a.gitlab-logo{ href: url_for(only_path: false, overwrite_params: nil), title: 'view on gitlab' }
on &nbsp;
%span.logo-text
GitLab
.file-actions.hidden-xs
.btn-group{ role: "group" }<
= embedded_snippet_raw_button
= embedded_snippet_download_button
%article.file-holder.snippet-file-content
= render 'projects/blob/viewer', viewer: @snippet.blob.simple_viewer, load_async: false, external_embed: true
...@@ -19,11 +19,32 @@ ...@@ -19,11 +19,32 @@
%h2.snippet-title.prepend-top-0.append-bottom-0 %h2.snippet-title.prepend-top-0.append-bottom-0
= markdown_field(@snippet, :title) = markdown_field(@snippet, :title)
- if @snippet.updated_at != @snippet.created_at
= edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true)
- if @snippet.description.present? - if @snippet.description.present?
.description .description
.wiki .wiki
= markdown_field(@snippet, :description) = markdown_field(@snippet, :description)
%textarea.hidden.js-task-list-field %textarea.hidden.js-task-list-field
= @snippet.description = @snippet.description
- if @snippet.updated_at != @snippet.created_at
= edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true)
- if public_snippet?
.embed-snippet
.input-group
.input-group-btn
%button.btn.embed-toggle{ 'data-toggle': 'dropdown', type: 'button' }
%span.js-embed-action= _("Embed")
= sprite_icon('angle-down', size: 12)
%ul.dropdown-menu.dropdown-menu-selectable.embed-toggle-list
%li
%button.js-embed-btn.btn.btn-transparent.is-active{ type: 'button' }
%strong.embed-toggle-list-item= _("Embed")
%li
%button.js-share-btn.btn.btn-transparent{ type: 'button' }
%strong.embed-toggle-list-item= _("Share")
%input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
.input-group-btn
%button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' }
= sprite_icon('duplicate', size: 16)
.clearfix
document.write('#{escape_javascript(stylesheet_link_tag "#{stylesheet_url 'snippets'}")}');
document.write('#{escape_javascript(render 'shared/snippets/embed')}');
---
title: Now `rake cache:clear` will also clear pipeline status cache
merge_request: 18257
author:
type: fixed
---
title: Adds Embedded Snippets Support
merge_request: 15695
author: haseebeqx
type: added
---
title: Validate project path prior to hitting the database.
merge_request: 18322
author:
type: performance
---
title: git SHA is now displayed alongside the GitLab version on the Admin Dashboard
merge_request:
author:
type: added
---
title: Replace the `project/commits/comments.feature` spinach test with an rspec analog
merge_request: 18356
author: "@blackst0ne"
type: other
---
title: Add deprecation message to dynamic milestone pages
merge_request: 17505
author:
type: added
...@@ -113,6 +113,7 @@ module Gitlab ...@@ -113,6 +113,7 @@ module Gitlab
config.assets.precompile << "performance_bar.css" config.assets.precompile << "performance_bar.css"
config.assets.precompile << "lib/ace.js" config.assets.precompile << "lib/ace.js"
config.assets.precompile << "test.css" config.assets.precompile << "test.css"
config.assets.precompile << "snippets.css"
config.assets.precompile << "locale/**/app.js" config.assets.precompile << "locale/**/app.js"
# Import gitlab-svgs directly from vendored directory # Import gitlab-svgs directly from vendored directory
......
deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab') deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab')
if Gitlab.inc_controlled? || Rails.env.development? if Gitlab.com? || Rails.env.development?
ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator) ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator)
end end
...@@ -183,7 +183,7 @@ instant how code changes impact your production environment. ...@@ -183,7 +183,7 @@ instant how code changes impact your production environment.
### Git and GitLab ### Git and GitLab
- [Git](topics/git/index.md): Getting started with Git, branching strategies, Git LFS, advanced use. - [Git](topics/git/index.md): Getting started with Git, branching strategies, Git LFS, advanced use.
- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf): Download a PDF describing the most used Git operations. - [Git cheatsheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf): Download a PDF describing the most used Git operations.
- [GitLab Flow](workflow/gitlab_flow.md): explore the best of Git with the GitLab Flow strategy. - [GitLab Flow](workflow/gitlab_flow.md): explore the best of Git with the GitLab Flow strategy.
## Administrator documentation ## Administrator documentation
......
...@@ -15,7 +15,7 @@ Parameters: ...@@ -15,7 +15,7 @@ Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, `marked`, or `approval_required`. | | `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, `marked`, `approval_required`, `unmergeable` or `directly_addressed`. |
| `author_id` | integer | no | The ID of an author | | `author_id` | integer | no | The ID of an author |
| `project_id` | integer | no | The ID of a project | | `project_id` | integer | no | The ID of a project |
| `state` | string | no | The state of the todo. Can be either `pending` or `done` | | `state` | string | no | The state of the todo. Can be either `pending` or `done` |
......
...@@ -101,12 +101,12 @@ In order to do that, follow the steps: ...@@ -101,12 +101,12 @@ In order to do that, follow the steps:
--registration-token REGISTRATION_TOKEN \ --registration-token REGISTRATION_TOKEN \
--executor docker \ --executor docker \
--description "My Docker Runner" \ --description "My Docker Runner" \
--docker-image "docker:latest" \ --docker-image "docker:stable" \
--docker-privileged --docker-privileged
``` ```
The above command will register a new Runner to use the special The above command will register a new Runner to use the special
`docker:latest` image which is provided by Docker. **Notice that it's using `docker:stable` image which is provided by Docker. **Notice that it's using
the `privileged` mode to start the build and service containers.** If you the `privileged` mode to start the build and service containers.** If you
want to use [docker-in-docker] mode, you always have to use `privileged = true` want to use [docker-in-docker] mode, you always have to use `privileged = true`
in your Docker containers. in your Docker containers.
...@@ -120,7 +120,7 @@ In order to do that, follow the steps: ...@@ -120,7 +120,7 @@ In order to do that, follow the steps:
executor = "docker" executor = "docker"
[runners.docker] [runners.docker]
tls_verify = false tls_verify = false
image = "docker:latest" image = "docker:stable"
privileged = true privileged = true
disable_cache = false disable_cache = false
volumes = ["/cache"] volumes = ["/cache"]
...@@ -132,7 +132,7 @@ In order to do that, follow the steps: ...@@ -132,7 +132,7 @@ In order to do that, follow the steps:
`docker:dind` service): `docker:dind` service):
```yaml ```yaml
image: docker:latest image: docker:stable
# When using dind, it's wise to use the overlayfs driver for # When using dind, it's wise to use the overlayfs driver for
# improved performance. # improved performance.
...@@ -201,12 +201,12 @@ In order to do that, follow the steps: ...@@ -201,12 +201,12 @@ In order to do that, follow the steps:
--registration-token REGISTRATION_TOKEN \ --registration-token REGISTRATION_TOKEN \
--executor docker \ --executor docker \
--description "My Docker Runner" \ --description "My Docker Runner" \
--docker-image "docker:latest" \ --docker-image "docker:stable" \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock --docker-volumes /var/run/docker.sock:/var/run/docker.sock
``` ```
The above command will register a new Runner to use the special The above command will register a new Runner to use the special
`docker:latest` image which is provided by Docker. **Notice that it's using `docker:stable` image which is provided by Docker. **Notice that it's using
the Docker daemon of the Runner itself, and any containers spawned by docker the Docker daemon of the Runner itself, and any containers spawned by docker
commands will be siblings of the Runner rather than children of the runner.** commands will be siblings of the Runner rather than children of the runner.**
This may have complications and limitations that are unsuitable for your workflow. This may have complications and limitations that are unsuitable for your workflow.
...@@ -220,7 +220,7 @@ In order to do that, follow the steps: ...@@ -220,7 +220,7 @@ In order to do that, follow the steps:
executor = "docker" executor = "docker"
[runners.docker] [runners.docker]
tls_verify = false tls_verify = false
image = "docker:latest" image = "docker:stable"
privileged = false privileged = false
disable_cache = false disable_cache = false
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"] volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
...@@ -232,7 +232,7 @@ In order to do that, follow the steps: ...@@ -232,7 +232,7 @@ In order to do that, follow the steps:
include the `docker:dind` service as when using the Docker in Docker executor): include the `docker:dind` service as when using the Docker in Docker executor):
```yaml ```yaml
image: docker:latest image: docker:stable
before_script: before_script:
- docker info - docker info
...@@ -286,7 +286,7 @@ any image that's used with the `--cache-from` argument must first be pulled ...@@ -286,7 +286,7 @@ any image that's used with the `--cache-from` argument must first be pulled
Here's a simple `.gitlab-ci.yml` file showing how Docker caching can be utilized: Here's a simple `.gitlab-ci.yml` file showing how Docker caching can be utilized:
```yaml ```yaml
image: docker:latest image: docker:stable
services: services:
- docker:dind - docker:dind
...@@ -388,7 +388,7 @@ could look like: ...@@ -388,7 +388,7 @@ could look like:
```yaml ```yaml
build: build:
image: docker:latest image: docker:stable
services: services:
- docker:dind - docker:dind
stage: build stage: build
...@@ -434,7 +434,7 @@ when needed. Changes to `master` also get tagged as `latest` and deployed using ...@@ -434,7 +434,7 @@ when needed. Changes to `master` also get tagged as `latest` and deployed using
an application-specific deploy script: an application-specific deploy script:
```yaml ```yaml
image: docker:latest image: docker:stable
services: services:
- docker:dind - docker:dind
......
...@@ -86,7 +86,7 @@ services](#accessing-the-services). ...@@ -86,7 +86,7 @@ services](#accessing-the-services).
### How the health check of services works ### How the health check of services works
Services are designed to provide additional functionality which is **network accessible**. Services are designed to provide additional functionality which is **network accessible**.
It may be a database like MySQL, or Redis, and even `docker:dind` which It may be a database like MySQL, or Redis, and even `docker:stable-dind` which
allows you to use Docker in Docker. It can be practically anything that is allows you to use Docker in Docker. It can be practically anything that is
required for the CI/CD job to proceed and is accessed by network. required for the CI/CD job to proceed and is accessed by network.
......
...@@ -17,7 +17,7 @@ performance: ...@@ -17,7 +17,7 @@ performance:
variables: variables:
URL: https://example.com URL: https://example.com
services: services:
- docker:dind - docker:stable-dind
script: script:
- mkdir gitlab-exporter - mkdir gitlab-exporter
- wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
...@@ -94,7 +94,7 @@ performance: ...@@ -94,7 +94,7 @@ performance:
stage: performance stage: performance
image: docker:git image: docker:git
services: services:
- docker:dind - docker:stable-dind
dependencies: dependencies:
- review - review
script: script:
......
...@@ -308,7 +308,9 @@ except master. ...@@ -308,7 +308,9 @@ except master.
## `only` and `except` (complex) ## `only` and `except` (complex)
> Introduced in GitLab 10.0 > `refs` and `kubernetes` policies introduced in GitLab 10.0
> `variables` policy introduced in 10.7
CAUTION: **Warning:** CAUTION: **Warning:**
This an _alpha_ feature, and it it subject to change at any time without This an _alpha_ feature, and it it subject to change at any time without
......
...@@ -80,7 +80,7 @@ runners: ...@@ -80,7 +80,7 @@ runners:
image: ubuntu:16.04 image: ubuntu:16.04
## Run all containers with the privileged flag enabled ## Run all containers with the privileged flag enabled
## This will allow the docker:dind image to run if you need to run Docker ## This will allow the docker:stable-dind image to run if you need to run Docker
## commands. Please read the docs before turning this on: ## commands. Please read the docs before turning this on:
## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind ## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind
## ##
...@@ -147,7 +147,7 @@ enable privileged mode in `values.yaml`: ...@@ -147,7 +147,7 @@ enable privileged mode in `values.yaml`:
```yaml ```yaml
runners: runners:
## Run all containers with the privileged flag enabled ## Run all containers with the privileged flag enabled
## This will allow the docker:dind image to run if you need to run Docker ## This will allow the docker:stable-dind image to run if you need to run Docker
## commands. Please read the docs before turning this on: ## commands. Please read the docs before turning this on:
## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind ## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind
## ##
......
...@@ -15,8 +15,8 @@ in the table below. ...@@ -15,8 +15,8 @@ in the table below.
Once you have configured and enabled Custom Issue Tracker Service you'll see a link on the GitLab project pages that takes you to that custom issue tracker. Once you have configured and enabled Custom Issue Tracker Service you'll see a link on the GitLab project pages that takes you to that custom issue tracker.
## Referencing issues ## Referencing issues
Issues are referenced with `#<ID>`, where `<ID>` is a number (example `#143`). - Issues are referenced with `ANYTHING-<ID>`, where `ANYTHING` can be any string and `<ID>` is a number used in the target project of the custom integration (example `PROJECT-143`).
So with the example above, `#143` would refer to `https://customissuetracker.com/project-name/143`. - `ANYTHING` is a placeholder to differentiate against GitLab issues, which are referenced with `#<ID>`. You can use a project name or project key to replace it for example.
\ No newline at end of file - So with the example above, `PROJECT-143` would refer to `https://customissuetracker.com/project-name/143`.
\ No newline at end of file
@project_commits
Feature: Project Commits Comments
Background:
Given I sign in as a user
And I own project "Shop"
And I visit project commit page
@javascript
Scenario: I can comment on a commit
Given I leave a comment like "XML attached"
Then I should see a comment saying "XML attached"
@javascript
Scenario: I can't cancel the main form
Then I should not see the cancel comment button
@javascript
Scenario: I can preview with text
Given I write a comment like ":+1: Nice"
Then The comment preview tab should be display rendered Markdown
@javascript
Scenario: I preview a comment
Given I preview a comment text like "Bug fixed :smile:"
Then I should see the comment preview
And I should not see the comment text field
@javascript
Scenario: I can edit after preview
Given I preview a comment text like "Bug fixed :smile:"
Then I should see the comment write tab
@javascript
Scenario: I have a reset form after posting from preview
Given I preview a comment text like "Bug fixed :smile:"
And I submit the comment
Then I should see an empty comment text field
And I should not see the comment preview
@javascript
Scenario: I can delete a comment
Given I leave a comment like "XML attached"
Then I should see a comment saying "XML attached"
And I delete a comment
Then I should not see a comment saying "XML attached"
@javascript
Scenario: I can edit a comment with +1
Given I leave a comment like "XML attached"
And I edit the last comment with a +1
Then I should see +1 in the description
...@@ -6,70 +6,12 @@ module SharedNote ...@@ -6,70 +6,12 @@ module SharedNote
wait_for_requests if javascript_test? wait_for_requests if javascript_test?
end end
step 'I delete a comment' do
page.within('.main-notes-list') do
note = find('.note')
note.hover
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
accept_confirm { find(".js-note-delete").click }
end
end
step 'I haven\'t written any comment text' do step 'I haven\'t written any comment text' do
page.within(".js-main-target-form") do page.within(".js-main-target-form") do
fill_in "note[note]", with: "" fill_in "note[note]", with: ""
end end
end end
step 'I leave a comment like "XML attached"' do
page.within(".js-main-target-form") do
fill_in "note[note]", with: "XML attached"
click_button "Comment"
end
wait_for_requests
end
step 'I preview a comment text like "Bug fixed :smile:"' do
page.within(".js-main-target-form") do
fill_in "note[note]", with: "Bug fixed :smile:"
find('.js-md-preview-button').click
end
end
step 'I submit the comment' do
page.within(".js-main-target-form") do
click_button "Comment"
end
wait_for_requests
end
step 'I write a comment like ":+1: Nice"' do
page.within(".js-main-target-form") do
fill_in 'note[note]', with: ':+1: Nice'
end
end
step 'I should not see a comment saying "XML attached"' do
expect(page).not_to have_css(".note")
end
step 'I should not see the cancel comment button' do
page.within(".js-main-target-form") do
should_not have_link("Cancel")
end
end
step 'I should not see the comment preview' do
page.within(".js-main-target-form") do
expect(find('.js-md-preview')).not_to be_visible
end
end
step 'The comment preview tab should say there is nothing to do' do step 'The comment preview tab should say there is nothing to do' do
page.within(".js-main-target-form") do page.within(".js-main-target-form") do
find('.js-md-preview-button').click find('.js-md-preview-button').click
...@@ -77,71 +19,7 @@ module SharedNote ...@@ -77,71 +19,7 @@ module SharedNote
end end
end end
step 'I should not see the comment text field' do
page.within(".js-main-target-form") do
expect(find('.js-note-text')).not_to be_visible
end
end
step 'I should see a comment saying "XML attached"' do
page.within(".note") do
expect(page).to have_content("XML attached")
end
end
step 'I should see an empty comment text field' do
page.within(".js-main-target-form") do
expect(page).to have_field("note[note]", with: "")
end
end
step 'I should see the comment write tab' do
page.within(".js-main-target-form") do
expect(page).to have_css('.js-md-write-button', visible: true)
end
end
step 'The comment preview tab should be display rendered Markdown' do
page.within(".js-main-target-form") do
find('.js-md-preview-button').click
expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true)
end
end
step 'I should see the comment preview' do
page.within(".js-main-target-form") do
expect(page).to have_css('.js-md-preview', visible: true)
end
end
step 'I should see no notes at all' do step 'I should see no notes at all' do
expect(page).not_to have_css('.note') expect(page).not_to have_css('.note')
end end
# Markdown
step 'I edit the last comment with a +1' do
page.within(".main-notes-list") do
note = find('.note')
note.hover
note.find('.js-note-edit').click
end
page.find('.current-note-edit-form textarea')
page.within(".current-note-edit-form") do
fill_in 'note[note]', with: '+1 Awesome!'
click_button 'Save comment'
end
wait_for_requests
end
step 'I should see +1 in the description' do
page.within(".note") do
expect(page).to have_content("+1 Awesome!")
end
wait_for_requests
end
end end
...@@ -21,13 +21,7 @@ Capybara.register_driver :chrome do |app| ...@@ -21,13 +21,7 @@ Capybara.register_driver :chrome do |app|
options.add_argument("no-sandbox") options.add_argument("no-sandbox")
# Run headless by default unless CHROME_HEADLESS specified # Run headless by default unless CHROME_HEADLESS specified
unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
options.add_argument("headless")
# Chrome documentation says this flag is needed for now
# https://developers.google.com/web/updates/2017/04/headless-chrome#cli
options.add_argument("disable-gpu")
end
# Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252 # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER'] options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER']
......
...@@ -103,9 +103,9 @@ module API ...@@ -103,9 +103,9 @@ module API
end end
def find_project(id) def find_project(id)
if id =~ /^\d+$/ if id.is_a?(Integer) || id =~ /^\d+$/
Project.find_by(id: id) Project.find_by(id: id)
else elsif id.include?("/")
Project.find_by_full_path(id) Project.find_by_full_path(id)
end end
end end
......
...@@ -3,21 +3,18 @@ require_dependency 'gitlab/git' ...@@ -3,21 +3,18 @@ require_dependency 'gitlab/git'
module Gitlab module Gitlab
COM_URL = 'https://gitlab.com'.freeze COM_URL = 'https://gitlab.com'.freeze
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))} APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
def self.com? def self.com?
# Check `staging?` as well to keep parity with gitlab.com # Check `gl_subdomain?` as well to keep parity with gitlab.com
Gitlab.config.gitlab.url == COM_URL || staging? Gitlab.config.gitlab.url == COM_URL || gl_subdomain?
end end
def self.staging? def self.gl_subdomain?
Gitlab.config.gitlab.url == 'https://staging.gitlab.com' SUBDOMAIN_REGEX === Gitlab.config.gitlab.url
end end
def self.dev? def self.dev_env_or_com?
Gitlab.config.gitlab.url == 'https://dev.gitlab.org' Rails.env.test? || Rails.env.development? || com?
end
def self.inc_controlled?
dev? || staging? || com?
end end
end end
...@@ -40,7 +40,7 @@ module Gitlab ...@@ -40,7 +40,7 @@ module Gitlab
end end
def self.cache_key_for_project(project) def self.cache_key_for_project(project)
"projects/#{project.id}/pipeline_status" "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:projects/#{project.id}/pipeline_status"
end end
def self.update_for_pipeline(pipeline) def self.update_for_pipeline(pipeline)
......
...@@ -6,17 +6,22 @@ namespace :cache do ...@@ -6,17 +6,22 @@ namespace :cache do
desc "GitLab | Clear redis cache" desc "GitLab | Clear redis cache"
task redis: :environment do task redis: :environment do
Gitlab::Redis::Cache.with do |redis| Gitlab::Redis::Cache.with do |redis|
cursor = REDIS_SCAN_START_STOP cache_key_pattern = %W[#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*
loop do projects/*/pipeline_status]
cursor, keys = redis.scan(
cursor,
match: "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*",
count: REDIS_CLEAR_BATCH_SIZE
)
redis.del(*keys) if keys.any? cache_key_pattern.each do |match|
cursor = REDIS_SCAN_START_STOP
loop do
cursor, keys = redis.scan(
cursor,
match: match,
count: REDIS_CLEAR_BATCH_SIZE
)
break if cursor == REDIS_SCAN_START_STOP redis.del(*keys) if keys.any?
break if cursor == REDIS_SCAN_START_STOP
end
end end
end end
end end
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
}, },
"dependencies": { "dependencies": {
"@gitlab-org/gitlab-svgs": "^1.17.0", "@gitlab-org/gitlab-svgs": "^1.18.0",
"autosize": "^4.0.0", "autosize": "^4.0.0",
"axios": "^0.17.1", "axios": "^0.17.1",
"babel-core": "^6.26.0", "babel-core": "^6.26.0",
......
...@@ -72,11 +72,10 @@ describe Admin::ApplicationSettingsController do ...@@ -72,11 +72,10 @@ describe Admin::ApplicationSettingsController do
expect(ApplicationSetting.current.restricted_visibility_levels).to eq([10, 20]) expect(ApplicationSetting.current.restricted_visibility_levels).to eq([10, 20])
end end
it 'falls back to defaults when settings are omitted' do it 'updates the restricted_visibility_levels when empty array is passed' do
put :update, application_setting: {} put :update, application_setting: { restricted_visibility_levels: [] }
expect(response).to redirect_to(admin_application_settings_path) expect(response).to redirect_to(admin_application_settings_path)
expect(ApplicationSetting.current.default_project_visibility).to eq(Gitlab::VisibilityLevel::PRIVATE)
expect(ApplicationSetting.current.restricted_visibility_levels).to be_empty expect(ApplicationSetting.current.restricted_visibility_levels).to be_empty
end end
end end
......
...@@ -2,10 +2,15 @@ require 'spec_helper' ...@@ -2,10 +2,15 @@ require 'spec_helper'
require 'rainbow/ext/string' require 'rainbow/ext/string'
describe 'seed production settings' do describe 'seed production settings' do
include StubENV
let(:settings_file) { Rails.root.join('db/fixtures/production/010_settings.rb') } let(:settings_file) { Rails.root.join('db/fixtures/production/010_settings.rb') }
let(:settings) { Gitlab::CurrentSettings.current_application_settings } let(:settings) { Gitlab::CurrentSettings.current_application_settings }
before do
# It's important to set this variable so that we don't save a memoized
# (supposed to be) in-memory record in `Gitlab::CurrentSettings.in_memory_application_settings`
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do
before do before do
stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789') stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789')
......
...@@ -206,7 +206,7 @@ FactoryBot.define do ...@@ -206,7 +206,7 @@ FactoryBot.define do
options do options do
{ {
image: { name: 'ruby:2.1', entrypoint: '/bin/sh' }, image: { name: 'ruby:2.1', entrypoint: '/bin/sh' },
services: ['postgres', { name: 'docker:dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }], services: ['postgres', { name: 'docker:stable-dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }],
after_script: %w(ls date), after_script: %w(ls date),
artifacts: { artifacts: {
name: 'artifacts_file', name: 'artifacts_file',
......
...@@ -32,6 +32,29 @@ feature 'Admin updates settings' do ...@@ -32,6 +32,29 @@ feature 'Admin updates settings' do
expect(find('#application_setting_visibility_level_20')).not_to be_checked expect(find('#application_setting_visibility_level_20')).not_to be_checked
end end
scenario 'Modify import sources' do
expect(Gitlab::CurrentSettings.import_sources).not_to be_empty
page.within('.as-visibility-access') do
Gitlab::ImportSources.options.map do |name, _|
uncheck name
end
click_button 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
expect(Gitlab::CurrentSettings.import_sources).to be_empty
page.within('.as-visibility-access') do
check "Repo by URL"
click_button 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
expect(Gitlab::CurrentSettings.import_sources).to eq(['git'])
end
scenario 'Change Visibility and Access Controls' do scenario 'Change Visibility and Access Controls' do
page.within('.as-visibility-access') do page.within('.as-visibility-access') do
uncheck 'Project export enabled' uncheck 'Project export enabled'
...@@ -62,6 +85,26 @@ feature 'Admin updates settings' do ...@@ -62,6 +85,26 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully" expect(page).to have_content "Application settings saved successfully"
end end
scenario 'Modify oauth providers' do
expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty
page.within('.as-signin') do
uncheck 'Google'
click_button 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2')
page.within('.as-signin') do
check "Google"
click_button 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).not_to include('google_oauth2')
end
scenario 'Change Help page' do scenario 'Change Help page' do
page.within('.as-help-page') do page.within('.as-help-page') do
fill_in 'Help page text', with: 'Example text' fill_in 'Help page text', with: 'Example text'
...@@ -211,16 +254,6 @@ feature 'Admin updates settings' do ...@@ -211,16 +254,6 @@ feature 'Admin updates settings' do
expect(find('#service_push_channel').value).to eq '#test_channel' expect(find('#service_push_channel').value).to eq '#test_channel'
end end
context 'sign-in restrictions', :js do
it 'de-activates oauth sign-in source' do
page.within('.as-signin') do
find('input#application_setting_enabled_oauth_sign_in_sources_[value=gitlab]').send_keys(:return)
expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active')
end
end
end
scenario 'Change Keys settings' do scenario 'Change Keys settings' do
page.within('.as-visibility-access') do page.within('.as-visibility-access') do
select 'Are forbidden', from: 'RSA SSH keys' select 'Are forbidden', from: 'RSA SSH keys'
......
...@@ -108,4 +108,18 @@ feature 'Milestone' do ...@@ -108,4 +108,18 @@ feature 'Milestone' do
expect(page).to have_selector('.js-delete-milestone-button', count: 0) expect(page).to have_selector('.js-delete-milestone-button', count: 0)
end end
end end
feature 'deprecation popover', :js do
it 'opens deprecation popover' do
milestone = create(:milestone, project: project)
visit group_milestone_path(group, milestone, title: milestone.title)
expect(page).to have_selector('.milestone-deprecation-message')
find('.milestone-deprecation-message .js-popover-link').click
expect(page).to have_selector('.milestone-deprecation-message .popover')
end
end
end end
require "spec_helper"
describe "User comments on commit", :js do
include Spec::Support::Helpers::Features::NotesHelpers
include RepoHelpers
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
COMMENT_TEXT = "XML attached".freeze
before do
sign_in(user)
project.add_developer(user)
visit(project_commit_path(project, sample_commit.id))
end
context "when adding new comment" do
it "adds comment" do
EMOJI = ":+1:".freeze
page.within(".js-main-target-form") do
expect(page).not_to have_link("Cancel")
fill_in("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}")
# Check on `Preview` tab
click_link("Preview")
expect(find(".js-md-preview")).to have_content(COMMENT_TEXT).and have_css("gl-emoji")
expect(page).not_to have_css(".js-note-text")
# Check on `Write` tab
click_link("Write")
expect(page).to have_field("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}")
# Submit comment from the `Preview` tab to get rid of a separate `it` block
# which would specially tests if everything gets cleared from the note form.
click_link("Preview")
click_button("Comment")
end
wait_for_requests
page.within(".note") do
expect(page).to have_content(COMMENT_TEXT).and have_css("gl-emoji")
end
page.within(".js-main-target-form") do
expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview")
end
end
end
context "when editing comment" do
before do
add_note(COMMENT_TEXT)
end
it "edits comment" do
NEW_COMMENT_TEXT = "+1 Awesome!".freeze
page.within(".main-notes-list") do
note = find(".note")
note.hover
note.find(".js-note-edit").click
end
page.find(".current-note-edit-form textarea")
page.within(".current-note-edit-form") do
fill_in("note[note]", with: NEW_COMMENT_TEXT)
click_button("Save comment")
end
wait_for_requests
page.within(".note") do
expect(page).to have_content(NEW_COMMENT_TEXT)
end
end
end
context "when deleting comment" do
before do
add_note(COMMENT_TEXT)
end
it "deletes comment" do
page.within(".note") do
expect(page).to have_content(COMMENT_TEXT)
end
page.within(".main-notes-list") do
note = find(".note")
note.hover
find(".more-actions").click
find(".more-actions .dropdown-menu li", match: :first)
accept_confirm { find(".js-note-delete").click }
end
expect(page).not_to have_css(".note")
end
end
end
require 'spec_helper'
describe 'Embedded Snippets' do
let(:snippet) { create(:personal_snippet, :public, file_name: 'random_dir.rb', content: content) }
let(:content) { "require 'fileutils'\nFileUtils.mkdir_p 'some/random_dir'\n" }
it 'loads snippet', :js do
script_url = "http://#{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}/#{snippet_path(snippet, format: 'js')}"
embed_body = "<html><body><script src=\"#{script_url}\"></script></body></html>"
rack_app = proc do
['200', { 'Content-Type' => 'text/html' }, [embed_body]]
end
server = Capybara::Server.new(rack_app)
server.boot
visit("http://#{server.host}:#{server.port}/embedded_snippet.html")
expect(page).to have_content("random_dir.rb")
expect(page).to have_content("require 'fileutils'")
expect(page).to have_link('Open raw')
expect(page).to have_link('Download')
end
end
...@@ -162,4 +162,11 @@ describe IconsHelper do ...@@ -162,4 +162,11 @@ describe IconsHelper do
expect(file_type_icon_class('file', 0, 'CHANGELOG')).to eq 'file-text-o' expect(file_type_icon_class('file', 0, 'CHANGELOG')).to eq 'file-text-o'
end end
end end
describe '#external_snippet_icon' do
it 'returns external snippet icon' do
expect(external_snippet_icon('download').to_s)
.to eq("<span class=\"gl-snippet-icon gl-snippet-icon-download\"></span>")
end
end
end end
require 'spec_helper'
describe SnippetsHelper do
include IconsHelper
describe '#embedded_snippet_raw_button' do
it 'gives view raw button of embedded snippets for project snippets' do
@snippet = create(:project_snippet, :public)
expect(embedded_snippet_raw_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open raw\" href=\"#{raw_project_snippet_url(@snippet.project, @snippet)}\">#{external_snippet_icon('doc_code')}</a>")
end
it 'gives view raw button of embedded snippets for personal snippets' do
@snippet = create(:personal_snippet, :public)
expect(embedded_snippet_raw_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open raw\" href=\"#{raw_snippet_url(@snippet)}\">#{external_snippet_icon('doc_code')}</a>")
end
end
describe '#embedded_snippet_download_button' do
it 'gives download button of embedded snippets for project snippets' do
@snippet = create(:project_snippet, :public)
expect(embedded_snippet_download_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" title=\"Download\" rel=\"noopener noreferrer\" href=\"#{raw_project_snippet_url(@snippet.project, @snippet, inline: false)}\">#{external_snippet_icon('download')}</a>")
end
it 'gives download button of embedded snippets for personal snippets' do
@snippet = create(:personal_snippet, :public)
expect(embedded_snippet_download_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" title=\"Download\" rel=\"noopener noreferrer\" href=\"#{raw_snippet_url(@snippet, inline: false)}\">#{external_snippet_icon('download')}</a>")
end
end
end
...@@ -3,12 +3,11 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -3,12 +3,11 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { import {
getSelector, getSelector,
togglePopover,
dismiss, dismiss,
mouseleave,
mouseenter,
inserted, inserted,
} from '~/feature_highlight/feature_highlight_helper'; } from '~/feature_highlight/feature_highlight_helper';
import { togglePopover } from '~/shared/popover';
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
describe('feature highlight helper', () => { describe('feature highlight helper', () => {
...@@ -19,110 +18,6 @@ describe('feature highlight helper', () => { ...@@ -19,110 +18,6 @@ describe('feature highlight helper', () => {
}); });
}); });
describe('togglePopover', () => {
describe('togglePopover(true)', () => {
it('returns true when popover is shown', () => {
const context = {
hasClass: () => false,
popover: () => {},
toggleClass: () => {},
};
expect(togglePopover.call(context, true)).toEqual(true);
});
it('returns false when popover is already shown', () => {
const context = {
hasClass: () => true,
};
expect(togglePopover.call(context, true)).toEqual(false);
});
it('shows popover', (done) => {
const context = {
hasClass: () => false,
popover: () => {},
toggleClass: () => {},
};
spyOn(context, 'popover').and.callFake((method) => {
expect(method).toEqual('show');
done();
});
togglePopover.call(context, true);
});
it('adds disable-animation and js-popover-show class', (done) => {
const context = {
hasClass: () => false,
popover: () => {},
toggleClass: () => {},
};
spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
expect(classNames).toEqual('disable-animation js-popover-show');
expect(show).toEqual(true);
done();
});
togglePopover.call(context, true);
});
});
describe('togglePopover(false)', () => {
it('returns true when popover is hidden', () => {
const context = {
hasClass: () => true,
popover: () => {},
toggleClass: () => {},
};
expect(togglePopover.call(context, false)).toEqual(true);
});
it('returns false when popover is already hidden', () => {
const context = {
hasClass: () => false,
};
expect(togglePopover.call(context, false)).toEqual(false);
});
it('hides popover', (done) => {
const context = {
hasClass: () => true,
popover: () => {},
toggleClass: () => {},
};
spyOn(context, 'popover').and.callFake((method) => {
expect(method).toEqual('hide');
done();
});
togglePopover.call(context, false);
});
it('removes disable-animation and js-popover-show class', (done) => {
const context = {
hasClass: () => true,
popover: () => {},
toggleClass: () => {},
};
spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
expect(classNames).toEqual('disable-animation js-popover-show');
expect(show).toEqual(false);
done();
});
togglePopover.call(context, false);
});
});
});
describe('dismiss', () => { describe('dismiss', () => {
let mock; let mock;
const context = { const context = {
...@@ -163,56 +58,6 @@ describe('feature highlight helper', () => { ...@@ -163,56 +58,6 @@ describe('feature highlight helper', () => {
}); });
}); });
describe('mouseleave', () => {
it('calls hide popover if .popover:hover is false', () => {
const fakeJquery = {
length: 0,
};
spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
spyOn(togglePopover, 'call');
mouseleave();
expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false);
});
it('does not call hide popover if .popover:hover is true', () => {
const fakeJquery = {
length: 1,
};
spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
spyOn(togglePopover, 'call');
mouseleave();
expect(togglePopover.call).not.toHaveBeenCalledWith(false);
});
});
describe('mouseenter', () => {
const context = {};
it('shows popover', () => {
spyOn(togglePopover, 'call').and.returnValue(false);
mouseenter.call(context);
expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true);
});
it('registers mouseleave event if popover is showed', (done) => {
spyOn(togglePopover, 'call').and.returnValue(true);
spyOn($.fn, 'on').and.callFake((eventName) => {
expect(eventName).toEqual('mouseleave');
done();
});
mouseenter.call(context);
});
it('does not register mouseleave event if popover is not showed', () => {
spyOn(togglePopover, 'call').and.returnValue(false);
const spy = spyOn($.fn, 'on').and.callFake(() => {});
mouseenter.call(context);
expect(spy).not.toHaveBeenCalled();
});
});
describe('inserted', () => { describe('inserted', () => {
it('registers click event callback', (done) => { it('registers click event callback', (done) => {
const context = { const context = {
......
import $ from 'jquery'; import $ from 'jquery';
import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper';
import * as featureHighlight from '~/feature_highlight/feature_highlight'; import * as featureHighlight from '~/feature_highlight/feature_highlight';
import * as popover from '~/shared/popover';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
...@@ -29,7 +29,6 @@ describe('feature highlight', () => { ...@@ -29,7 +29,6 @@ describe('feature highlight', () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet('/test').reply(200); mock.onGet('/test').reply(200);
spyOn(window, 'addEventListener'); spyOn(window, 'addEventListener');
spyOn(window, 'removeEventListener');
featureHighlight.setupFeatureHighlightPopover('test', 0); featureHighlight.setupFeatureHighlightPopover('test', 0);
}); });
...@@ -45,14 +44,14 @@ describe('feature highlight', () => { ...@@ -45,14 +44,14 @@ describe('feature highlight', () => {
}); });
it('setup mouseenter', () => { it('setup mouseenter', () => {
const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); const toggleSpy = spyOn(popover.togglePopover, 'call');
$(selector).trigger('mouseenter'); $(selector).trigger('mouseenter');
expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true); expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true);
}); });
it('setup debounced mouseleave', (done) => { it('setup debounced mouseleave', (done) => {
const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); const toggleSpy = spyOn(popover.togglePopover, 'call');
$(selector).trigger('mouseleave'); $(selector).trigger('mouseleave');
// Even though we've set the debounce to 0ms, setTimeout is needed for the debounce // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce
...@@ -64,12 +63,7 @@ describe('feature highlight', () => { ...@@ -64,12 +63,7 @@ describe('feature highlight', () => {
it('setup show.bs.popover', () => { it('setup show.bs.popover', () => {
$(selector).trigger('show.bs.popover'); $(selector).trigger('show.bs.popover');
expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), { once: true });
});
it('setup hide.bs.popover', () => {
$(selector).trigger('hide.bs.popover');
expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
}); });
it('removes disabled attribute', () => { it('removes disabled attribute', () => {
...@@ -85,7 +79,7 @@ describe('feature highlight', () => { ...@@ -85,7 +79,7 @@ describe('feature highlight', () => {
it('toggles when clicked', () => { it('toggles when clicked', () => {
$(selector).trigger('mouseenter'); $(selector).trigger('mouseenter');
const popoverId = $(selector).attr('aria-describedby'); const popoverId = $(selector).attr('aria-describedby');
const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); const toggleSpy = spyOn(popover.togglePopover, 'call');
$(`#${popoverId} .dismiss-feature-highlight`).click(); $(`#${popoverId} .dismiss-feature-highlight`).click();
......
...@@ -27,7 +27,7 @@ describe('RepoLoadingFile', () => { ...@@ -27,7 +27,7 @@ describe('RepoLoadingFile', () => {
const lines = [...container.querySelectorAll(':scope > div')]; const lines = [...container.querySelectorAll(':scope > div')];
expect(container).toBeTruthy(); expect(container).toBeTruthy();
expect(lines.length).toEqual(6); expect(lines.length).toEqual(3);
assertLines(lines); assertLines(lines);
}); });
} }
......
This diff is collapsed.
...@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import pipelinesComp from '~/pipelines/components/pipelines.vue'; import pipelinesComp from '~/pipelines/components/pipelines.vue';
import Store from '~/pipelines/stores/pipelines_store'; import Store from '~/pipelines/stores/pipelines_store';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { pipelineWithStages, stageReply } from './mock_data';
describe('Pipelines', () => { describe('Pipelines', () => {
const jsonFixtureName = 'pipelines/pipelines.json'; const jsonFixtureName = 'pipelines/pipelines.json';
...@@ -668,4 +669,79 @@ describe('Pipelines', () => { ...@@ -668,4 +669,79 @@ describe('Pipelines', () => {
}); });
}); });
}); });
describe('updates results when a staged is clicked', () => {
beforeEach(() => {
const copyPipeline = Object.assign({}, pipelineWithStages);
copyPipeline.id += 1;
mock
.onGet('twitter/flight/pipelines.json').reply(200, {
pipelines: [pipelineWithStages],
count: {
all: 1,
finished: 1,
pending: 0,
running: 0,
},
}, {
'POLL-INTERVAL': 100,
})
.onGet(pipelineWithStages.details.stages[0].dropdown_path)
.reply(200, stageReply);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
});
describe('when a request is being made', () => {
it('stops polling, cancels the request, fetches pipelines & restarts polling', (done) => {
spyOn(vm.poll, 'stop');
spyOn(vm.poll, 'restart');
spyOn(vm, 'getPipelines').and.returnValue(Promise.resolve());
spyOn(vm.service.cancelationSource, 'cancel').and.callThrough();
setTimeout(() => {
vm.isMakingRequest = true;
return vm.$nextTick()
.then(() => {
vm.$el.querySelector('.js-builds-dropdown-button').click();
})
.then(() => {
expect(vm.service.cancelationSource.cancel).toHaveBeenCalled();
expect(vm.poll.stop).toHaveBeenCalled();
setTimeout(() => {
expect(vm.getPipelines).toHaveBeenCalled();
expect(vm.poll.restart).toHaveBeenCalled();
done();
}, 0);
});
}, 0);
});
});
describe('when no request is being made', () => {
it('stops polling, fetches pipelines & restarts polling', (done) => {
spyOn(vm.poll, 'stop');
spyOn(vm.poll, 'restart');
spyOn(vm, 'getPipelines').and.returnValue(Promise.resolve());
setTimeout(() => {
vm.$el.querySelector('.js-builds-dropdown-button').click();
expect(vm.poll.stop).toHaveBeenCalled();
setTimeout(() => {
expect(vm.getPipelines).toHaveBeenCalled();
expect(vm.poll.restart).toHaveBeenCalled();
done();
}, 0);
}, 0);
});
});
});
}); });
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import stage from '~/pipelines/components/stage.vue'; import stage from '~/pipelines/components/stage.vue';
import eventHub from '~/pipelines/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Pipelines stage component', () => { describe('Pipelines stage component', () => {
...@@ -43,13 +44,15 @@ describe('Pipelines stage component', () => { ...@@ -43,13 +44,15 @@ describe('Pipelines stage component', () => {
mock.onGet('path.json').reply(200, { html: 'foo' }); mock.onGet('path.json').reply(200, { html: 'foo' });
}); });
it('should render the received data', done => { it('should render the received data and emit `clickedDropdown` event', done => {
spyOn(eventHub, '$emit');
component.$el.querySelector('button').click(); component.$el.querySelector('button').click();
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(), component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
).toEqual('foo'); ).toEqual('foo');
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
done(); done();
}, 0); }, 0);
}); });
......
import $ from 'jquery';
import {
togglePopover,
mouseleave,
mouseenter,
} from '~/shared/popover';
describe('popover', () => {
describe('togglePopover', () => {
describe('togglePopover(true)', () => {
it('returns true when popover is shown', () => {
const context = {
hasClass: () => false,
popover: () => {},
toggleClass: () => {},
};
expect(togglePopover.call(context, true)).toEqual(true);
});
it('returns false when popover is already shown', () => {
const context = {
hasClass: () => true,
};
expect(togglePopover.call(context, true)).toEqual(false);
});
it('shows popover', (done) => {
const context = {
hasClass: () => false,
popover: () => {},
toggleClass: () => {},
};
spyOn(context, 'popover').and.callFake((method) => {
expect(method).toEqual('show');
done();
});
togglePopover.call(context, true);
});
it('adds disable-animation and js-popover-show class', (done) => {
const context = {
hasClass: () => false,
popover: () => {},
toggleClass: () => {},
};
spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
expect(classNames).toEqual('disable-animation js-popover-show');
expect(show).toEqual(true);
done();
});
togglePopover.call(context, true);
});
});
describe('togglePopover(false)', () => {
it('returns true when popover is hidden', () => {
const context = {
hasClass: () => true,
popover: () => {},
toggleClass: () => {},
};
expect(togglePopover.call(context, false)).toEqual(true);
});
it('returns false when popover is already hidden', () => {
const context = {
hasClass: () => false,
};
expect(togglePopover.call(context, false)).toEqual(false);
});
it('hides popover', (done) => {
const context = {
hasClass: () => true,
popover: () => {},
toggleClass: () => {},
};
spyOn(context, 'popover').and.callFake((method) => {
expect(method).toEqual('hide');
done();
});
togglePopover.call(context, false);
});
it('removes disable-animation and js-popover-show class', (done) => {
const context = {
hasClass: () => true,
popover: () => {},
toggleClass: () => {},
};
spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
expect(classNames).toEqual('disable-animation js-popover-show');
expect(show).toEqual(false);
done();
});
togglePopover.call(context, false);
});
});
});
describe('mouseleave', () => {
it('calls hide popover if .popover:hover is false', () => {
const fakeJquery = {
length: 0,
};
spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
spyOn(togglePopover, 'call');
mouseleave();
expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false);
});
it('does not call hide popover if .popover:hover is true', () => {
const fakeJquery = {
length: 1,
};
spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
spyOn(togglePopover, 'call');
mouseleave();
expect(togglePopover.call).not.toHaveBeenCalledWith(false);
});
});
describe('mouseenter', () => {
const context = {};
it('shows popover', () => {
spyOn(togglePopover, 'call').and.returnValue(false);
mouseenter.call(context);
expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true);
});
it('registers mouseleave event if popover is showed', (done) => {
spyOn(togglePopover, 'call').and.returnValue(true);
spyOn($.fn, 'on').and.callFake((eventName) => {
expect(eventName).toEqual('mouseleave');
done();
});
mouseenter.call(context);
});
it('does not register mouseleave event if popover is not showed', () => {
spyOn(togglePopover, 'call').and.returnValue(false);
const spy = spyOn($.fn, 'on').and.callFake(() => {});
mouseenter.call(context);
expect(spy).not.toHaveBeenCalled();
});
});
});
import findAndFollowLink from '~/shortcuts_dashboard_navigation';
import * as urlUtility from '~/lib/utils/url_utility';
describe('findAndFollowLink', () => {
it('visits a link when the selector exists', () => {
const href = '/some/path';
const locationSpy = spyOn(urlUtility, 'visitUrl');
setFixtures(`<a class="my-shortcut" href="${href}">link</a>`);
findAndFollowLink('.my-shortcut');
expect(locationSpy).toHaveBeenCalledWith(href);
});
it('does not throw an exception when the selector does not exist', () => {
const locationSpy = spyOn(urlUtility, 'visitUrl');
// this should not throw an exception
findAndFollowLink('.this-selector-does-not-exist');
expect(locationSpy).not.toHaveBeenCalled();
});
});
...@@ -51,8 +51,7 @@ const createComponent = () => { ...@@ -51,8 +51,7 @@ const createComponent = () => {
const messages = { const messages = {
loadingMetrics: 'Loading deployment statistics', loadingMetrics: 'Loading deployment statistics',
hasMetrics: hasMetrics: 'Memory usage is unchanged at 0MB',
'<a href="/root/acets-review-apps/environments/15/metrics"> Memory </a> usage is <b> unchanged </b> at 0MB',
loadFailed: 'Failed to load deployment statistics', loadFailed: 'Failed to load deployment statistics',
metricsUnavailable: 'Deployment statistics are not available currently', metricsUnavailable: 'Deployment statistics are not available currently',
}; };
......
...@@ -14,8 +14,8 @@ describe('Skeleton loading container', () => { ...@@ -14,8 +14,8 @@ describe('Skeleton loading container', () => {
vm.$destroy(); vm.$destroy();
}); });
it('renders 6 skeleton lines by default', () => { it('renders 3 skeleton lines by default', () => {
expect(vm.$el.querySelector('.skeleton-line-6')).not.toBeNull(); expect(vm.$el.querySelector('.skeleton-line-3')).not.toBeNull();
}); });
it('renders in full mode by default', () => { it('renders in full mode by default', () => {
......
...@@ -3,6 +3,48 @@ require 'spec_helper' ...@@ -3,6 +3,48 @@ require 'spec_helper'
describe API::Helpers do describe API::Helpers do
subject { Class.new.include(described_class).new } subject { Class.new.include(described_class).new }
describe '#find_project' do
let(:project) { create(:project) }
shared_examples 'project finder' do
context 'when project exists' do
it 'returns requested project' do
expect(subject.find_project(existing_id)).to eq(project)
end
it 'returns nil' do
expect(subject.find_project(non_existing_id)).to be_nil
end
end
end
context 'when ID is used as an argument' do
let(:existing_id) { project.id }
let(:non_existing_id) { (Project.maximum(:id) || 0) + 1 }
it_behaves_like 'project finder'
end
context 'when PATH is used as an argument' do
let(:existing_id) { project.full_path }
let(:non_existing_id) { 'something/else' }
it_behaves_like 'project finder'
context 'with an invalid PATH' do
let(:non_existing_id) { 'undefined' } # path without slash
it_behaves_like 'project finder'
it 'does not hit the database' do
expect(Project).not_to receive(:find_by_full_path)
subject.find_project(non_existing_id)
end
end
end
end
describe '#find_namespace' do describe '#find_namespace' do
let(:namespace) { create(:namespace) } let(:namespace) { create(:namespace) }
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do
let!(:project) { create(:project, :repository) } let!(:project) { create(:project, :repository) }
let(:pipeline_status) { described_class.new(project) } let(:pipeline_status) { described_class.new(project) }
let(:cache_key) { "projects/#{project.id}/pipeline_status" } let(:cache_key) { described_class.cache_key_for_project(project) }
describe '.load_for_project' do describe '.load_for_project' do
it "loads the status" do it "loads the status" do
......
...@@ -14,6 +14,12 @@ describe Gitlab do ...@@ -14,6 +14,12 @@ describe Gitlab do
expect(described_class.com?).to eq true expect(described_class.com?).to eq true
end end
it 'is true when on other gitlab subdomain' do
stub_config_setting(url: 'https://example.gitlab.com')
expect(described_class.com?).to eq true
end
it 'is false when not on GitLab.com' do it 'is false when not on GitLab.com' do
stub_config_setting(url: 'http://example.com') stub_config_setting(url: 'http://example.com')
......
...@@ -5,7 +5,7 @@ describe InternalId do ...@@ -5,7 +5,7 @@ describe InternalId do
let(:usage) { :issues } let(:usage) { :issues }
let(:issue) { build(:issue, project: project) } let(:issue) { build(:issue, project: project) }
let(:scope) { { project: project } } let(:scope) { { project: project } }
let(:init) { ->(s) { s.project.issues.size } } let(:init) { ->(s) { s.project.issues.maximum(:iid) } }
context 'validations' do context 'validations' do
it { is_expected.to validate_presence_of(:usage) } it { is_expected.to validate_presence_of(:usage) }
...@@ -39,6 +39,29 @@ describe InternalId do ...@@ -39,6 +39,29 @@ describe InternalId do
end end
end end
context 'with an InternalId record present and existing issues with a higher internal id' do
# This can happen if the old NonatomicInternalId is still in use
before do
issues = Array.new(rand(1..10)).map { create(:issue, project: project) }
issue = issues.last
issue.iid = issues.map { |i| i.iid }.max + 1
issue.save
end
let(:maximum_iid) { project.issues.map { |i| i.iid }.max }
it 'updates last_value to the maximum internal id present' do
subject
expect(described_class.find_by(project: project, usage: described_class.usages[usage.to_s]).last_value).to eq(maximum_iid + 1)
end
it 'returns next internal id correctly' do
expect(subject).to eq(maximum_iid + 1)
end
end
context 'with concurrent inserts on table' do context 'with concurrent inserts on table' do
it 'looks up the record if it was created concurrently' do it 'looks up the record if it was created concurrently' do
args = { **scope, usage: described_class.usages[usage.to_s] } args = { **scope, usage: described_class.usages[usage.to_s] }
...@@ -81,7 +104,8 @@ describe InternalId do ...@@ -81,7 +104,8 @@ describe InternalId do
describe '#increment_and_save!' do describe '#increment_and_save!' do
let(:id) { create(:internal_id) } let(:id) { create(:internal_id) }
subject { id.increment_and_save! } let(:maximum_iid) { nil }
subject { id.increment_and_save!(maximum_iid) }
it 'returns incremented iid' do it 'returns incremented iid' do
value = id.last_value value = id.last_value
...@@ -102,5 +126,14 @@ describe InternalId do ...@@ -102,5 +126,14 @@ describe InternalId do
expect(subject).to eq(1) expect(subject).to eq(1)
end end
end end
context 'with maximum_iid given' do
let(:id) { create(:internal_id, last_value: 1) }
let(:maximum_iid) { id.last_value + 10 }
it 'returns maximum_iid instead' do
expect(subject).to eq(12)
end
end
end end
end end
...@@ -406,7 +406,7 @@ describe API::Runner do ...@@ -406,7 +406,7 @@ describe API::Runner do
expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' }) expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' })
expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
'alias' => nil, 'command' => nil }, 'alias' => nil, 'command' => nil },
{ 'name' => 'docker:dind', 'entrypoint' => '/bin/sh', { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
'alias' => 'docker', 'command' => 'sleep 30' }]) 'alias' => 'docker', 'command' => 'sleep 30' }])
expect(json_response['steps']).to eq(expected_steps) expect(json_response['steps']).to eq(expected_steps)
expect(json_response['artifacts']).to eq(expected_artifacts) expect(json_response['artifacts']).to eq(expected_artifacts)
......
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