diff options
162 files changed, 5646 insertions, 1351 deletions
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js index 2e3ad244375..1e5c733d151 100644 --- a/app/assets/javascripts/clusters/clusters_index.js +++ b/app/assets/javascripts/clusters/clusters_index.js @@ -1,20 +1,24 @@ -import Flash from '../flash'; -import { s__ } from '../locale'; -import setupToggleButtons from '../toggle_buttons'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; +import setupToggleButtons from '~/toggle_buttons'; +import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; + import ClustersService from './services/clusters_service'; export default () => { const clusterList = document.querySelector('.js-clusters-list'); + + gcpSignupOffer(); + // The empty state won't have a clusterList if (clusterList) { - setupToggleButtons( - document.querySelector('.js-clusters-list'), - (value, toggle) => - ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }) - .catch((err) => { - Flash(s__('ClusterIntegration|Something went wrong on our end.')); - throw err; - }), + setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) => + ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch( + err => { + createFlash(__('Something went wrong on our end.')); + throw err; + }, + ), ); } }; diff --git a/app/assets/javascripts/clusters/components/gcp_signup_offer.js b/app/assets/javascripts/clusters/components/gcp_signup_offer.js new file mode 100644 index 00000000000..8bc20a1c09f --- /dev/null +++ b/app/assets/javascripts/clusters/components/gcp_signup_offer.js @@ -0,0 +1,27 @@ +import $ from 'jquery'; +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import Flash from '~/flash'; + +export default function gcpSignupOffer() { + const alertEl = document.querySelector('.gcp-signup-offer'); + if (!alertEl) { + return; + } + + const closeButtonEl = alertEl.getElementsByClassName('close')[0]; + const { dismissEndpoint, featureId } = closeButtonEl.dataset; + + closeButtonEl.addEventListener('click', () => { + axios + .post(dismissEndpoint, { + feature_name: featureId, + }) + .then(() => { + $(alertEl).alert('close'); + }) + .catch(() => { + Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.')); + }); + }); +} diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue index b839b9f286f..67dda0e29cb 100644 --- a/app/assets/javascripts/deploy_keys/components/action_btn.vue +++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue @@ -1,55 +1,50 @@ <script> - import eventHub from '../eventhub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import loadingIcon from '~/vue_shared/components/loading_icon.vue'; +import eventHub from '../eventhub'; - export default { - components: { - loadingIcon, +export default { + components: { + loadingIcon, + }, + props: { + deployKey: { + type: Object, + required: true, }, - props: { - deployKey: { - type: Object, - required: true, - }, - type: { - type: String, - required: true, - }, - btnCssClass: { - type: String, - required: false, - default: 'btn-default', - }, + type: { + type: String, + required: true, }, - data() { - return { - isLoading: false, - }; + btnCssClass: { + type: String, + required: false, + default: 'btn-default', }, - computed: { - text() { - return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`; - }, - }, - methods: { - doAction() { - this.isLoading = true; + }, + data() { + return { + isLoading: false, + }; + }, + methods: { + doAction() { + this.isLoading = true; - eventHub.$emit(`${this.type}.key`, this.deployKey, () => { - this.isLoading = false; - }); - }, + eventHub.$emit(`${this.type}.key`, this.deployKey, () => { + this.isLoading = false; + }); }, - }; + }, +}; </script> <template> <button - class="btn btn-sm prepend-left-10" + class="btn" :class="[{ disabled: isLoading }, btnCssClass]" :disabled="isLoading" @click="doAction"> - {{ text }} + <slot></slot> <loading-icon v-if="isLoading" :inline="true" diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index 5a782237b7d..c41fe55db63 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -1,80 +1,115 @@ <script> - import Flash from '../../flash'; - import eventHub from '../eventhub'; - import DeployKeysService from '../service'; - import DeployKeysStore from '../store'; - import keysPanel from './keys_panel.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import { s__ } from '~/locale'; +import Flash from '~/flash'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; +import eventHub from '../eventhub'; +import DeployKeysService from '../service'; +import DeployKeysStore from '../store'; +import KeysPanel from './keys_panel.vue'; - export default { - components: { - keysPanel, - loadingIcon, +export default { + components: { + KeysPanel, + LoadingIcon, + NavigationTabs, + }, + props: { + endpoint: { + type: String, + required: true, }, - props: { - endpoint: { - type: String, - required: true, - }, + projectId: { + type: String, + required: true, }, - data() { - return { - isLoading: false, - store: new DeployKeysStore(), - }; + }, + data() { + return { + currentTab: 'enabled_keys', + isLoading: false, + store: new DeployKeysStore(), + }; + }, + scopes: { + enabled_keys: s__('DeployKeys|Enabled deploy keys'), + available_project_keys: s__('DeployKeys|Privately accessible deploy keys'), + public_keys: s__('DeployKeys|Publicly accessible deploy keys'), + }, + computed: { + tabs() { + return Object.keys(this.$options.scopes).map(scope => { + const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null; + + return { + name: this.$options.scopes[scope], + scope, + isActive: scope === this.currentTab, + count, + }; + }); + }, + hasKeys() { + return Object.keys(this.keys).length; }, - computed: { - hasKeys() { - return Object.keys(this.keys).length; - }, - keys() { - return this.store.keys; - }, + keys() { + return this.store.keys; }, - created() { - this.service = new DeployKeysService(this.endpoint); + }, + created() { + this.service = new DeployKeysService(this.endpoint); - eventHub.$on('enable.key', this.enableKey); - eventHub.$on('remove.key', this.disableKey); - eventHub.$on('disable.key', this.disableKey); + eventHub.$on('enable.key', this.enableKey); + eventHub.$on('remove.key', this.disableKey); + eventHub.$on('disable.key', this.disableKey); + }, + mounted() { + this.fetchKeys(); + }, + beforeDestroy() { + eventHub.$off('enable.key', this.enableKey); + eventHub.$off('remove.key', this.disableKey); + eventHub.$off('disable.key', this.disableKey); + }, + methods: { + onChangeTab(tab) { + this.currentTab = tab; }, - mounted() { - this.fetchKeys(); + fetchKeys() { + this.isLoading = true; + + return this.service + .getKeys() + .then(data => { + this.isLoading = false; + this.store.keys = data; + }) + .catch(() => { + this.isLoading = false; + this.store.keys = {}; + return new Flash(s__('DeployKeys|Error getting deploy keys')); + }); }, - beforeDestroy() { - eventHub.$off('enable.key', this.enableKey); - eventHub.$off('remove.key', this.disableKey); - eventHub.$off('disable.key', this.disableKey); + enableKey(deployKey) { + this.service + .enableKey(deployKey.id) + .then(this.fetchKeys) + .catch(() => new Flash(s__('DeployKeys|Error enabling deploy key'))); }, - methods: { - fetchKeys() { - this.isLoading = true; - - this.service.getKeys() - .then((data) => { - this.isLoading = false; - this.store.keys = data; - }) - .catch(() => new Flash('Error getting deploy keys')); - }, - enableKey(deployKey) { - this.service.enableKey(deployKey.id) - .then(() => this.fetchKeys()) - .catch(() => new Flash('Error enabling deploy key')); - }, - disableKey(deployKey, callback) { - // eslint-disable-next-line no-alert - if (confirm('You are going to remove this deploy key. Are you sure?')) { - this.service.disableKey(deployKey.id) - .then(() => this.fetchKeys()) - .then(callback) - .catch(() => new Flash('Error removing deploy key')); - } else { - callback(); - } - }, + disableKey(deployKey, callback) { + // eslint-disable-next-line no-alert + if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) { + this.service + .disableKey(deployKey.id) + .then(this.fetchKeys) + .then(callback) + .catch(() => new Flash(s__('DeployKeys|Error removing deploy key'))); + } else { + callback(); + } }, - }; + }, +}; </script> <template> @@ -82,29 +117,38 @@ <loading-icon v-if="isLoading && !hasKeys" size="2" - label="Loading deploy keys" + :label="s__('DeployKeys|Loading deploy keys')" /> - <div v-else-if="hasKeys"> + <template v-else-if="hasKeys"> + <div class="top-area scrolling-tabs-container inner-page-scroll-tabs"> + <div class="fade-left"> + <i + class="fa fa-angle-left" + aria-hidden="true" + > + </i> + </div> + <div class="fade-right"> + <i + class="fa fa-angle-right" + aria-hidden="true" + > + </i> + </div> + + <navigation-tabs + :tabs="tabs" + @onChangeTab="onChangeTab" + scope="deployKeys" + /> + </div> <keys-panel - title="Enabled deploy keys for this project" class="qa-project-deploy-keys" - :keys="keys.enabled_keys" - :store="store" - :endpoint="endpoint" - /> - <keys-panel - title="Deploy keys from projects you have access to" - :keys="keys.available_project_keys" - :store="store" - :endpoint="endpoint" - /> - <keys-panel - v-if="keys.public_keys.length" - title="Public deploy keys available to any project" - :keys="keys.public_keys" + :project-id="projectId" + :keys="keys[currentTab]" :store="store" :endpoint="endpoint" /> - </div> + </template> </div> </template> diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index c6091efd62f..6c2af7fa768 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -1,111 +1,235 @@ <script> - import actionBtn from './action_btn.vue'; - import { getTimeago } from '../../lib/utils/datetime_utility'; - import tooltip from '../../vue_shared/directives/tooltip'; +import _ from 'underscore'; +import { s__, sprintf } from '~/locale'; +import icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; - export default { - components: { - actionBtn, - }, - directives: { - tooltip, - }, - props: { - deployKey: { - type: Object, - required: true, - }, - store: { - type: Object, - required: true, - }, - endpoint: { - type: String, - required: true, - }, - }, - computed: { - timeagoDate() { - return getTimeago().format(this.deployKey.created_at); - }, - editDeployKeyPath() { - return `${this.endpoint}/${this.deployKey.id}/edit`; - }, - }, - methods: { - isEnabled(id) { - return this.store.findEnabledKey(id) !== undefined; - }, - tooltipTitle(project) { - return project.can_push ? 'Write access allowed' : 'Read access only'; - }, - }, - }; +import actionBtn from './action_btn.vue'; + +export default { + components: { + actionBtn, + icon, + }, + directives: { + tooltip, + }, + mixins: [timeagoMixin], + props: { + deployKey: { + type: Object, + required: true, + }, + store: { + type: Object, + required: true, + }, + endpoint: { + type: String, + required: true, + }, + projectId: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + projectsExpanded: false, + }; + }, + computed: { + editDeployKeyPath() { + return `${this.endpoint}/${this.deployKey.id}/edit`; + }, + projects() { + const projects = [...this.deployKey.deploy_keys_projects]; + + if (this.projectId !== null) { + const indexOfCurrentProject = _.findIndex( + projects, + project => + project && + project.project && + project.project.id && + project.project.id.toString() === this.projectId, + ); + + if (indexOfCurrentProject > -1) { + const currentProject = projects.splice(indexOfCurrentProject, 1); + currentProject[0].project.full_name = s__('DeployKeys|Current project'); + return currentProject.concat(projects); + } + } + return projects; + }, + firstProject() { + return _.head(this.projects); + }, + restProjects() { + return _.tail(this.projects); + }, + restProjectsTooltip() { + return sprintf(s__('DeployKeys|Expand %{count} other projects'), { + count: this.restProjects.length, + }); + }, + restProjectsLabel() { + return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length }); + }, + isEnabled() { + return this.store.isEnabled(this.deployKey.id); + }, + isRemovable() { + return ( + this.store.isEnabled(this.deployKey.id) && + this.deployKey.destroyed_when_orphaned && + this.deployKey.almost_orphaned + ); + }, + isExpandable() { + return !this.projectsExpanded && this.restProjects.length > 1; + }, + isExpanded() { + return this.projectsExpanded || this.restProjects.length === 1; + }, + }, + methods: { + projectTooltipTitle(project) { + return project.can_push + ? s__('DeployKeys|Write access allowed') + : s__('DeployKeys|Read access only'); + }, + toggleExpanded() { + this.projectsExpanded = !this.projectsExpanded; + }, + }, +}; </script> <template> - <div> - <div class="pull-left append-right-10 hidden-xs"> - <i - aria-hidden="true" - class="fa fa-key key-icon" - > - </i> + <div class="gl-responsive-table-row deploy-key"> + <div class="table-section section-40"> + <div + role="rowheader" + class="table-mobile-header"> + {{ s__('DeployKeys|Deploy key') }} + </div> + <div class="table-mobile-content"> + <strong class="title qa-key-title"> + {{ deployKey.title }} + </strong> + <div class="fingerprint qa-key-fingerprint"> + {{ deployKey.fingerprint }} + </div> + </div> </div> - <div class="deploy-key-content key-list-item-info"> - <strong class="title qa-key-title"> - {{ deployKey.title }} - </strong> - <div class="description qa-key-fingerprint"> - {{ deployKey.fingerprint }} + <div class="table-section section-30 section-wrap"> + <div + role="rowheader" + class="table-mobile-header"> + {{ s__('DeployKeys|Project usage') }} + </div> + <div class="table-mobile-content deploy-project-list"> + <template v-if="projects.length > 0"> + <a + class="label deploy-project-label" + :title="projectTooltipTitle(firstProject)" + v-tooltip + > + <span> + {{ firstProject.project.full_name }} + </span> + <icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/> + </a> + <a + v-if="isExpandable" + class="label deploy-project-label" + @click="toggleExpanded" + :title="restProjectsTooltip" + v-tooltip + > + <span>{{ restProjectsLabel }}</span> + </a> + <a + v-else-if="isExpanded" + v-for="deployKeysProject in restProjects" + :key="deployKeysProject.project.full_path" + class="label deploy-project-label" + :href="deployKeysProject.project.full_path" + :title="projectTooltipTitle(deployKeysProject)" + v-tooltip + > + <span> + {{ deployKeysProject.project.full_name }} + </span> + <icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'"/> + </a> + </template> + <span + v-else + class="text-secondary">{{ __('None') }}</span> </div> </div> - <div class="deploy-key-content prepend-left-default deploy-key-projects"> - <a - v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects" - :key="i" - class="label deploy-project-label" - :href="deployKeysProject.project.full_path" - :title="tooltipTitle(deployKeysProject)" - v-tooltip - > - {{ deployKeysProject.project.full_name }} - <i - v-if="!deployKeysProject.can_push" - aria-hidden="true" - class="fa fa-lock" - > - </i> - </a> + <div class="table-section section-15 text-right"> + <div + role="rowheader" + class="table-mobile-header"> + {{ __('Created') }} + </div> + <div class="table-mobile-content text-secondary key-created-at"> + <span + :title="tooltipTitle(deployKey.created_at)" + v-tooltip> + <icon name="calendar"/> + <span>{{ timeFormated(deployKey.created_at) }}</span> + </span> + </div> </div> - <div class="deploy-key-content"> - <span class="key-created-at"> - created {{ timeagoDate }} - </span> - <a - v-if="deployKey.can_edit" - class="btn btn-sm" - :href="editDeployKeyPath" - > - Edit - </a> - <action-btn - v-if="!isEnabled(deployKey.id)" - :deploy-key="deployKey" - type="enable" - /> - <action-btn - v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned" - :deploy-key="deployKey" - btn-css-class="btn-warning" - type="remove" - /> - <action-btn - v-else - :deploy-key="deployKey" - btn-css-class="btn-warning" - type="disable" - /> + <div class="table-section section-15 table-button-footer deploy-key-actions"> + <div class="btn-group table-action-buttons"> + <action-btn + v-if="!isEnabled" + :deploy-key="deployKey" + type="enable" + > + {{ __('Enable') }} + </action-btn> + <a + v-if="deployKey.can_edit" + class="btn btn-default text-secondary" + :href="editDeployKeyPath" + :title="__('Edit')" + data-container="body" + v-tooltip + > + <icon name="pencil"/> + </a> + <action-btn + v-if="isRemovable" + :deploy-key="deployKey" + btn-css-class="btn-danger" + type="remove" + :title="__('Remove')" + data-container="body" + v-tooltip + > + <icon name="remove"/> + </action-btn> + <action-btn + v-else-if="isEnabled" + :deploy-key="deployKey" + btn-css-class="btn-warning" + type="disable" + :title="__('Disable')" + data-container="body" + v-tooltip + > + <icon name="cancel"/> + </action-btn> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue index 822b0323156..3b146c7389a 100644 --- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue +++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue @@ -1,62 +1,68 @@ <script> - import key from './key.vue'; +import deployKey from './key.vue'; - export default { - components: { - key, +export default { + components: { + deployKey, + }, + props: { + keys: { + type: Array, + required: true, }, - props: { - title: { - type: String, - required: true, - }, - keys: { - type: Array, - required: true, - }, - showHelpBox: { - type: Boolean, - required: false, - default: true, - }, - store: { - type: Object, - required: true, - }, - endpoint: { - type: String, - required: true, - }, + store: { + type: Object, + required: true, }, - }; + endpoint: { + type: String, + required: true, + }, + projectId: { + type: String, + required: false, + default: null, + }, + }, +}; </script> <template> - <div class="deploy-keys-panel"> - <h5> - {{ title }} - ({{ keys.length }}) - </h5> - <ul - class="well-list" - v-if="keys.length" - > - <li + <div class="deploy-keys-panel table-holder"> + <template v-if="keys.length > 0"> + <div + role="row" + class="gl-responsive-table-row table-row-header"> + <div + role="rowheader" + class="table-section section-40"> + {{ s__('DeployKeys|Deploy key') }} + </div> + <div + role="rowheader" + class="table-section section-30"> + {{ s__('DeployKeys|Project usage') }} + </div> + <div + role="rowheader" + class="table-section section-15 text-right"> + {{ __('Created') }} + </div> + </div> + <deploy-key v-for="deployKey in keys" :key="deployKey.id" - > - <key - :deploy-key="deployKey" - :store="store" - :endpoint="endpoint" - /> - </li> - </ul> + :deploy-key="deployKey" + :store="store" + :endpoint="endpoint" + :project-id="projectId" + /> + </template> <div class="settings-message text-center" - v-else-if="showHelpBox" + v-else > - No deploy keys found. Create one with the form above. + {{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }} </div> </div> </template> diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js index b727261648c..6e439be42ae 100644 --- a/app/assets/javascripts/deploy_keys/index.js +++ b/app/assets/javascripts/deploy_keys/index.js @@ -1,21 +1,24 @@ import Vue from 'vue'; import deployKeysApp from './components/app.vue'; -export default () => new Vue({ - el: document.getElementById('js-deploy-keys'), - components: { - deployKeysApp, - }, - data() { - return { - endpoint: this.$options.el.dataset.endpoint, - }; - }, - render(createElement) { - return createElement('deploy-keys-app', { - props: { - endpoint: this.endpoint, - }, - }); - }, -}); +export default () => + new Vue({ + el: document.getElementById('js-deploy-keys'), + components: { + deployKeysApp, + }, + data() { + return { + endpoint: this.$options.el.dataset.endpoint, + projectId: this.$options.el.dataset.projectId, + }; + }, + render(createElement) { + return createElement('deploy-keys-app', { + props: { + endpoint: this.endpoint, + projectId: this.projectId, + }, + }); + }, + }); diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js index fe6dbaa9498..194e95e4fca 100644 --- a/app/assets/javascripts/deploy_keys/service/index.js +++ b/app/assets/javascripts/deploy_keys/service/index.js @@ -7,21 +7,24 @@ export default class DeployKeysService { constructor(endpoint) { this.endpoint = endpoint; - this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, { - enable: { - method: 'PUT', - url: `${this.endpoint}{/id}/enable`, + this.resource = Vue.resource( + `${this.endpoint}{/id}`, + {}, + { + enable: { + method: 'PUT', + url: `${this.endpoint}{/id}/enable`, + }, + disable: { + method: 'PUT', + url: `${this.endpoint}{/id}/disable`, + }, }, - disable: { - method: 'PUT', - url: `${this.endpoint}{/id}/disable`, - }, - }); + ); } getKeys() { - return this.resource.get() - .then(response => response.json()); + return this.resource.get().then(response => response.json()); } enableKey(id) { diff --git a/app/assets/javascripts/deploy_keys/store/index.js b/app/assets/javascripts/deploy_keys/store/index.js index 6210361af26..a350bc99a70 100644 --- a/app/assets/javascripts/deploy_keys/store/index.js +++ b/app/assets/javascripts/deploy_keys/store/index.js @@ -3,7 +3,7 @@ export default class DeployKeysStore { this.keys = {}; } - findEnabledKey(id) { - return this.keys.enabled_keys.find(key => key.id === id); + isEnabled(id) { + return this.keys.enabled_keys.some(key => key.id === id); } } diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 77b35078e56..d479b705e35 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -65,61 +65,63 @@ export default { </script> <template> - <div - class="ide-view" - > - <find-file - v-show="fileFindVisible" - /> - <ide-sidebar /> + <article class="ide"> <div - class="multi-file-edit-pane" + class="ide-view" > - <template - v-if="activeFile" - > - <repo-tabs - :active-file="activeFile" - :files="openFiles" - :viewer="viewer" - :has-changes="hasChanges" - :merge-request-id="currentMergeRequestId" - /> - <repo-editor - class="multi-file-edit-pane-content" - :file="activeFile" - /> - <ide-status-bar - :file="activeFile" - /> - </template> - <template - v-else + <find-file + v-show="fileFindVisible" + /> + <ide-sidebar /> + <div + class="multi-file-edit-pane" > - <div - v-once - class="ide-empty-state" + <template + v-if="activeFile" + > + <repo-tabs + :active-file="activeFile" + :files="openFiles" + :viewer="viewer" + :has-changes="hasChanges" + :merge-request-id="currentMergeRequestId" + /> + <repo-editor + class="multi-file-edit-pane-content" + :file="activeFile" + /> + </template> + <template + v-else > - <div class="row js-empty-state"> - <div class="col-xs-12"> - <div class="svg-content svg-250"> - <img :src="emptyStateSvgPath" /> + <div + v-once + class="ide-empty-state" + > + <div class="row js-empty-state"> + <div class="col-xs-12"> + <div class="svg-content svg-250"> + <img :src="emptyStateSvgPath" /> + </div> </div> - </div> - <div class="col-xs-12"> - <div class="text-content text-center"> - <h4> - Welcome to the GitLab IDE - </h4> - <p> - You can select a file in the left sidebar to begin - editing and use the right sidebar to commit your changes. - </p> + <div class="col-xs-12"> + <div class="text-content text-center"> + <h4> + Welcome to the GitLab IDE + </h4> + <p> + You can select a file in the left sidebar to begin + editing and use the right sidebar to commit your changes. + </p> + </div> </div> </div> </div> - </div> - </template> + </template> + </div> </div> - </div> + <ide-status-bar + :file="activeFile" + /> + </article> </template> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index c13eeeace3f..70c6d53c3ab 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,11 +1,14 @@ <script> +import { mapGetters } from 'vuex'; import icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import timeAgoMixin from '~/vue_shared/mixins/timeago'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; export default { components: { icon, + userAvatarImage, }, directives: { tooltip, @@ -14,40 +17,93 @@ export default { props: { file: { type: Object, - required: true, + required: false, + default: null, + }, + }, + data() { + return { + lastCommitFormatedAge: null, + }; + }, + computed: { + ...mapGetters(['currentProject', 'lastCommit']), + }, + mounted() { + this.startTimer(); + }, + beforeDestroy() { + if (this.intervalId) { + clearInterval(this.intervalId); + } + }, + methods: { + startTimer() { + this.intervalId = setInterval(() => { + this.commitAgeUpdate(); + }, 1000); + }, + commitAgeUpdate() { + if (this.lastCommit) { + this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date); + } + }, + getCommitPath(shortSha) { + return `${this.currentProject.web_url}/commit/${shortSha}`; }, }, }; </script> <template> - <div class="ide-status-bar"> - <div> - <div v-if="file.lastCommit && file.lastCommit.id"> - Last commit: - <a - v-tooltip - :title="file.lastCommit.message" - :href="file.lastCommit.url" - > - {{ timeFormated(file.lastCommit.updatedAt) }} by - {{ file.lastCommit.author }} - </a> - </div> + <footer class="ide-status-bar"> + <div + class="ide-status-branch" + v-if="lastCommit && lastCommitFormatedAge" + > + <icon + name="commit" + /> + <a + v-tooltip + class="commit-sha" + :title="lastCommit.message" + :href="getCommitPath(lastCommit.short_id)" + >{{ lastCommit.short_id }}</a> + by + {{ lastCommit.author_name }} + <time + v-tooltip + data-placement="top" + data-container="body" + :datetime="lastCommit.committed_date" + :title="tooltipTitle(lastCommit.committed_date)" + > + {{ lastCommitFormatedAge }} + </time> </div> - <div class="text-right"> + <div + v-if="file" + class="ide-status-file" + > {{ file.name }} </div> - <div class="text-right"> + <div + v-if="file" + class="ide-status-file" + > {{ file.eol }} </div> <div - class="text-right" - v-if="!file.binary"> + class="ide-status-file" + v-if="file && !file.binary"> {{ file.editorRow }}:{{ file.editorColumn }} </div> - <div class="text-right"> + <div + v-if="file" + class="ide-status-file" + > {{ file.fileLanguage }} </div> - </div> + </footer> </template> diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index e5c47d09886..c5835cd3b06 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -4,7 +4,9 @@ import ide from './components/ide.vue'; import store from './stores'; import router from './ide_router'; -function initIde(el) { +Vue.use(Translate); + +export function initIde(el) { if (!el) return null; return new Vue({ @@ -27,8 +29,12 @@ function initIde(el) { }); } -const ideElement = document.getElementById('ide'); - -Vue.use(Translate); - -initIde(ideElement); +// tell webpack to load assets from origin so that web workers don't break +export function resetServiceWorkersPublicPath() { + // __webpack_public_path__ is a global variable that can be used to adjust + // the webpack publicPath setting at runtime. + // see: https://webpack.js.org/guides/public-path/ + const relativeRootPath = (gon && gon.relative_url_root) || ''; + const webpackAssetPath = `${relativeRootPath}/assets/webpack/`; + __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase +} diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 971a15a4036..eff9bc03651 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -72,3 +72,26 @@ export const getBranchData = ( resolve(state.projects[`${projectId}`].branches[branchId]); } }); + +export const refreshLastCommitData = ( + { commit, state, dispatch }, + { projectId, branchId } = {}, +) => service + .getBranchData(projectId, branchId) + .then(({ data }) => { + commit(types.SET_BRANCH_COMMIT, { + projectId, + branchId, + commit: data.commit, + }); + }) + .catch(() => { + flash( + 'Error loading last commit.', + 'alert', + document, + null, + false, + true, + ); + }); diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 75286ed0cd2..b239a605371 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -81,5 +81,11 @@ export const getUnstagedFilesCountForPath = state => path => export const getStagedFilesCountForPath = state => path => getChangesCountForFiles(state.stagedFiles, path); +export const lastCommit = (state, getters) => { + const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId]; + + return branch ? branch.commit : null; +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index ab00d12089d..b85246b2502 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -210,7 +210,11 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ); } }) - .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)); + .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)) + .then(() => dispatch('refreshLastCommitData', { + projectId: rootState.currentProjectId, + branchId: rootState.currentBranchId, + }, { root: true })); }) .catch(err => { let errMsg = __('Error committing changes. Please try again.'); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 08e22e5ad7e..a3fb3232f1d 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -20,6 +20,7 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS'; // Branch Mutation Types export const SET_BRANCH = 'SET_BRANCH'; +export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT'; export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js index 2972ba5e38e..e09f88878f4 100644 --- a/app/assets/javascripts/ide/stores/mutations/branch.js +++ b/app/assets/javascripts/ide/stores/mutations/branch.js @@ -23,4 +23,9 @@ export default { workingReference: reference, }); }, + [types.SET_BRANCH_COMMIT](state, { projectId, branchId, commit }) { + Object.assign(state.projects[projectId].branches[branchId], { + commit, + }); + }, }; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 2c80baba10b..247aeb481c6 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,22 +1,19 @@ -/* eslint-disable import/first */ /* global $ */ import jQuery from 'jquery'; import Cookies from 'js-cookie'; import svg4everybody from 'svg4everybody'; -// expose common libraries as globals (TODO: remove these) -window.jQuery = jQuery; -window.$ = jQuery; +// bootstrap webpack, common libs, polyfills, and behaviors +import './webpack'; +import './commons'; +import './behaviors'; // lib/utils import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils'; import { localTimeAgo } from './lib/utils/datetime_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; -// behaviors -import './behaviors/'; - // everything else import loadAwardsHandler from './awards_handler'; import bp from './breakpoints'; @@ -31,9 +28,12 @@ import initLogoAnimation from './logo'; import './milestone_select'; import './projects_dropdown'; import initBreadcrumbs from './breadcrumb'; - import initDispatcher from './dispatcher'; +// expose jQuery as global (TODO: remove these) +window.jQuery = jQuery; +window.$ = jQuery; + // inject test utilities if necessary if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) { $.fx.off = true; @@ -52,10 +52,14 @@ document.addEventListener('beforeunload', () => { }); window.addEventListener('hashchange', handleLocationHash); -window.addEventListener('load', function onLoad() { - window.removeEventListener('load', onLoad, false); - handleLocationHash(); -}, false); +window.addEventListener( + 'load', + function onLoad() { + window.removeEventListener('load', onLoad, false); + handleLocationHash(); + }, + false, +); gl.lazyLoader = new LazyLoader({ scrollContainer: window, @@ -89,9 +93,7 @@ document.addEventListener('DOMContentLoaded', () => { if (bootstrapBreakpoint === 'xs') { const $rightSidebar = $('aside.right-sidebar, .layout-page'); - $rightSidebar - .removeClass('right-sidebar-expanded') - .addClass('right-sidebar-collapsed'); + $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); } // prevent default action for disabled buttons @@ -108,7 +110,8 @@ document.addEventListener('DOMContentLoaded', () => { addSelectOnFocusBehaviour('.js-select-on-focus'); $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { - $(this).tooltip('destroy') + $(this) + .tooltip('destroy') .closest('li') .fadeOut(); }); @@ -118,7 +121,9 @@ document.addEventListener('DOMContentLoaded', () => { }); $('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() { - $(this).closest('tr').fadeOut(); + $(this) + .closest('tr') + .fadeOut(); }); // Initialize select2 selects @@ -155,7 +160,9 @@ document.addEventListener('DOMContentLoaded', () => { // Form submitter $('.trigger-submit').on('change', function triggerSubmitCallback() { - $(this).parents('form').submit(); + $(this) + .parents('form') + .submit(); }); localTimeAgo($('abbr.timeago, .js-timeago'), true); @@ -204,9 +211,15 @@ document.addEventListener('DOMContentLoaded', () => { $this.toggleClass('active'); if ($this.hasClass('active')) { - notesHolders.show().find('.hide, .content').show(); + notesHolders + .show() + .find('.hide, .content') + .show(); } else { - notesHolders.hide().find('.content').hide(); + notesHolders + .hide() + .find('.content') + .hide(); } $(document).trigger('toggle.comments'); @@ -247,9 +260,11 @@ document.addEventListener('DOMContentLoaded', () => { const flashContainer = document.querySelector('.flash-container'); if (flashContainer && flashContainer.children.length) { - flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => { - removeFlashClickListener(flashEl); - }); + flashContainer + .querySelectorAll('.flash-alert, .flash-notice, .flash-success') + .forEach(flashEl => { + removeFlashClickListener(flashEl); + }); } initDispatcher(); diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js index 04a0d8117cc..d3b2656743d 100644 --- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -1,6 +1,10 @@ +import initSettingsPanels from '~/settings_panels'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; document.addEventListener('DOMContentLoaded', () => { + // Initialize expandable settings panels + initSettingsPanels(); + const variableListEl = document.querySelector('.js-ci-variable-list-section'); // eslint-disable-next-line no-new new AjaxVariableList({ diff --git a/app/assets/javascripts/pages/ide/index.js b/app/assets/javascripts/pages/ide/index.js new file mode 100644 index 00000000000..efadf6967aa --- /dev/null +++ b/app/assets/javascripts/pages/ide/index.js @@ -0,0 +1,9 @@ +import { initIde, resetServiceWorkersPublicPath } from '~/ide/index'; + +document.addEventListener('DOMContentLoaded', () => { + const ideElement = document.getElementById('ide'); + if (ideElement) { + resetServiceWorkersPublicPath(); + initIde(ideElement); + } +}); diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js new file mode 100644 index 00000000000..0c2d7d7c96a --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js @@ -0,0 +1,3 @@ +import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; + +gcpSignupOffer(); diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js new file mode 100644 index 00000000000..0c2d7d7c96a --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/new/index.js @@ -0,0 +1,3 @@ +import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; + +gcpSignupOffer(); diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index e31e067033f..99c71d6524a 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -85,6 +85,7 @@ export default class Shortcuts { if ($modal.length) { $modal.modal('toggle'); + return null; } return axios.get(gon.shortcuts_path, { diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 1a0df49bc29..c42c4a1fbe7 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -65,6 +65,9 @@ export default { spriteHref() { return `${gon.sprite_icons}#${this.name}`; }, + iconTestClass() { + return `ic-${this.name}`; + }, iconSizeClass() { return this.size ? `s${this.size}` : ''; }, @@ -74,7 +77,7 @@ export default { <template> <svg - :class="[iconSizeClass, cssClasses]" + :class="[iconSizeClass, iconTestClass, cssClasses]" :width="width" :height="height" :x="x" diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index b33a0101dbf..92d187e24bf 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -1,53 +1,53 @@ <script> - import $ from 'jquery'; +import $ from 'jquery'; - /** - * Given an array of tabs, renders non linked bootstrap tabs. - * When a tab is clicked it will trigger an event and provide the clicked scope. - * - * This component is used in apps that handle the API call. - * If you only need to change the URL this component should not be used. - * - * @example - * <navigation-tabs - * :tabs="[ - * { - * name: String, - * scope: String, - * count: Number || Undefined, - * isActive: Boolean, - * }, - * ]" - * @onChangeTab="onChangeTab" - * /> - */ - export default { - name: 'NavigationTabs', - props: { - tabs: { - type: Array, - required: true, - }, - scope: { - type: String, - required: false, - default: '', - }, +/** + * Given an array of tabs, renders non linked bootstrap tabs. + * When a tab is clicked it will trigger an event and provide the clicked scope. + * + * This component is used in apps that handle the API call. + * If you only need to change the URL this component should not be used. + * + * @example + * <navigation-tabs + * :tabs="[ + * { + * name: String, + * scope: String, + * count: Number || Undefined || Null, + * isActive: Boolean, + * }, + * ]" + * @onChangeTab="onChangeTab" + * /> + */ +export default { + name: 'NavigationTabs', + props: { + tabs: { + type: Array, + required: true, }, - mounted() { - $(document).trigger('init.scrolling-tabs'); + scope: { + type: String, + required: false, + default: '', + }, + }, + mounted() { + $(document).trigger('init.scrolling-tabs'); + }, + methods: { + shouldRenderBadge(count) { + // 0 is valid in a badge, but evaluates to false, we need to check for undefined or null + return !(count === undefined || count === null); }, - methods: { - shouldRenderBadge(count) { - // 0 is valid in a badge, but evaluates to false, we need to check for undefined - return count !== undefined; - }, - onTabClick(tab) { - this.$emit('onChangeTab', tab.scope); - }, + onTabClick(tab) { + this.$emit('onChangeTab', tab.scope); }, - }; + }, +}; </script> <template> <ul class="nav-links scrolling-tabs separator"> diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 8d43bb32175..b5505538541 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -230,6 +230,7 @@ $row-hover: $blue-50; $row-hover-border: $blue-200; $progress-color: #c0392b; $header-height: 40px; +$ide-statusbar-height: 27px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; $limited-layout-width-sm: 790px; @@ -264,6 +265,7 @@ $performance-bar-height: 35px; $flash-height: 52px; $context-header-height: 60px; $breadcrumb-min-height: 48px; +$gcp-signup-offer-icon-max-width: 125px; /* * Common component specific colors @@ -333,11 +335,10 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); /* * Fonts */ -$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', - 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; -$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif, - 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; +$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', + 'Courier New', 'andale mono', 'lucida console', monospace; +$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, + 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; /* * Dropdowns @@ -465,11 +466,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5); */ $issue-boards-filter-height: 68px; $issue-boards-breadcrumbs-height-xs: 63px; -$issue-board-list-difference-xs: $header-height + - $issue-boards-breadcrumbs-height-xs; +$issue-board-list-difference-xs: $header-height + $issue-boards-breadcrumbs-height-xs; $issue-board-list-difference-sm: $header-height + $breadcrumb-min-height; -$issue-board-list-difference-md: $issue-board-list-difference-sm + - $issue-boards-filter-height; +$issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-height; /* * Avatar diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 7b8ee026357..3fd13078131 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -26,3 +26,51 @@ margin-right: 0; } } + +.gcp-signup-offer { + background-color: $blue-50; + border: 1px solid $blue-300; + border-radius: $border-radius-default; + + // TODO: To be superceded by cssLab + &.alert { + padding: 24px 16px; + + &-dismissable { + padding-right: 32px; + + .close { + top: -8px; + right: -16px; + color: $blue-500; + opacity: 1; + } + } + } + + .gcp-logo { + margin-bottom: $gl-padding; + text-align: center; + } + + img { + max-width: $gcp-signup-offer-icon-max-width; + } + + a:not(.btn) { + color: $gl-link-color; + font-weight: normal; + text-decoration: none; + } + + @media (min-width: $screen-sm-min) { + > div { + display: flex; + align-items: center; + } + + .gcp-logo { + margin: 0; + } + } +} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 1aca3c5cf1a..944996159d7 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -2,7 +2,6 @@ background: none; border: 0; padding: 0; - margin-top: 10px; word-break: normal; white-space: pre-wrap; } @@ -21,10 +20,6 @@ margin: 0; color: $gl-text-color; } - - .commit-description { - margin-top: 15px; - } } .commit-hash-full { @@ -178,7 +173,7 @@ .commit-detail { display: flex; justify-content: space-between; - align-items: center; + align-items: start; flex-grow: 1; } @@ -268,20 +263,16 @@ .commit-row-description { font-size: 14px; - padding: 10px 15px; - margin: 10px 0; - background: $gray-light; + padding: 0 0 0 $gl-padding-8; + border: 0; display: none; white-space: pre-wrap; word-break: normal; - - pre { - border: 0; - background: inherit; - padding: 0; - margin: 0; - white-space: pre-wrap; - } + color: $gl-text-color-secondary; + background: none; + font-family: inherit; + border-left: 2px solid $theme-gray-300; + border-radius: unset; a { color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index d7d343b088a..ea6467f0f11 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -354,30 +354,48 @@ min-width: 200px; } -.deploy-key-content { - @media (min-width: $screen-sm-min) { - float: left; +.deploy-keys { + .scrolling-tabs-container { + position: relative; + } +} - &:last-child { - float: right; +.deploy-key { + // Ensure that the fingerprint does not overflow on small screens + .fingerprint { + word-break: break-all; + white-space: normal; + } + + .deploy-project-label, + .key-created-at { + svg { + vertical-align: text-top; } } -} -.deploy-key-projects { - @media (min-width: $screen-sm-min) { - line-height: 42px; + .btn svg { + vertical-align: top; + } + + .key-created-at { + line-height: unset; } } -a.deploy-project-label { - padding: 5px; - margin-right: 5px; - color: $gl-text-color; - background-color: $row-hover; +.deploy-project-list { + margin-bottom: -$gl-padding-4; - &:hover { - color: $gl-link-color; + a.deploy-project-label { + margin-right: $gl-padding-4; + margin-bottom: $gl-padding-4; + color: $gl-text-color-secondary; + background-color: $theme-gray-100; + line-height: $gl-btn-line-height; + + &:hover { + color: $gl-link-color; + } } } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 7f1f0c1f5f1..00457717f00 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -23,6 +23,7 @@ margin-top: 0; border-top: 1px solid $white-dark; border-bottom: 1px solid $white-dark; + padding-bottom: $ide-statusbar-height; &.is-collapsed { .ide-file-list { @@ -375,7 +376,13 @@ padding: $gl-bar-padding $gl-padding; background: $white-light; display: flex; - justify-content: flex-end; + justify-content: space-between; + height: $ide-statusbar-height; + + position: absolute; + bottom: 0; + left: 0; + width: 100%; > div + div { padding-left: $gl-padding; @@ -386,6 +393,14 @@ } } +.ide-status-file { + text-align: right; + + .ide-status-branch + &, + &:first-child { + margin-left: auto; + } +} // Not great, but this is to deal with our current output .multi-file-preview-holder { height: 100%; diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 0379f76fc3d..c925b4aada5 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -18,7 +18,6 @@ module IssuableActions def update @issuable = update_service.execute(issuable) # rubocop:disable Gitlab/ModuleWithInstanceVariables - respond_to do |format| format.html do recaptcha_check_if_spammable { render :edit } diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb new file mode 100644 index 00000000000..78992ec7f46 --- /dev/null +++ b/app/controllers/groups/runners_controller.rb @@ -0,0 +1,58 @@ +class Groups::RunnersController < Groups::ApplicationController + # Proper policies should be implemented per + # https://gitlab.com/gitlab-org/gitlab-ce/issues/45894 + before_action :authorize_admin_pipeline! + + before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show] + + def show + render 'shared/runners/show' + end + + def edit + end + + def update + if Ci::UpdateRunnerService.new(@runner).update(runner_params) + redirect_to group_runner_path(@group, @runner), notice: 'Runner was successfully updated.' + else + render 'edit' + end + end + + def destroy + @runner.destroy + + redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: 302 + end + + def resume + if Ci::UpdateRunnerService.new(@runner).update(active: true) + redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.' + else + redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.' + end + end + + def pause + if Ci::UpdateRunnerService.new(@runner).update(active: false) + redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.' + else + redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.' + end + end + + private + + def runner + @runner ||= @group.runners.find(params[:id]) + end + + def authorize_admin_pipeline! + return render_404 unless can?(current_user, :admin_pipeline, group) + end + + def runner_params + params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) + end +end diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb new file mode 100644 index 00000000000..5698ff4e706 --- /dev/null +++ b/app/controllers/projects/mirrors_controller.rb @@ -0,0 +1,67 @@ +class Projects::MirrorsController < Projects::ApplicationController + include RepositorySettingsRedirect + + # Authorize + before_action :remote_mirror, only: [:update] + before_action :check_mirror_available! + before_action :authorize_admin_project! + + layout "project_settings" + + def show + redirect_to_repository_settings(project) + end + + def update + if project.update_attributes(mirror_params) + flash[:notice] = 'Mirroring settings were successfully updated.' + else + flash[:alert] = project.errors.full_messages.join(', ').html_safe + end + + respond_to do |format| + format.html { redirect_to_repository_settings(project) } + format.json do + if project.errors.present? + render json: project.errors, status: :unprocessable_entity + else + render json: ProjectMirrorSerializer.new.represent(project) + end + end + end + end + + def update_now + if params[:sync_remote] + project.update_remote_mirrors + flash[:notice] = "The remote repository is being updated..." + end + + redirect_to_repository_settings(project) + end + + private + + def remote_mirror + @remote_mirror = project.remote_mirrors.first_or_initialize + end + + def check_mirror_available! + Gitlab::CurrentSettings.current_application_settings.mirror_available || current_user&.admin? + end + + def mirror_params_attributes + [ + remote_mirrors_attributes: %i[ + url + id + enabled + only_protected_branches + ] + ] + end + + def mirror_params + params.require(:project).permit(mirror_params_attributes) + end +end diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb index 3cb01405b05..0ec2490655f 100644 --- a/app/controllers/projects/runner_projects_controller.rb +++ b/app/controllers/projects/runner_projects_controller.rb @@ -8,7 +8,7 @@ class Projects::RunnerProjectsController < Projects::ApplicationController return head(403) unless can?(current_user, :assign_runner, @runner) - path = runners_path(project) + path = project_runners_path(project) runner_project = @runner.assign_to(project, current_user) if runner_project.persisted? @@ -22,6 +22,6 @@ class Projects::RunnerProjectsController < Projects::ApplicationController runner_project = project.runner_projects.find(params[:id]) runner_project.destroy - redirect_to runners_path(project), status: 302 + redirect_to project_runners_path(project), status: 302 end end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index b9bbe7115c4..bef94cea989 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -1,6 +1,6 @@ class Projects::RunnersController < Projects::ApplicationController before_action :authorize_admin_build! - before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show] + before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show] layout 'project_settings' @@ -13,7 +13,7 @@ class Projects::RunnersController < Projects::ApplicationController def update if Ci::UpdateRunnerService.new(@runner).update(runner_params) - redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' + redirect_to project_runner_path(@project, @runner), notice: 'Runner was successfully updated.' else render 'edit' end @@ -24,26 +24,27 @@ class Projects::RunnersController < Projects::ApplicationController @runner.destroy end - redirect_to runners_path(@project), status: 302 + redirect_to project_runners_path(@project), status: 302 end def resume if Ci::UpdateRunnerService.new(@runner).update(active: true) - redirect_to runners_path(@project), notice: 'Runner was successfully updated.' + redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.' else - redirect_to runners_path(@project), alert: 'Runner was not updated.' + redirect_to project_runners_path(@project), alert: 'Runner was not updated.' end end def pause if Ci::UpdateRunnerService.new(@runner).update(active: false) - redirect_to runners_path(@project), notice: 'Runner was successfully updated.' + redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.' else - redirect_to runners_path(@project), alert: 'Runner was not updated.' + redirect_to project_runners_path(@project), alert: 'Runner was not updated.' end end def show + render 'shared/runners/show' end def toggle_shared_runners @@ -60,7 +61,7 @@ class Projects::RunnersController < Projects::ApplicationController protected - def set_runner + def runner @runner ||= project.runners.find(params[:id]) end diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index f17056f13e0..4697af4f26a 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -2,6 +2,7 @@ module Projects module Settings class RepositoryController < Projects::ApplicationController before_action :authorize_admin_project! + before_action :remote_mirror, only: [:show] def show render_show @@ -25,6 +26,7 @@ module Projects define_deploy_token define_protected_refs + remote_mirror render 'show' end @@ -41,6 +43,10 @@ module Projects load_gon_index end + def remote_mirror + @remote_mirror = project.remote_mirrors.first_or_initialize + end + def access_levels_options { create_access_levels: levels_for_dropdown, diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 1bf98d550b0..b948e431882 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -250,7 +250,8 @@ module ApplicationSettingsHelper :version_check_enabled, :allow_local_requests_from_hooks_and_services, :enforce_terms, - :terms + :terms, + :mirror_available ] end end diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 7e4eb06b99d..c24d340d184 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -2,4 +2,12 @@ module ClustersHelper def has_multiple_clusters?(project) false end + + def render_gcp_signup_offer + return unless show_gcp_signup_offer? + + content_tag :section, class: 'no-animate expanded' do + render 'projects/clusters/gcp_signup_offer_banner' + end + end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 40073f714ee..61e12b0f31e 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -19,14 +19,6 @@ module GitlabRoutingHelper project_commits_path(project, ref_name, *args) end - def runners_path(project, *args) - project_runners_path(project, *args) - end - - def runner_path(runner, *args) - project_runner_path(@project, runner, *args) - end - def environment_path(environment, *args) project_environment_path(environment.project, environment, *args) end diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index 36abfaf19a5..da5fe25c07d 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -1,11 +1,16 @@ module UserCalloutsHelper GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze + GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze def show_gke_cluster_integration_callout?(project) can?(current_user, :create_cluster, project) && !user_dismissed?(GKE_CLUSTER_INTEGRATION) end + def show_gcp_signup_offer? + !user_dismissed?(GCP_SIGNUP_OFFER) + end + private def user_dismissed?(feature_name) diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index 8bcced70d63..e12e4ba70e9 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -1,12 +1,12 @@ -require 'webpack/rails/manifest' +require 'gitlab/webpack/manifest' module WebpackHelper - def webpack_bundle_tag(bundle, force_same_domain: false) - javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: force_same_domain)) + def webpack_bundle_tag(bundle) + javascript_include_tag(*webpack_entrypoint_paths(bundle)) end def webpack_controller_bundle_tags - bundles = [] + chunks = [] action = case controller.action_name when 'create' then 'new' @@ -16,37 +16,44 @@ module WebpackHelper route = [*controller.controller_path.split('/'), action].compact - until route.empty? + until chunks.any? || route.empty? + entrypoint = "pages.#{route.join('.')}" begin - asset_paths = gitlab_webpack_asset_paths("pages.#{route.join('.')}", extension: 'js') - bundles.unshift(*asset_paths) - rescue Webpack::Rails::Manifest::EntryPointMissingError + chunks = webpack_entrypoint_paths(entrypoint, extension: 'js') + rescue Gitlab::Webpack::Manifest::AssetMissingError # no bundle exists for this path end - route.pop end - javascript_include_tag(*bundles) + if chunks.empty? + chunks = webpack_entrypoint_paths("default", extension: 'js') + end + + javascript_include_tag(*chunks) end - # override webpack-rails gem helper until changes can make it upstream - def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false) + def webpack_entrypoint_paths(source, extension: nil, exclude_duplicates: true) return "" unless source.present? - paths = Webpack::Rails::Manifest.asset_paths(source) + paths = Gitlab::Webpack::Manifest.entrypoint_paths(source) if extension paths.select! { |p| p.ends_with? ".#{extension}" } end - unless force_same_domain - force_host = webpack_public_host - if force_host - paths.map! { |p| "#{force_host}#{p}" } - end + force_host = webpack_public_host + if force_host + paths.map! { |p| "#{force_host}#{p}" } end - paths + if exclude_duplicates + @used_paths ||= [] + new_paths = paths - @used_paths + @used_paths += new_paths + new_paths + else + paths + end end def webpack_public_host diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index a734cc7a26b..451e512aef7 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -334,7 +334,8 @@ class ApplicationSetting < ActiveRecord::Base gitaly_timeout_fast: 10, gitaly_timeout_medium: 30, gitaly_timeout_default: 55, - allow_local_requests_from_hooks_and_services: false + allow_local_requests_from_hooks_and_services: false, + mirror_available: true } end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index c184f398f6c..0b90834d415 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -271,19 +271,39 @@ module Ci end def git_author_name - commit.try(:author_name) + strong_memoize(:git_author_name) do + commit.try(:author_name) + end end def git_author_email - commit.try(:author_email) + strong_memoize(:git_author_email) do + commit.try(:author_email) + end end def git_commit_message - commit.try(:message) + strong_memoize(:git_commit_message) do + commit.try(:message) + end end def git_commit_title - commit.try(:title) + strong_memoize(:git_commit_title) do + commit.try(:title) + end + end + + def git_commit_full_title + strong_memoize(:git_commit_full_title) do + commit.try(:full_title) + end + end + + def git_commit_description + strong_memoize(:git_commit_description) do + commit.try(:description) + end end def short_sha @@ -493,6 +513,9 @@ module Ci .append(key: 'CI_PIPELINE_ID', value: id.to_s) .append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path) .append(key: 'CI_PIPELINE_SOURCE', value: source.to_s) + .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message) + .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title) + .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description) end def queued_duration diff --git a/app/models/project.rb b/app/models/project.rb index 37f1fcea280..0a549d843f3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -65,6 +65,9 @@ class Project < ActiveRecord::Base default_value_for :only_allow_merge_if_all_discussions_are_resolved, false add_authentication_token_field :runners_token + + before_validation :mark_remote_mirrors_for_removal, if: -> { ActiveRecord::Base.connection.table_exists?(:remote_mirrors) } + before_save :ensure_runners_token after_save :update_project_statistics, if: :namespace_id_changed? @@ -246,11 +249,17 @@ class Project < ActiveRecord::Base has_many :project_badges, class_name: 'ProjectBadge' has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :remote_mirrors, inverse_of: :project + accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :import_data accepts_nested_attributes_for :auto_devops, update_only: true + accepts_nested_attributes_for :remote_mirrors, + allow_destroy: true, + reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? } + delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true delegate :add_user, :add_users, to: :team @@ -340,6 +349,7 @@ class Project < ActiveRecord::Base scope :with_issues_enabled, -> { with_feature_enabled(:issues) } scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) } scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) } + scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct } scope :with_group_runners_enabled, -> do joins(:ci_cd_settings) @@ -759,6 +769,37 @@ class Project < ActiveRecord::Base import_type == 'gitea' end + def has_remote_mirror? + remote_mirror_available? && remote_mirrors.enabled.exists? + end + + def updating_remote_mirror? + remote_mirrors.enabled.started.exists? + end + + def update_remote_mirrors + return unless remote_mirror_available? + + remote_mirrors.enabled.each(&:sync) + end + + def mark_stuck_remote_mirrors_as_failed! + remote_mirrors.stuck.update_all( + update_status: :failed, + last_error: 'The remote mirror took to long to complete.', + last_update_at: Time.now + ) + end + + def mark_remote_mirrors_for_removal + remote_mirrors.each(&:mark_for_delete_if_blank_url) + end + + def remote_mirror_available? + remote_mirror_available_overridden || + ::Gitlab::CurrentSettings.mirror_available + end + def check_limit unless creator.can_create_project? || namespace.kind == 'group' projects_limit = creator.projects_limit diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb new file mode 100644 index 00000000000..bbf8fd9c6a7 --- /dev/null +++ b/app/models/remote_mirror.rb @@ -0,0 +1,219 @@ +class RemoteMirror < ActiveRecord::Base + include AfterCommitQueue + + PROTECTED_BACKOFF_DELAY = 1.minute + UNPROTECTED_BACKOFF_DELAY = 5.minutes + + attr_encrypted :credentials, + key: Gitlab::Application.secrets.db_key_base, + marshal: true, + encode: true, + mode: :per_attribute_iv_and_salt, + insecure_mode: true, + algorithm: 'aes-256-cbc' + + default_value_for :only_protected_branches, true + + belongs_to :project, inverse_of: :remote_mirrors + + validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true } + validates :url, addressable_url: true, if: :url_changed? + + before_save :set_new_remote_name, if: :mirror_url_changed? + + after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available } + after_save :refresh_remote, if: :mirror_url_changed? + after_update :reset_fields, if: :mirror_url_changed? + + after_commit :remove_remote, on: :destroy + + scope :enabled, -> { where(enabled: true) } + scope :started, -> { with_update_status(:started) } + scope :stuck, -> { started.where('last_update_at < ? OR (last_update_at IS NULL AND updated_at < ?)', 1.day.ago, 1.day.ago) } + + state_machine :update_status, initial: :none do + event :update_start do + transition [:none, :finished, :failed] => :started + end + + event :update_finish do + transition started: :finished + end + + event :update_fail do + transition started: :failed + end + + state :started + state :finished + state :failed + + after_transition any => :started do |remote_mirror, _| + Gitlab::Metrics.add_event(:remote_mirrors_running, path: remote_mirror.project.full_path) + + remote_mirror.update(last_update_started_at: Time.now) + end + + after_transition started: :finished do |remote_mirror, _| + Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path) + + timestamp = Time.now + remote_mirror.update_attributes!( + last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil + ) + end + + after_transition started: :failed do |remote_mirror, _| + Gitlab::Metrics.add_event(:remote_mirrors_failed, path: remote_mirror.project.full_path) + + remote_mirror.update(last_update_at: Time.now) + end + end + + def remote_name + super || fallback_remote_name + end + + def update_failed? + update_status == 'failed' + end + + def update_in_progress? + update_status == 'started' + end + + def update_repository(options) + raw.update(options) + end + + def sync? + enabled? + end + + def sync + return unless sync? + + if recently_scheduled? + RepositoryUpdateRemoteMirrorWorker.perform_in(backoff_delay, self.id, Time.now) + else + RepositoryUpdateRemoteMirrorWorker.perform_async(self.id, Time.now) + end + end + + def enabled + return false unless project && super + return false unless project.remote_mirror_available? + return false unless project.repository_exists? + return false if project.pending_delete? + + true + end + alias_method :enabled?, :enabled + + def updated_since?(timestamp) + last_update_started_at && last_update_started_at > timestamp && !update_failed? + end + + def mark_for_delete_if_blank_url + mark_for_destruction if url.blank? + end + + def mark_as_failed(error_message) + update_fail + update_column(:last_error, Gitlab::UrlSanitizer.sanitize(error_message)) + end + + def url=(value) + super(value) && return unless Gitlab::UrlSanitizer.valid?(value) + + mirror_url = Gitlab::UrlSanitizer.new(value) + self.credentials = mirror_url.credentials + + super(mirror_url.sanitized_url) + end + + def url + if super + Gitlab::UrlSanitizer.new(super, credentials: credentials).full_url + end + rescue + super + end + + def safe_url + return if url.nil? + + result = URI.parse(url) + result.password = '*****' if result.password + result.user = '*****' if result.user && result.user != "git" # tokens or other data may be saved as user + result.to_s + end + + private + + def raw + @raw ||= Gitlab::Git::RemoteMirror.new(project.repository.raw, remote_name) + end + + def fallback_remote_name + return unless id + + "remote_mirror_#{id}" + end + + def recently_scheduled? + return false unless self.last_update_started_at + + self.last_update_started_at >= Time.now - backoff_delay + end + + def backoff_delay + if self.only_protected_branches + PROTECTED_BACKOFF_DELAY + else + UNPROTECTED_BACKOFF_DELAY + end + end + + def reset_fields + update_columns( + last_error: nil, + last_update_at: nil, + last_successful_update_at: nil, + update_status: 'finished' + ) + end + + def set_override_remote_mirror_available + enabled = read_attribute(:enabled) + + project.update(remote_mirror_available_overridden: enabled) + end + + def set_new_remote_name + self.remote_name = "remote_mirror_#{SecureRandom.hex}" + end + + def refresh_remote + return unless project + + # Before adding a new remote we have to delete the data from + # the previous remote name + prev_remote_name = remote_name_was || fallback_remote_name + run_after_commit do + project.repository.async_remove_remote(prev_remote_name) + end + + project.repository.add_remote(remote_name, url) + end + + def remove_remote + return unless project # could be pending to delete so don't need to touch the git repository + + project.repository.async_remove_remote(remote_name) + end + + def mirror_url_changed? + url_changed? || encrypted_credentials_changed? + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index 6831305fb93..b75c4aca982 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -854,13 +854,27 @@ class Repository add_remote(remote_name, url, mirror_refmap: refmap) fetch_remote(remote_name, forced: forced, prune: prune) ensure - remove_remote(remote_name) if tmp_remote_name + async_remove_remote(remote_name) if tmp_remote_name end def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false, prune: true) gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune) end + def async_remove_remote(remote_name) + return unless remote_name + + job_id = RepositoryRemoveRemoteWorker.perform_async(project.id, remote_name) + + if job_id + Rails.logger.info("Remove remote job scheduled for #{project.id} with remote name: #{remote_name} job ID #{job_id}.") + else + Rails.logger.info("Remove remote job failed to create for #{project.id} with remote name #{remote_name}.") + end + + job_id + end + def fetch_source_branch!(source_repository, source_branch, local_ref) raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref) end diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 29035480371..1c2161accc4 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -17,7 +17,11 @@ class SystemNoteMetadata < ActiveRecord::Base ].freeze validates :note, presence: true - validates :action, inclusion: ICON_TYPES, allow_nil: true + validates :action, inclusion: { in: :icon_types }, allow_nil: true belongs_to :note + + def icon_types + ICON_TYPES + end end diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index e4b69382626..9d461c6750a 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -2,7 +2,8 @@ class UserCallout < ActiveRecord::Base belongs_to :user enum feature_name: { - gke_cluster_integration: 1 + gke_cluster_integration: 1, + gcp_signup_offer: 2 } validates :user, presence: true diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 3529d0aa60c..5759b1a376f 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -80,6 +80,11 @@ class ProjectPolicy < BasePolicy project.merge_requests_allowing_push_to_user(user).any? end + with_scope :global + condition(:mirror_available, score: 0) do + ::Gitlab::CurrentSettings.current_application_settings.mirror_available + end + # We aren't checking `:read_issue` or `:read_merge_request` in this case # because it could be possible for a user to see an issuable-iid # (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be @@ -246,6 +251,8 @@ class ProjectPolicy < BasePolicy enable :create_cluster end + rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror + rule { archived }.policy do prevent :push_code prevent :push_to_delete_protected_branch diff --git a/app/serializers/project_mirror_entity.rb b/app/serializers/project_mirror_entity.rb new file mode 100644 index 00000000000..a9c08ac021a --- /dev/null +++ b/app/serializers/project_mirror_entity.rb @@ -0,0 +1,11 @@ +class ProjectMirrorEntity < Grape::Entity + expose :id + + expose :remote_mirrors_attributes do |project| + next [] unless project.remote_mirrors.present? + + project.remote_mirrors.map do |remote| + remote.as_json(only: %i[id url enabled]) + end + end +end diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb new file mode 100644 index 00000000000..30be6accc32 --- /dev/null +++ b/app/services/concerns/exclusive_lease_guard.rb @@ -0,0 +1,52 @@ +# +# Concern that helps with getting an exclusive lease for running a block +# of code. +# +# `#try_obtain_lease` takes a block which will be run if it was able to +# obtain the lease. Implement `#lease_timeout` to configure the timeout +# for the exclusive lease. Optionally override `#lease_key` to set the +# lease key, it defaults to the class name with underscores. +# +module ExclusiveLeaseGuard + extend ActiveSupport::Concern + + def try_obtain_lease + lease = exclusive_lease.try_obtain + + unless lease + log_error('Cannot obtain an exclusive lease. There must be another instance already in execution.') + return + end + + begin + yield lease + ensure + release_lease(lease) + end + end + + def exclusive_lease + @lease ||= Gitlab::ExclusiveLease.new(lease_key, timeout: lease_timeout) + end + + def lease_key + @lease_key ||= self.class.name.underscore + end + + def lease_timeout + raise NotImplementedError, + "#{self.class.name} does not implement #{__method__}" + end + + def release_lease(uuid) + Gitlab::ExclusiveLease.cancel(lease_key, uuid) + end + + def renew_lease! + exclusive_lease.renew + end + + def log_error(message, extra_args = {}) + logger.error(message) + end +end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index c037141fcde..f3bfc53dcd3 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -55,6 +55,7 @@ class GitPushService < BaseService execute_related_hooks perform_housekeeping + update_remote_mirrors update_caches update_signatures @@ -119,6 +120,13 @@ class GitPushService < BaseService protected + def update_remote_mirrors + return unless @project.has_remote_mirror? + + @project.mark_stuck_remote_mirrors_as_failed! + @project.update_remote_mirrors + end + def execute_related_hooks # Update merge requests that may be affected by this push. A new branch # could cause the last commit of a merge request to change. diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb new file mode 100644 index 00000000000..8183a2f26d7 --- /dev/null +++ b/app/services/projects/update_remote_mirror_service.rb @@ -0,0 +1,30 @@ +module Projects + class UpdateRemoteMirrorService < BaseService + attr_reader :errors + + def execute(remote_mirror) + @errors = [] + + return success unless remote_mirror.enabled? + + begin + repository.fetch_remote(remote_mirror.remote_name, no_tags: true) + + opts = {} + if remote_mirror.only_protected_branches? + opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name) + end + + remote_mirror.update_repository(opts) + rescue => e + errors << e.message.strip + end + + if errors.present? + error(errors.join("\n\n")) + else + success + end + end + end +end diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml new file mode 100644 index 00000000000..09183ec6260 --- /dev/null +++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml @@ -0,0 +1,16 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-2' + .col-sm-10 + .checkbox + = f.label :mirror_available do + = f.check_box :mirror_available + Allow mirrors to be setup for projects + %span.help-block + If disabled, only admins will be able to setup mirrors in projects. + = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring') + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index 3c00e3c8fc4..3f440c76ee0 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -313,3 +313,14 @@ = _('Allow requests to the local network from hooks and services.') .settings-content = render 'outbound' + +%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Repository mirror settings') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Configure push mirrors.') + .settings-content + = render partial: 'repository_mirrors_form' diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index d022016f70d..73fadc042b1 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -29,7 +29,7 @@ %hr .append-bottom-20 - = render '/projects/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner) + = render 'shared/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner) .row .col-md-6 diff --git a/app/views/groups/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml new file mode 100644 index 00000000000..1f9b43e8727 --- /dev/null +++ b/app/views/groups/runners/_group_runners.html.haml @@ -0,0 +1,24 @@ +- link = link_to 'Runners API', help_page_path('api/runners.md') + +%h3 + = _('Group Runners') + +.bs-callout.bs-callout-warning + = _('GitLab Group Runners can execute code for all the projects in this group.') + = _('They can be managed using the %{link}.').html_safe % { link: link } + +-# Proper policies should be implemented per +-# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894 +- if can?(current_user, :admin_pipeline, @group) + = render partial: 'ci/runner/how_to_setup_runner', + locals: { registration_token: @group.runners_token, type: 'group' } + +- if @group.runners.empty? + %h4.underlined-title + = _('This group does not provide any group Runners yet.') + +- else + %h4.underlined-title + = _('Available group Runners : %{runners}.').html_safe % { runners: @group.runners.count } + %ul.bordered-list + = render partial: 'groups/runners/runner', collection: @group.runners, as: :runner diff --git a/app/views/groups/runners/_index.html.haml b/app/views/groups/runners/_index.html.haml new file mode 100644 index 00000000000..0cf9011b471 --- /dev/null +++ b/app/views/groups/runners/_index.html.haml @@ -0,0 +1,9 @@ += render 'shared/runners/runner_description' + +%hr + +%p.lead + = _('To start serving your jobs you can add Runners to your group') +.row + .col-sm-6 + = render 'groups/runners/group_runners' diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml new file mode 100644 index 00000000000..76650a961d6 --- /dev/null +++ b/app/views/groups/runners/_runner.html.haml @@ -0,0 +1,27 @@ +%li.runner{ id: dom_id(runner) } + %h4 + = runner_status_icon(runner) + + = link_to runner.short_sha, group_runner_path(@group, runner), class: 'commit-sha' + + %small.edit-runner + = link_to edit_group_runner_path(@group, runner) do + = icon('edit') + + .pull-right + - if runner.active? + = link_to _('Pause'), pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") } + - else + = link_to _('Resume'), resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-success btn-sm' + = link_to _('Remove Runner'), group_runner_path(@group, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm' + .pull-right + %small.light + \##{runner.id} + - if runner.description.present? + %p.runner-description + = runner.description + - if runner.tag_list.present? + %p + - runner.tag_list.sort.each do |tag| + %span.label.label-primary + = tag diff --git a/app/views/groups/runners/edit.html.haml b/app/views/groups/runners/edit.html.haml new file mode 100644 index 00000000000..d4993d9c235 --- /dev/null +++ b/app/views/groups/runners/edit.html.haml @@ -0,0 +1,6 @@ +- page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners" + +%h4 Runner ##{@runner.id} + +%hr + = render 'shared/runners/form', runner: @runner, runner_form_url: group_runner_path(@group, @runner) diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index dd82922ec55..082e1b7befa 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -1,11 +1,27 @@ - breadcrumb_title "CI / CD Settings" - page_title "CI / CD" -%h4 - = _('Secret variables') - = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' +- expanded = Rails.env.test? -%p - = render "ci/variables/content" +%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Secret variables') + = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' + %button.btn.btn-default.js-settings-toggle{ type: "button" } + = expanded ? _('Collapse') : _('Expand') + %p.append-bottom-0 + = render "ci/variables/content" + .settings-content + = render 'ci/variables/index', save_endpoint: group_variables_path -= render 'ci/variables/index', save_endpoint: group_variables_path +%section.settings#runners-settings.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Runners settings') + %button.btn.btn-default.js-settings-toggle{ type: "button" } + = expanded ? _('Collapse') : _('Expand') + %p + = _('Register and see your runners for this group.') + .settings-content + = render 'groups/runners/index' diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml index e0e8fe548d0..da9331b45dd 100644 --- a/app/views/ide/index.html.haml +++ b/app/views/ide/index.html.haml @@ -1,9 +1,6 @@ - @body_class = 'ide' - page_title 'IDE' -- content_for :page_specific_javascripts do - = webpack_bundle_tag 'ide', force_same_domain: true - #ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'), "no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'), "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg') } } diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index b981b5fdafa..02bdfe9aa3c 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -38,9 +38,6 @@ = yield :library_javascripts = javascript_include_tag locale_path unless I18n.locale == :en - = webpack_bundle_tag "webpack_runtime" - = webpack_bundle_tag "common" - = webpack_bundle_tag "main" = webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled - if content_for?(:page_specific_javascripts) diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml new file mode 100644 index 00000000000..d0402197821 --- /dev/null +++ b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml @@ -0,0 +1,12 @@ +- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer') +.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' } + %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } × + %div + .col-sm-2.gcp-logo + = image_tag 'illustrations/logos/google-cloud-platform_logo.svg' + .col-sm-10 + %h4= s_('ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform') + %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link } + %a.btn.btn-info{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' } + Apply for credit + diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml index dada51f39da..ff046c59a7a 100644 --- a/app/views/projects/clusters/gcp/login.html.haml +++ b/app/views/projects/clusters/gcp/login.html.haml @@ -1,6 +1,8 @@ - breadcrumb_title 'Kubernetes' - page_title _("Login") += render_gcp_signup_offer + .row.prepend-top-default .col-sm-4 = render 'projects/clusters/sidebar' diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml index 17b244f4bf7..a55de84b5cd 100644 --- a/app/views/projects/clusters/index.html.haml +++ b/app/views/projects/clusters/index.html.haml @@ -1,6 +1,8 @@ - breadcrumb_title 'Kubernetes' - page_title "Kubernetes Clusters" += render_gcp_signup_offer + .clusters-container - if @clusters.empty? = render "empty_state" diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index e004966bdcc..828e2a84753 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -1,6 +1,8 @@ - breadcrumb_title 'Kubernetes' - page_title _("Kubernetes Cluster") += render_gcp_signup_offer + .row.prepend-top-default .col-sm-4 = render 'sidebar' diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 213c4c90a0e..1bffb3e8bf0 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -54,7 +54,7 @@ %h3.commit-title = markdown_field(@commit, :title) - if @commit.description.present? - %pre.commit-description + .commit-description< = preserve(markdown_field(@commit, :description)) .info-well diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 3fd0fa348b3..c390c9c4469 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -36,16 +36,16 @@ - if commit.description? %button.text-expander.hidden-xs.js-toggle-button{ type: "button" } ... - - if commit.description? - %pre.commit-row-description.js-toggle-content - = preserve(markdown_field(commit, :description)) - .commiter - commit_author_link = commit_author_link(commit, avatar: false, size: 24) - commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom') - commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } #{ commit_text.html_safe } + - if commit.description? + %pre.commit-row-description.js-toggle-content.prepend-top-8.append-bottom-8 + = preserve(markdown_field(commit, :description)) + .commit-actions.flex-row.hidden-xs - if request.xhr? = render partial: 'projects/commit/signature', object: commit.signature diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 7dd8dc28e5b..6af57d3ab26 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -12,4 +12,4 @@ Create a new deploy key for this project = render @deploy_keys.form_partial_path %hr - #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project) } } + #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project), project_id: @project.id } } diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 4e10511411f..773b12b4536 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -4,7 +4,7 @@ = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| .hide.alert.alert-danger.mr-compare-errors .js-merge-request-new-compare.row{ 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) } - .col-md-6 + .col-lg-6 .panel.panel-default.panel-new-merge-request .panel-heading Source branch @@ -31,7 +31,7 @@ .text-center= icon('spinner spin', class: 'js-source-loading') %ul.list-unstyled.mr_source_commit - .col-md-6 + .col-lg-6 .panel.panel-default.panel-new-merge-request .panel-heading Target branch diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml new file mode 100644 index 00000000000..64f0fde30cf --- /dev/null +++ b/app/views/projects/mirrors/_instructions.html.haml @@ -0,0 +1,10 @@ +.account-well.prepend-top-default.append-bottom-default + %ul + %li + The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> or <code>git://</code>. + %li + Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>. + %li + The update action will time out after 10 minutes. For big repositories, use a clone/push combination. + %li + The Git LFS objects will <strong>not</strong> be synced. diff --git a/app/views/projects/mirrors/_push.html.haml b/app/views/projects/mirrors/_push.html.haml new file mode 100644 index 00000000000..4a6aefce351 --- /dev/null +++ b/app/views/projects/mirrors/_push.html.haml @@ -0,0 +1,50 @@ +- expanded = Rails.env.test? +%section.settings.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4 + Push to a remote repository + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Set up the remote repository that you want to update with the content of the current repository + every time someone pushes to it. + = link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank' + .settings-content + = form_for @project, url: project_mirror_path(@project) do |f| + %div + = form_errors(@project) + = render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror + - if @remote_mirror.last_error.present? + .panel.panel-danger + .panel-heading + - if @remote_mirror.last_update_at + The remote repository failed to update #{time_ago_with_tooltip(@remote_mirror.last_update_at)}. + - else + The remote repository failed to update. + + - if @remote_mirror.last_successful_update_at + Last successful update #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}. + .panel-body + %pre + :preserve + #{h(@remote_mirror.last_error.strip)} + = f.fields_for :remote_mirrors, @remote_mirror do |rm_form| + .form-group + = rm_form.check_box :enabled, class: "pull-left" + .prepend-left-20 + = rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0" + %p.light.append-bottom-0 + Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it. + .form-group.has-feedback + = rm_form.label :url, "Git repository URL", class: "label-light" + = rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git' + + = render "projects/mirrors/instructions" + + .form-group + = rm_form.check_box :only_protected_branches, class: 'pull-left' + .prepend-left-20 + = rm_form.label :only_protected_branches, class: 'label-light' + = link_to icon('question-circle'), help_page_path('user/project/protected_branches') + + = f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror' diff --git a/app/views/projects/mirrors/_show.html.haml b/app/views/projects/mirrors/_show.html.haml new file mode 100644 index 00000000000..de77701a373 --- /dev/null +++ b/app/views/projects/mirrors/_show.html.haml @@ -0,0 +1,3 @@ +- if can?(current_user, :admin_remote_mirror, @project) + = render 'projects/mirrors/push' + diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 85946aec1f2..9db30042bf4 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -5,7 +5,7 @@ %h3.commit-title = markdown(@commit.title, pipeline: :single_line) - if @commit.description.present? - %pre.commit-description + .commit-description< = preserve(markdown(@commit.description, pipeline: :single_line)) .info-well diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml index a9dfd9cc786..ec8011182de 100644 --- a/app/views/projects/runners/_group_runners.html.haml +++ b/app/views/projects/runners/_group_runners.html.haml @@ -1,32 +1,37 @@ -%h3 Group Runners +- link = link_to 'Runners API', help_page_path('api/runners.md') + +%h3 + = _('Group Runners') .bs-callout.bs-callout-warning - GitLab Group Runners can execute code for all the projects in this group. - They can be managed using the #{link_to 'Runners API', help_page_path('api/runners.md')}. + = _('GitLab Group Runners can execute code for all the projects in this group.') + = _('They can be managed using the %{link}.').html_safe % { link: link } - if @project.group %hr - if @project.group_runners_enabled? - = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-warning', method: :post do - Disable group Runners + = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do + = _('Disable group Runners') - else - = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do - Enable group Runners - for this project + = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success btn-inverted', method: :post do + = _('Enable group Runners') + + = _('for this project') - if !@project.group - This project does not belong to a group and can therefore not make use of group Runners. + = _('This project does not belong to a group and can therefore not make use of group Runners.') - elsif @group_runners.empty? - This group does not provide any group Runners yet. + = _('This group does not provide any group Runners yet.') - if can?(current_user, :admin_pipeline, @project.group) - = render partial: 'ci/runner/how_to_setup_runner', - locals: { registration_token: @project.group.runners_token, type: 'group' } + - group_link = link_to 'Group CI/CD settings', group_settings_ci_cd_path(@project.group) + = _('Group masters can register group runners in the %{link}').html_safe % { link: group_link } - else - Ask your group master to setup a group Runner. + = _('Ask your group master to setup a group Runner.') - else - %h4.underlined-title Available group Runners : #{@group_runners.count} + %h4.underlined-title + = _('Available group Runners : %{runners}').html_safe % { runners: @group_runners.count } %ul.bordered-list = render partial: 'projects/runners/runner', collection: @group_runners, as: :runner diff --git a/app/views/projects/runners/_index.html.haml b/app/views/projects/runners/_index.html.haml index 3f5119d408b..022687b831f 100644 --- a/app/views/projects/runners/_index.html.haml +++ b/app/views/projects/runners/_index.html.haml @@ -1,19 +1,4 @@ -.light.prepend-top-default - %p - A 'Runner' is a process which runs a job. - You can setup as many Runners as you need. - %br - Runners can be placed on separate users, servers, and even on your local machine. - - %p Each Runner can be in one of the following states: - %div - %ul - %li - %span.label.label-success active - \- Runner is active and can process any new jobs - %li - %span.label.label-danger paused - \- Runner is paused and will not receive any new jobs += render 'shared/runners/runner_description' %hr @@ -23,7 +8,4 @@ = render 'projects/runners/specific_runners' .col-sm-6 = render 'projects/runners/shared_runners' -.row - .col-sm-6 - .col-sm-6 = render 'projects/runners/group_runners' diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 0d2c0536eb5..69218f344f7 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -3,10 +3,10 @@ = runner_status_icon(runner) - if @project_runners.include?(runner) - = link_to runner.short_sha, runner_path(runner), class: 'commit-sha' + = link_to runner.short_sha, project_runner_path(@project, runner), class: 'commit-sha' - if runner.locked? - = icon('lock', class: 'has-tooltip', title: 'Locked to current projects') + = icon('lock', class: 'has-tooltip', title: _('Locked to current projects')) %small.edit-runner = link_to edit_project_runner_path(@project, runner) do @@ -18,18 +18,18 @@ .pull-right - if @project_runners.include?(runner) - if runner.active? - = link_to 'Pause', pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: "Are you sure?" } + = link_to _('Pause'), pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") } - else - = link_to 'Resume', resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm' + = link_to _('Resume'), resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm' - if runner.belongs_to_one_project? - = link_to 'Remove Runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + = link_to _('Remove Runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm' - else - runner_project = @project.runner_projects.find_by(runner_id: runner) - = link_to 'Disable for this project', project_runner_project_path(@project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + = link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm' - elsif !(runner.is_shared? || runner.group_type?) # We can simplify this to `runner.project_type?` when migrating #runner_type is complete = form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: runner.id - = f.submit 'Enable for this project', class: 'btn btn-sm' + = f.submit _('Enable for this project'), class: 'btn btn-sm' .pull-right %small.light \##{runner.id} diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 4fd4ca355a8..969efdb2560 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -17,8 +17,8 @@ for this project - if @shared_runners_count.zero? - This GitLab server does not provide any shared Runners yet. - Please use the specific Runners or ask your administrator to create one. + This GitLab instance does not provide any shared Runners yet. Instance + administrators can register shared Runners in the admin area. - else %h4.underlined-title Available shared Runners : #{@shared_runners_count} %ul.bordered-list.available-shared-runners diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index 78dc4817ed7..56eb526bfd5 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -3,4 +3,4 @@ %h4 Runner ##{@runner.id} %hr - = render 'form', runner: @runner, runner_form_url: runner_path(@runner) + = render 'shared/runners/form', runner: @runner, runner_form_url: project_runner_path(@project, @runner) diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index f57590a908f..5dda2ec28b4 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -2,6 +2,8 @@ - page_title "Repository" - @content_class = "limit-container-width" unless fluid_layout += render "projects/mirrors/show" + -# Protected branches & tags use a lot of nested partials. -# The shared parts of the views can be found in the `shared` directory. -# Those are used throughout the actual views. These `shared` views are then diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml new file mode 100644 index 00000000000..34de1c0695f --- /dev/null +++ b/app/views/shared/_remote_mirror_update_button.html.haml @@ -0,0 +1,13 @@ +- if @project.has_remote_mirror? + .append-bottom-default + - if remote_mirror.update_in_progress? + %span.btn.disabled + = icon("refresh spin") + Updating… + - else + = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn" do + = icon("refresh") + Update Now + - if @remote_mirror.last_successful_update_at + %p.inline.prepend-left-10 + Successfully updated #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}. diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index 149bf8da4b9..4bff6468bb0 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -15,8 +15,9 @@ ":title" => '(list.label ? list.label.description : "")', data: { container: "body", placement: "bottom" }, class: "label color-label title board-title-text", - ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color : \"#2e2e2e\") }" } + ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.text_color ? list.label.text_color : \"#2e2e2e\") }" } {{ list.title }} + - if can?(current_user, :admin_list, current_board_parent) %board-delete{ "inline-template" => true, ":list" => "list", diff --git a/app/views/projects/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml index 6a681736b6f..a995c355bd8 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/shared/runners/_form.html.haml @@ -18,12 +18,13 @@ .checkbox = f.check_box :run_untagged %span.light Indicates whether this runner can pick jobs without tags - .form-group - = label :locked, 'Lock to current projects', class: 'control-label' - .col-sm-10 - .checkbox - = f.check_box :locked - %span.light When a runner is locked, it cannot be assigned to other projects + - unless runner.group_type? + .form-group + = label :locked, 'Lock to current projects', class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :locked + %span.light When a runner is locked, it cannot be assigned to other projects .form-group = label_tag :token, class: 'control-label' do Token diff --git a/app/views/shared/runners/_runner_description.html.haml b/app/views/shared/runners/_runner_description.html.haml new file mode 100644 index 00000000000..1d59c2f7078 --- /dev/null +++ b/app/views/shared/runners/_runner_description.html.haml @@ -0,0 +1,16 @@ +.light.prepend-top-default + %p + = _("A 'Runner' is a process which runs a job. You can setup as many Runners as you need.") + %br + = _('Runners can be placed on separate users, servers, and even on your local machine.') + + %p + = _('Each Runner can be in one of the following states:') + %div + %ul + %li + %span.label.label-success active + = _('- Runner is active and can process any new jobs') + %li + %span.label.label-danger paused + = _('- Runner is paused and will not receive any new jobs') diff --git a/app/views/projects/runners/show.html.haml b/app/views/shared/runners/show.html.haml index f33e7e25b68..1265305608c 100644 --- a/app/views/projects/runners/show.html.haml +++ b/app/views/shared/runners/show.html.haml @@ -6,6 +6,9 @@ - if @runner.shared? %span.runner-state.runner-state-shared Shared + - elsif @runner.group_type? + %span.runner-state.runner-state-shared + Group - else %span.runner-state.runner-state-specific Specific @@ -25,9 +28,10 @@ %tr %td Can run untagged jobs %td= @runner.run_untagged? ? 'Yes' : 'No' - %tr - %td Locked to this project - %td= @runner.locked? ? 'Yes' : 'No' + - unless @runner.group_type? + %tr + %td Locked to this project + %td= @runner.locked? ? 'Yes' : 'No' %tr %td Tags %td diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 5d9ec6142d7..b6433eb3eff 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -107,9 +107,11 @@ - rebase - repository_fork - repository_import +- repository_remove_remote - storage_migrator - system_hook_push - update_merge_requests - update_user_activity - upload_checksum - web_hook +- repository_update_remote_mirror diff --git a/app/workers/repository_remove_remote_worker.rb b/app/workers/repository_remove_remote_worker.rb new file mode 100644 index 00000000000..1c19b604b77 --- /dev/null +++ b/app/workers/repository_remove_remote_worker.rb @@ -0,0 +1,35 @@ +class RepositoryRemoveRemoteWorker + include ApplicationWorker + include ExclusiveLeaseGuard + + LEASE_TIMEOUT = 1.hour + + attr_reader :project, :remote_name + + def perform(project_id, remote_name) + @remote_name = remote_name + @project = Project.find_by_id(project_id) + + return unless @project + + logger.info("Removing remote #{remote_name} from project #{project.id}") + + try_obtain_lease do + remove_remote = @project.repository.remove_remote(remote_name) + + if remove_remote + logger.info("Remote #{remote_name} was successfully removed from project #{project.id}") + else + logger.error("Could not remove remote #{remote_name} from project #{project.id}") + end + end + end + + def lease_timeout + LEASE_TIMEOUT + end + + def lease_key + "remove_remote_#{project.id}_#{remote_name}" + end +end diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb new file mode 100644 index 00000000000..bb963979e88 --- /dev/null +++ b/app/workers/repository_update_remote_mirror_worker.rb @@ -0,0 +1,49 @@ +class RepositoryUpdateRemoteMirrorWorker + UpdateAlreadyInProgressError = Class.new(StandardError) + UpdateError = Class.new(StandardError) + + include ApplicationWorker + include Gitlab::ShellAdapter + + sidekiq_options retry: 3, dead: false + + sidekiq_retry_in { |count| 30 * count } + + sidekiq_retries_exhausted do |msg, _| + Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}" + end + + def perform(remote_mirror_id, scheduled_time) + remote_mirror = RemoteMirror.find(remote_mirror_id) + return if remote_mirror.updated_since?(scheduled_time) + + raise UpdateAlreadyInProgressError if remote_mirror.update_in_progress? + + remote_mirror.update_start + + project = remote_mirror.project + current_user = project.creator + result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror) + raise UpdateError, result[:message] if result[:status] == :error + + remote_mirror.update_finish + rescue UpdateAlreadyInProgressError + raise + rescue UpdateError => ex + fail_remote_mirror(remote_mirror, ex.message) + raise + rescue => ex + return unless remote_mirror + + fail_remote_mirror(remote_mirror, ex.message) + raise UpdateError, "#{ex.class}: #{ex.message}" + end + + private + + def fail_remote_mirror(remote_mirror, message) + remote_mirror.mark_as_failed(message) + + Rails.logger.error(message) + end +end diff --git a/changelogs/unreleased/36983-osw-heading-labels-color-fix.yml b/changelogs/unreleased/36983-osw-heading-labels-color-fix.yml new file mode 100644 index 00000000000..082e0544dea --- /dev/null +++ b/changelogs/unreleased/36983-osw-heading-labels-color-fix.yml @@ -0,0 +1,5 @@ +--- +title: Adjust issue boards list header label text color +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml b/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml new file mode 100644 index 00000000000..23704c2b37b --- /dev/null +++ b/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml @@ -0,0 +1,5 @@ +--- +title: Make project deploy keys table more clearly structured +merge_request: 18279 +author: +type: changed diff --git a/changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.yml b/changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.yml new file mode 100644 index 00000000000..f23521ea416 --- /dev/null +++ b/changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.yml @@ -0,0 +1,5 @@ +--- +title: Adds push mirrors to GitLab Community Edition +merge_request: 18715 +author: +type: changed diff --git a/changelogs/unreleased/43469-gcp-account-offer.yml b/changelogs/unreleased/43469-gcp-account-offer.yml new file mode 100644 index 00000000000..323a4b81731 --- /dev/null +++ b/changelogs/unreleased/43469-gcp-account-offer.yml @@ -0,0 +1,5 @@ +--- +title: Add GCP signup offer to cluster index / create pages +merge_request: 18684 +author: +type: added diff --git a/changelogs/unreleased/44833-ide-clean-up-status-bar.yml b/changelogs/unreleased/44833-ide-clean-up-status-bar.yml new file mode 100644 index 00000000000..4c827e57195 --- /dev/null +++ b/changelogs/unreleased/44833-ide-clean-up-status-bar.yml @@ -0,0 +1,5 @@ +--- +title: Clean up WebIDE status bar and add useful info +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/add-git-commit-message-predefined-variable.yml b/changelogs/unreleased/add-git-commit-message-predefined-variable.yml new file mode 100644 index 00000000000..183fe69936e --- /dev/null +++ b/changelogs/unreleased/add-git-commit-message-predefined-variable.yml @@ -0,0 +1,5 @@ +--- +title: Add CI_COMMIT_MESSAGE, CI_COMMIT_TITLE and CI_COMMIT_DESCRIPTION predefined variables +merge_request: 18672 +author: +type: added diff --git a/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml b/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml new file mode 100644 index 00000000000..9cbc856a075 --- /dev/null +++ b/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml @@ -0,0 +1,5 @@ +--- +title: Fix close keyboard shortcuts dialog using the keyboard shortcut +merge_request: 18783 +author: Lars Greiss +type: fixed diff --git a/changelogs/unreleased/improve-commit-message-body-rendering.yml b/changelogs/unreleased/improve-commit-message-body-rendering.yml new file mode 100644 index 00000000000..3fb9b03725e --- /dev/null +++ b/changelogs/unreleased/improve-commit-message-body-rendering.yml @@ -0,0 +1,5 @@ +--- +title: Improve commit message body rendering and fix responsive compare panels +merge_request: 18725 +author: Constance Okoghenun +type: changed diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index d3a7a2b9f8b..6c28686e69a 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -34,7 +34,7 @@ if app.config.serve_static_files ) app.config.middleware.insert_before( Gitlab::Middleware::Static, - Gitlab::Middleware::WebpackProxy, + Gitlab::Webpack::DevServerMiddleware, proxy_path: app.config.webpack.public_path, proxy_host: dev_server.host, proxy_port: dev_server.port diff --git a/config/karma.config.js b/config/karma.config.js index 3eb220eed99..28a688797d9 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -12,16 +12,14 @@ function fatalError(message) { process.exit(1); } -// remove problematic plugins -if (webpackConfig.plugins) { - webpackConfig.plugins = webpackConfig.plugins.filter(function(plugin) { - return !( - plugin instanceof webpack.optimize.CommonsChunkPlugin || - plugin instanceof webpack.optimize.ModuleConcatenationPlugin || - plugin instanceof webpack.DefinePlugin - ); - }); -} +// disable problematic options +webpackConfig.entry = undefined; +webpackConfig.mode = 'development'; +webpackConfig.optimization.runtimeChunk = false; +webpackConfig.optimization.splitChunks = false; + +// use quicker sourcemap option +webpackConfig.devtool = 'cheap-inline-source-map'; const specFilters = argumentsParser .option( @@ -77,9 +75,6 @@ if (specFilters.length) { ); } -webpackConfig.entry = undefined; -webpackConfig.devtool = 'cheap-inline-source-map'; - // Karma configuration module.exports = function(config) { process.env.TZ = 'Etc/UTC'; diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml index 10ca612b246..13732384953 100644 --- a/config/prometheus/additional_metrics.yml +++ b/config/prometheus/additional_metrics.yml @@ -103,10 +103,10 @@ - title: "Throughput" y_label: "Requests / Sec" required_metrics: - - nginx_responses_total + - nginx_server_requests weight: 1 queries: - - query_range: 'sum(rate(nginx_responses_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (status_code)' + - query_range: 'sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code)' unit: req / sec label: Status Code series: @@ -121,19 +121,19 @@ - title: "Latency" y_label: "Latency (ms)" required_metrics: - - nginx_upstream_response_msecs_avg + - nginx_server_requestMsec weight: 1 queries: - - query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}})' + - query_range: 'avg(nginx_server_requestMsec{%{environment_filter}})' label: Upstream unit: ms - title: "HTTP Error Rate" y_label: "HTTP 500 Errors / Sec" required_metrics: - - nginx_responses_total + - nginx_server_requests weight: 1 queries: - - query_range: 'sum(rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m]))' + - query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))' label: HTTP Errors unit: "errors / sec" - group: System metrics (Kubernetes) diff --git a/config/routes/group.rb b/config/routes/group.rb index 170508e893d..7c4c3d370e0 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -58,6 +58,13 @@ constraints(::Constraints::GroupUrlConstrainer.new) do # On CE only index and show actions are needed resources :boards, only: [:index, :show] + + resources :runners, only: [:index, :edit, :update, :destroy, :show] do + member do + post :resume + post :pause + end + end end scope(path: '*id', diff --git a/config/routes/project.rb b/config/routes/project.rb index f36341cdcaf..5a1be1a8b73 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -174,6 +174,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end + resource :mirror, only: [:show, :update] do + member do + post :update_now + end + end + resources :pipelines, only: [:index, :new, :create, :show] do collection do resource :pipelines_settings, path: 'settings', only: [:show, :update] diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 47fbbed44cf..e1e8f36b663 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -73,3 +73,6 @@ - [object_storage, 1] - [plugin, 1] - [pipeline_background, 1] + - [repository_update_remote_mirror, 1] + - [repository_remove_remote, 1] + diff --git a/config/webpack.config.js b/config/webpack.config.js index b9d098ff9b9..5096f35e808 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -1,4 +1,3 @@ -const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const glob = require('glob'); @@ -6,9 +5,7 @@ const webpack = require('webpack'); const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; const CopyWebpackPlugin = require('copy-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); -const NameAllModulesPlugin = require('name-all-modules-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; -const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); const ROOT_PATH = path.resolve(__dirname, '..'); const IS_PRODUCTION = process.env.NODE_ENV === 'production'; @@ -21,10 +18,12 @@ const NO_COMPRESSION = process.env.NO_COMPRESSION; let autoEntriesCount = 0; let watchAutoEntries = []; +const defaultEntries = ['./main']; function generateEntries() { // generate automatic entry points const autoEntries = {}; + const autoEntriesMap = {}; const pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts'), }); @@ -33,25 +32,38 @@ function generateEntries() { function generateAutoEntries(path, prefix = '.') { const chunkPath = path.replace(/\/index\.js$/, ''); const chunkName = chunkPath.replace(/\//g, '.'); - autoEntries[chunkName] = `${prefix}/${path}`; + autoEntriesMap[chunkName] = `${prefix}/${path}`; } pageEntries.forEach(path => generateAutoEntries(path)); - autoEntriesCount = Object.keys(autoEntries).length; + const autoEntryKeys = Object.keys(autoEntriesMap); + autoEntriesCount = autoEntryKeys.length; + + // import ancestor entrypoints within their children + autoEntryKeys.forEach(entry => { + const entryPaths = [autoEntriesMap[entry]]; + const segments = entry.split('.'); + while (segments.pop()) { + const ancestor = segments.join('.'); + if (autoEntryKeys.includes(ancestor)) { + entryPaths.unshift(autoEntriesMap[ancestor]); + } + } + autoEntries[entry] = defaultEntries.concat(entryPaths); + }); const manualEntries = { - common: './commons/index.js', - main: './main.js', + default: defaultEntries, raven: './raven/index.js', - webpack_runtime: './webpack.js', - ide: './ide/index.js', }; return Object.assign(manualEntries, autoEntries); } const config = { + mode: IS_PRODUCTION ? 'production' : 'development', + context: path.join(ROOT_PATH, 'app/assets/javascripts'), entry: generateEntries, @@ -59,8 +71,36 @@ const config = { output: { path: path.join(ROOT_PATH, 'public/assets/webpack'), publicPath: '/assets/webpack/', - filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js', - chunkFilename: IS_PRODUCTION ? '[name].[chunkhash].chunk.js' : '[name].chunk.js', + filename: IS_PRODUCTION ? '[name].[chunkhash:8].bundle.js' : '[name].bundle.js', + chunkFilename: IS_PRODUCTION ? '[name].[chunkhash:8].chunk.js' : '[name].chunk.js', + globalObject: 'this', // allow HMR and web workers to play nice + }, + + optimization: { + nodeEnv: false, + runtimeChunk: 'single', + splitChunks: { + maxInitialRequests: 4, + cacheGroups: { + default: false, + common: () => ({ + priority: 20, + name: 'main', + chunks: 'initial', + minChunks: autoEntriesCount * 0.9, + }), + vendors: { + priority: 10, + chunks: 'async', + test: /[\\/](node_modules|vendor[\\/]assets[\\/]javascripts)[\\/]/, + }, + commons: { + chunks: 'all', + minChunks: 2, + reuseExistingChunk: true, + }, + }, + }, }, module: { @@ -92,10 +132,10 @@ const config = { { loader: 'worker-loader', options: { - inline: true, + name: '[name].[hash:8].worker.js', }, }, - { loader: 'babel-loader' }, + 'babel-loader', ], }, { @@ -103,7 +143,7 @@ const config = { exclude: /node_modules/, loader: 'file-loader', options: { - name: '[name].[hash].[ext]', + name: '[name].[hash:8].[ext]', }, }, { @@ -114,7 +154,7 @@ const config = { { loader: 'css-loader', options: { - name: '[name].[hash].[ext]', + name: '[name].[hash:8].[ext]', }, }, ], @@ -124,7 +164,7 @@ const config = { include: /node_modules\/katex\/dist\/fonts/, loader: 'file-loader', options: { - name: '[name].[hash].[ext]', + name: '[name].[hash:8].[ext]', }, }, { @@ -166,54 +206,6 @@ const config = { jQuery: 'jquery', }), - // assign deterministic module ids - new webpack.NamedModulesPlugin(), - new NameAllModulesPlugin(), - - // assign deterministic chunk ids - new webpack.NamedChunksPlugin(chunk => { - if (chunk.name) { - return chunk.name; - } - - const moduleNames = []; - - function collectModuleNames(m) { - // handle ConcatenatedModule which does not have resource nor context set - if (m.modules) { - m.modules.forEach(collectModuleNames); - return; - } - - const pagesBase = path.join(ROOT_PATH, 'app/assets/javascripts/pages'); - - if (m.resource.indexOf(pagesBase) === 0) { - moduleNames.push( - path - .relative(pagesBase, m.resource) - .replace(/\/index\.[a-z]+$/, '') - .replace(/\//g, '__') - ); - } else { - moduleNames.push(path.relative(m.context, m.resource)); - } - } - - chunk.forEachModule(collectModuleNames); - - const hash = crypto - .createHash('sha256') - .update(moduleNames.join('_')) - .digest('hex'); - - return `${moduleNames[0]}-${hash.substr(0, 6)}`; - }), - - // create cacheable common library bundles - new webpack.optimize.CommonsChunkPlugin({ - names: ['main', 'common', 'webpack_runtime'], - }), - // copy pre-compiled vendor libraries verbatim new CopyWebpackPlugin([ { @@ -260,20 +252,6 @@ const config = { if (IS_PRODUCTION) { config.devtool = 'source-map'; - config.plugins.push( - new webpack.NoEmitOnErrorsPlugin(), - new webpack.LoaderOptionsPlugin({ - minimize: true, - debug: false, - }), - new webpack.optimize.ModuleConcatenationPlugin(), - new webpack.optimize.UglifyJsPlugin({ - sourceMap: true, - }), - new webpack.DefinePlugin({ - 'process.env': { NODE_ENV: JSON.stringify('production') }, - }) - ); // compression can require a lot of compute time and is disabled in CI if (!NO_COMPRESSION) { @@ -292,29 +270,30 @@ if (IS_DEV_SERVER) { hot: DEV_SERVER_LIVERELOAD, inline: DEV_SERVER_LIVERELOAD, }; - config.plugins.push( - // watch node_modules for changes if we encounter a missing module compile error - new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')), - - // watch for changes to our automatic entry point modules - { - apply(compiler) { - compiler.plugin('emit', (compilation, callback) => { - compilation.contextDependencies = [ - ...compilation.contextDependencies, - ...watchAutoEntries, - ]; - - // report our auto-generated bundle count - console.log( - `${autoEntriesCount} entries from '/pages' automatically added to webpack output.` - ); - - callback(); - }); - }, - } - ); + config.plugins.push({ + apply(compiler) { + compiler.hooks.emit.tapAsync('WatchForChangesPlugin', (compilation, callback) => { + const missingDeps = Array.from(compilation.missingDependencies); + const nodeModulesPath = path.join(ROOT_PATH, 'node_modules'); + const hasMissingNodeModules = missingDeps.some( + file => file.indexOf(nodeModulesPath) !== -1 + ); + + // watch for changes to missing node_modules + if (hasMissingNodeModules) compilation.contextDependencies.add(nodeModulesPath); + + // watch for changes to automatic entrypoints + watchAutoEntries.forEach(watchPath => compilation.contextDependencies.add(watchPath)); + + // report our auto-generated bundle count + console.log( + `${autoEntriesCount} entries from '/pages' automatically added to webpack output.` + ); + + callback(); + }); + }, + }); if (DEV_SERVER_LIVERELOAD) { config.plugins.push(new webpack.HotModuleReplacementPlugin()); } diff --git a/db/migrate/20180420010616_cleanup_build_stage_migration.rb b/db/migrate/20180420010616_cleanup_build_stage_migration.rb new file mode 100644 index 00000000000..0342695ec3d --- /dev/null +++ b/db/migrate/20180420010616_cleanup_build_stage_migration.rb @@ -0,0 +1,28 @@ +class CleanupBuildStageMigration < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class Build < ActiveRecord::Base + include EachBatch + + self.table_name = 'ci_builds' + self.inheritance_column = :_type_disabled + end + + def up + Gitlab::BackgroundMigration.steal('MigrateBuildStage') + + Build.where('stage_id IS NULL').each_batch(of: 50) do |batch| + range = batch.pluck('MIN(id)', 'MAX(id)').first + + Gitlab::BackgroundMigration::MigrateBuildStage.new.perform(*range) + end + end + + def down + # noop + end +end diff --git a/db/migrate/20180503131624_create_remote_mirrors.rb b/db/migrate/20180503131624_create_remote_mirrors.rb new file mode 100644 index 00000000000..7800186455f --- /dev/null +++ b/db/migrate/20180503131624_create_remote_mirrors.rb @@ -0,0 +1,33 @@ +class CreateRemoteMirrors < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + return if table_exists?(:remote_mirrors) + + create_table :remote_mirrors do |t| + t.references :project, index: true, foreign_key: { on_delete: :cascade } + t.string :url + t.boolean :enabled, default: true + t.string :update_status + t.datetime :last_update_at + t.datetime :last_successful_update_at + t.datetime :last_update_started_at + t.string :last_error + t.boolean :only_protected_branches, default: false, null: false + t.string :remote_name + t.text :encrypted_credentials + t.string :encrypted_credentials_iv + t.string :encrypted_credentials_salt + + t.timestamps null: false + end + end + + def down + drop_table(:remote_mirrors) if table_exists?(:remote_mirrors) + end +end diff --git a/db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb b/db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb new file mode 100644 index 00000000000..841393971f4 --- /dev/null +++ b/db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb @@ -0,0 +1,15 @@ +class AddRemoteMirrorAvailableOverriddenToProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column(:projects, :remote_mirror_available_overridden, :boolean) unless column_exists?(:projects, :remote_mirror_available_overridden) + end + + def down + remove_column(:projects, :remote_mirror_available_overridden) if column_exists?(:projects, :remote_mirror_available_overridden) + end +end diff --git a/db/migrate/20180503193542_add_indexes_to_remote_mirror.rb b/db/migrate/20180503193542_add_indexes_to_remote_mirror.rb new file mode 100644 index 00000000000..9a9decffdab --- /dev/null +++ b/db/migrate/20180503193542_add_indexes_to_remote_mirror.rb @@ -0,0 +1,15 @@ +class AddIndexesToRemoteMirror < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :remote_mirrors, :last_successful_update_at unless index_exists?(:remote_mirrors, :last_successful_update_at) + end + + def down + remove_index :remote_mirrors, :last_successful_update_at if index_exists? :remote_mirrors, :last_successful_update_at + end +end diff --git a/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb b/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb new file mode 100644 index 00000000000..25b9905b1a9 --- /dev/null +++ b/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb @@ -0,0 +1,15 @@ +class AddMirrorAvailableToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:application_settings, :mirror_available, :boolean, default: true, allow_null: false) unless column_exists?(:application_settings, :mirror_available) + end + + def down + remove_column(:application_settings, :mirror_available) if column_exists?(:application_settings, :mirror_available) + end +end diff --git a/db/schema.rb b/db/schema.rb index 9c6caaeb7ef..65e9cc4ea08 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -165,6 +165,7 @@ ActiveRecord::Schema.define(version: 20180503200320) do t.boolean "pages_domain_verification_enabled", default: true, null: false t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false t.boolean "enforce_terms", default: false + t.boolean "mirror_available", default: true, null: false end create_table "audit_events", force: :cascade do |t| @@ -1602,6 +1603,7 @@ ActiveRecord::Schema.define(version: 20180503200320) do t.boolean "merge_requests_rebase_enabled", default: false, null: false t.integer "jobs_cache_index" t.boolean "pages_https_only", default: true + t.boolean "remote_mirror_available_overridden" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree @@ -1707,6 +1709,27 @@ ActiveRecord::Schema.define(version: 20180503200320) do add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree + create_table "remote_mirrors", force: :cascade do |t| + t.integer "project_id" + t.string "url" + t.boolean "enabled", default: true + t.string "update_status" + t.datetime "last_update_at" + t.datetime "last_successful_update_at" + t.datetime "last_update_started_at" + t.string "last_error" + t.boolean "only_protected_branches", default: false, null: false + t.string "remote_name" + t.text "encrypted_credentials" + t.string "encrypted_credentials_iv" + t.string "encrypted_credentials_salt" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "remote_mirrors", ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree + add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree + create_table "routes", force: :cascade do |t| t.integer "source_id", null: false t.string "source_type", null: false @@ -2243,6 +2266,7 @@ ActiveRecord::Schema.define(version: 20180503200320) do add_foreign_key "protected_tags", "projects", name: "fk_8e4af87648", on_delete: :cascade add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade + add_foreign_key "remote_mirrors", "projects", on_delete: :cascade add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade add_foreign_key "subscriptions", "projects", on_delete: :cascade diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 38a988f4507..42367bf13f7 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -41,6 +41,9 @@ future GitLab releases.** | **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. | | **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built | | **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. | +| **CI_COMMIT_MESSAGE** | 10.8 | all | The full commit message. | +| **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message | +| **CI_COMMIT_DESCRIPTION** | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. | | **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` | | **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled | | **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. | diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 5a67414e835..6d3796e7560 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -1,5 +1,8 @@ # Frontend Development Guidelines +> **Notice:** +We are currently in the process of re-writing our development guide to make it easier to find information. The new guide is still WIP but viewable in [development/new_fe_guide](../new_fe_guide/index.md) + This document describes various guidelines to ensure consistency and quality across GitLab's frontend team. diff --git a/doc/update/10.7-to-10.8.md b/doc/update/10.7-to-10.8.md new file mode 100644 index 00000000000..13101a987f4 --- /dev/null +++ b/doc/update/10.7-to-10.8.md @@ -0,0 +1,362 @@ +--- +comments: false +--- + +# From 10.7 to 10.8 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +NOTE: If you installed GitLab from source, make sure `rsync` is installed. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download Ruby and compile it: + + ```bash + mkdir /tmp/ruby && cd /tmp/ruby + curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.7.tar.gz + echo '540996fec64984ab6099e34d2f5820b14904f15a ruby-2.3.7.tar.gz' | shasum -c - && tar xzf ruby-2.3.7.tar.gz + cd ruby-2.3.7 + + ./configure --disable-install-rdoc + make + sudo make install + ``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets. +This requires a minimum version of node v6.0.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v6.0.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + +GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript +dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go +1.5.x through 1.7.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.8.3.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all --prune +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-8-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-8-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 8. Update gitlab-workhorse + +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 9. Update Gitaly + +#### New Gitaly configuration options required + +In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`. + +```shell +echo ' +[gitaly-ruby] +dir = "/home/git/gitaly/ruby" + +[gitlab-shell] +dir = "/home/git/gitlab-shell" +' | sudo -u git tee -a /home/git/gitaly/config.toml +``` + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" +``` + +If you use MySQL with replication, or just have MySQL configured with binary logging, +you will need to also run the following on all of your MySQL servers: + +```bash +mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;" +``` + +You can make this setting permanent by adding it to your `my.cnf`: + +``` +log_bin_trust_function_creators=1 +``` + +### 11. Update configuration files + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/10-7-stable:config/gitlab.yml.example origin/10-8-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/10-7-stable:lib/support/nginx/gitlab-ssl origin/10-8-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/10-7-stable:lib/support/nginx/gitlab origin/10-8-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-8-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-8-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/10-7-stable:lib/support/init.d/gitlab.default.example origin/10-8-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 12. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 13. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 14. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (10.7) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 10.6 to 10.7](10.6-to-10.7.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-8-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-8-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md index fea3231006b..557487e1a75 100644 --- a/doc/user/project/integrations/prometheus_library/nginx.md +++ b/doc/user/project/integrations/prometheus_library/nginx.md @@ -10,17 +10,19 @@ The [Prometheus service](../prometheus.md) must be enabled. ## Metrics supported +NGINX server metrics are detected, which tracks the pages and content directly served by NGINX. + | Name | Query | | ---- | ----- | -| Throughput (req/sec) | sum(rate(nginx_responses_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (status_code) | -| Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) | -| HTTP Error Rate (HTTP Errors / sec) | rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) | +| Throughput (req/sec) | sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code) | +| Latency (ms) | avg(nginx_server_requestMsec{%{environment_filter}}) | +| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m])) | ## Configuring Prometheus to monitor for NGINX metrics To get started with NGINX monitoring, you should first enable the [VTS statistics](https://github.com/vozlt/nginx-module-vts)) module for your NGINX server. This will capture and display statistics in an HTML readable form. Next, you should install and configure the [NGINX VTS exporter](https://github.com/hnlq715/nginx-vts-exporter) which parses these statistics and translates them into a Prometheus monitoring endpoint. -If you are using NGINX as your Kubernetes ingress, there is [upcoming direct support](https://github.com/kubernetes/ingress/pull/423) for enabling Prometheus monitoring in the 0.9.0 release. +If you are using NGINX as your Kubernetes ingress, GitLab will [automatically detect](nginx_ingress.md) the metrics once enabled in 0.9.0 and later releases. ## Specifying the Environment label diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md new file mode 100644 index 00000000000..dbe63144e38 --- /dev/null +++ b/doc/workflow/repository_mirroring.md @@ -0,0 +1,111 @@ +# Repository mirroring + +Repository Mirroring is a way to mirror repositories from external sources. +It can be used to mirror all branches, tags, and commits that you have +in your repository. + +Your mirror at GitLab will be updated automatically. You can +also manually trigger an update at most once every 5 minutes. + +## Overview + +Repository mirroring is very useful when, for some reason, you must use a +project from another source. + +There are two kinds of repository mirroring features supported by GitLab: +**push** and **pull**, the latter being only available in GitLab Enterprise Edition. +The **push** method mirrors the repository in GitLab to another location. + +Once the mirror repository is updated, all new branches, +tags, and commits will be visible in the project's activity feed. +Users with at least [developer access][perms] to the project can also force an +immediate update with the click of a button. This button will not be available if +the mirror is already being updated or 5 minutes still haven't passed since its last update. + +A few things/limitations to consider: + +- The repository must be accessible over `http://`, `https://`, `ssh://` or `git://`. +- If your HTTP repository is not publicly accessible, add authentication + information to the URL, like: `https://username@gitlab.company.com/group/project.git`. + In some cases, you might need to use a personal access token instead of a + password, e.g., you want to mirror to GitHub and have 2FA enabled. +- The import will time out after 15 minutes. For repositories that take longer + use a clone/push combination. +- The Git LFS objects will not be synced. You'll need to push/pull them + manually. + +## Use-case + +- You have old projects in another source that you don't use actively anymore, + but don't want to remove for archiving purposes. In that case, you can create + a push mirror so that your active GitLab repository can push its changes to the + old location. + +## Pushing to a remote repository **[STARTER]** + +>[Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in +GitLab Enterprise Edition 8.7. [Moved to GitLab Community Edition][ce-18715] in 10.8. + +For an existing project, you can set up push mirror from your project's +**Settings âž” Repository** and searching for the "Push to a remote repository" +section. Check the "Remote mirror repository" box and fill in the Git URL of +the repository to push to. Click **Save changes** for the changes to take +effect. + + + +When push mirroring is enabled, you are advised not to push commits directly +to the mirrored repository to prevent the mirror diverging. +All changes will end up in the mirrored repository whenever commits +are pushed to GitLab, or when a [forced update](#forcing-an-update) is +initiated. + +Pushes into GitLab are automatically pushed to the remote mirror at least once +every 5 minutes after they are received or once every minute if **push only +protected branches** is enabled. + +In case of a diverged branch, you will see an error indicated at the **Mirror +repository** settings. + + + +### Push only protected branches + +>[Introduced][ee-3350] in GitLab Enterprise Edition 10.3. [Moved to GitLab Community Edition][ce-18715] in 10.8. + +You can choose to only push your protected branches from GitLab to your remote repository. + +To use this option go to your project's repository settings page under push mirror. + +## Setting up a push mirror from GitLab to GitHub + +To set up a mirror from GitLab to GitHub, you need to follow these steps: + +1. Create a [GitHub personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) with the "public_repo" box checked: + +  + +1. Fill in the "Git repository URL" with the personal access token replacing the password `https://GitHubUsername:GitHubPersonalAccessToken@github.com/group/project.git`: + +  + +1. Save +1. And either wait or trigger the "Update Now" button: + +  + +## Forcing an update + +While mirrors are scheduled to update automatically, you can always force an update +by using the **Update now** button which is exposed in various places: + +- in the commits page +- in the branches page +- in the tags page +- in the **Mirror repository** settings page + +[ee-3350]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350 +[ce-18715]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715 +[perms]: ../user/permissions.md + diff --git a/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch_push.png b/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch_push.png Binary files differnew file mode 100644 index 00000000000..038b05cb31d --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch_push.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_github_edit_personal_access_token.png b/doc/workflow/repository_mirroring/repository_mirroring_github_edit_personal_access_token.png Binary files differnew file mode 100644 index 00000000000..139de42d8db --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_github_edit_personal_access_token.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.png b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.png Binary files differnew file mode 100644 index 00000000000..ccbc1d92329 --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png Binary files differnew file mode 100644 index 00000000000..b16b3d2828e --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_push_settings.png b/doc/workflow/repository_mirroring/repository_mirroring_push_settings.png Binary files differnew file mode 100644 index 00000000000..f8199aa7c0f --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_push_settings.png diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb index 9db31522c5c..8e2f594328d 100644 --- a/features/steps/project/deploy_keys.rb +++ b/features/steps/project/deploy_keys.rb @@ -9,18 +9,21 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps step 'I should see project deploy key' do page.within(find('.deploy-keys')) do + find('.js-deployKeys-tab-enabled_keys').click() expect(page).to have_content deploy_key.title end end step 'I should see other project deploy key' do page.within(find('.deploy-keys')) do + find('.js-deployKeys-tab-available_project_keys').click() expect(page).to have_content other_deploy_key.title end end step 'I should see public deploy key' do page.within(find('.deploy-keys')) do + find('.js-deployKeys-tab-public_keys').click() expect(page).to have_content public_deploy_key.title end end @@ -42,6 +45,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps step 'I should see newly created deploy key' do @project.reload page.within(find('.deploy-keys')) do + find('.js-deployKeys-tab-enabled_keys').click() expect(page).to have_content(deploy_key.title) end end @@ -58,7 +62,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps step 'I should only see the same deploy key once' do page.within(find('.deploy-keys')) do - expect(page).to have_selector('ul li', count: 1) + expect(find('.js-deployKeys-tab-available_project_keys .badge')).to have_content('1') end end @@ -68,6 +72,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps step 'I click attach deploy key' do page.within(find('.deploy-keys')) do + find('.badge', text: '1').click() click_button 'Enable' expect(page).not_to have_selector('.fa-spinner') end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 0d1c4f73c6e..21ac7f7e0b6 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -106,6 +106,7 @@ excluded_attributes: - :last_repository_updated_at - :last_repository_check_at - :storage_version + - :remote_mirror_available_overridden - :description_html snippets: - :expired_at diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 8c0a4d55ea2..e294f3c4ebc 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -71,6 +71,7 @@ module Gitlab projects_imported_from_github: Project.where(import_type: 'github').count, protected_branches: ProtectedBranch.count, releases: Release.count, + remote_mirrors: RemoteMirror.count, snippets: Snippet.count, todos: Todo.count, uploads: Upload.count, diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/webpack/dev_server_middleware.rb index 6aecf63231f..b9a75eaac63 100644 --- a/lib/gitlab/middleware/webpack_proxy.rb +++ b/lib/gitlab/webpack/dev_server_middleware.rb @@ -3,8 +3,8 @@ # :nocov: module Gitlab - module Middleware - class WebpackProxy < Rack::Proxy + module Webpack + class DevServerMiddleware < Rack::Proxy def initialize(app = nil, opts = {}) @proxy_host = opts.fetch(:proxy_host, 'localhost') @proxy_port = opts.fetch(:proxy_port, 3808) diff --git a/lib/gitlab/webpack/manifest.rb b/lib/gitlab/webpack/manifest.rb new file mode 100644 index 00000000000..0c343e5bc1d --- /dev/null +++ b/lib/gitlab/webpack/manifest.rb @@ -0,0 +1,27 @@ +require 'webpack/rails/manifest' + +module Gitlab + module Webpack + class Manifest < ::Webpack::Rails::Manifest + # Raised if a supplied asset does not exist in the webpack manifest + AssetMissingError = Class.new(StandardError) + + class << self + def entrypoint_paths(source) + raise ::Webpack::Rails::Manifest::WebpackError, manifest["errors"] unless manifest_bundled? + + entrypoint = manifest["entrypoints"][source] + if entrypoint && entrypoint["assets"] + # Can be either a string or an array of strings. + # Do not include source maps as they are not javascript + [entrypoint["assets"]].flatten.reject { |p| p =~ /.*\.map$/ }.map do |p| + "/#{::Rails.configuration.webpack.public_path}/#{p}" + end + else + raise AssetMissingError, "Can't find entry point '#{source}' in webpack manifest" + end + end + end + end + end +end diff --git a/package.json b/package.json index c2137ab8316..4a1a594454c 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,11 @@ "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, "dependencies": { - "@gitlab-org/gitlab-svgs": "^1.18.0", + "@gitlab-org/gitlab-svgs": "^1.20.0", "autosize": "^4.0.0", "axios": "^0.17.1", - "babel-core": "^6.26.0", - "babel-loader": "^7.1.2", + "babel-core": "^6.26.3", + "babel-loader": "^7.1.4", "babel-plugin-transform-define": "^1.3.0", "babel-preset-latest": "^6.24.1", "babel-preset-stage-2": "^6.24.1", @@ -30,11 +30,11 @@ "chart.js": "1.0.2", "classlist-polyfill": "^1.2.0", "clipboard": "^1.7.1", - "compression-webpack-plugin": "^1.1.7", - "copy-webpack-plugin": "^4.4.1", + "compression-webpack-plugin": "^1.1.11", + "copy-webpack-plugin": "^4.5.1", "core-js": "^2.4.1", "cropper": "^2.3.0", - "css-loader": "^0.28.9", + "css-loader": "^0.28.11", "d3-array": "^1.2.1", "d3-axis": "^1.0.8", "d3-brush": "^1.0.4", @@ -49,7 +49,7 @@ "dropzone": "^4.2.0", "emoji-unicode-version": "^0.2.1", "exports-loader": "^0.7.0", - "file-loader": "^1.1.8", + "file-loader": "^1.1.11", "fuzzaldrin-plus": "^0.5.0", "glob": "^7.1.2", "imports-loader": "^0.8.0", @@ -64,24 +64,22 @@ "marked": "^0.3.12", "monaco-editor": "0.10.0", "mousetrap": "^1.4.6", - "name-all-modules-plugin": "^1.0.1", "pikaday": "^1.6.1", "prismjs": "^1.6.0", "raphael": "^2.2.7", "raven-js": "^3.22.1", "raw-loader": "^0.5.1", - "react-dev-utils": "^5.0.0", "sanitize-html": "^1.16.1", "select2": "3.5.2-browserify", "sql.js": "^0.4.0", - "style-loader": "^0.20.2", + "style-loader": "^0.21.0", "svg4everybody": "2.1.9", "three": "^0.84.0", "three-orbit-controls": "^82.1.0", "three-stl-loader": "^1.0.4", "timeago.js": "^3.0.2", "underscore": "^1.9.0", - "url-loader": "^0.6.2", + "url-loader": "^1.0.1", "visibilityjs": "^1.2.4", "vue": "^2.5.16", "vue-loader": "^14.1.1", @@ -90,10 +88,11 @@ "vue-template-compiler": "^2.5.16", "vue-virtual-scroll-list": "^1.2.5", "vuex": "^3.0.1", - "webpack": "^3.11.0", - "webpack-bundle-analyzer": "^2.10.0", - "webpack-stats-plugin": "^0.1.5", - "worker-loader": "^1.1.0" + "webpack": "^4.7.0", + "webpack-bundle-analyzer": "^2.11.1", + "webpack-cli": "^2.1.2", + "webpack-stats-plugin": "^0.2.1", + "worker-loader": "^1.1.1" }, "devDependencies": { "axios-mock-adapter": "^1.10.0", @@ -124,8 +123,8 @@ "karma-mocha-reporter": "^2.2.5", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "3.0.0", - "nodemon": "^1.15.1", + "nodemon": "^1.17.3", "prettier": "1.11.1", - "webpack-dev-server": "^2.11.2" + "webpack-dev-server": "^3.1.4" } } diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb new file mode 100644 index 00000000000..6d31b0ce959 --- /dev/null +++ b/spec/controllers/groups/runners_controller_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe Groups::RunnersController do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:runner) { create(:ci_runner) } + + let(:params) do + { + group_id: group, + id: runner + } + end + + before do + sign_in(user) + group.add_master(user) + group.runners << runner + end + + describe '#update' do + it 'updates the runner and ticks the queue' do + new_desc = runner.description.swapcase + + expect do + post :update, params.merge(runner: { description: new_desc } ) + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_gitlab_http_status(302) + expect(runner.description).to eq(new_desc) + end + end + + describe '#destroy' do + it 'destroys the runner' do + delete :destroy, params + + expect(response).to have_gitlab_http_status(302) + expect(Ci::Runner.find_by(id: runner.id)).to be_nil + end + end + + describe '#resume' do + it 'marks the runner as active and ticks the queue' do + runner.update(active: false) + + expect do + post :resume, params + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_gitlab_http_status(302) + expect(runner.active).to eq(true) + end + end + + describe '#pause' do + it 'marks the runner as inactive and ticks the queue' do + runner.update(active: true) + + expect do + post :pause, params + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_gitlab_http_status(302) + expect(runner.active).to eq(false) + end + end +end diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb new file mode 100644 index 00000000000..45c1218a39c --- /dev/null +++ b/spec/controllers/projects/mirrors_controller_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe Projects::MirrorsController do + include ReactiveCachingHelpers + + describe 'setting up a remote mirror' do + set(:project) { create(:project, :repository) } + + context 'when the current project is not a mirror' do + it 'allows to create a remote mirror' do + sign_in(project.owner) + + expect do + do_put(project, remote_mirrors_attributes: { '0' => { 'enabled' => 1, 'url' => 'http://foo.com' } }) + end.to change { RemoteMirror.count }.to(1) + end + end + end + + describe '#update' do + let(:project) { create(:project, :repository, :remote_mirror) } + + before do + sign_in(project.owner) + end + + around do |example| + Sidekiq::Testing.fake! { example.run } + end + + context 'With valid URL for a push' do + let(:remote_mirror_attributes) do + { "0" => { "enabled" => "0", url: 'https://updated.example.com' } } + end + + it 'processes a successful update' do + do_put(project, remote_mirrors_attributes: remote_mirror_attributes) + + expect(response).to redirect_to(project_settings_repository_path(project)) + expect(flash[:notice]).to match(/successfully updated/) + end + + it 'should create a RemoteMirror object' do + expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.to change(RemoteMirror, :count).by(1) + end + end + + context 'With invalid URL for a push' do + let(:remote_mirror_attributes) do + { "0" => { "enabled" => "0", url: 'ftp://invalid.invalid' } } + end + + it 'processes an unsuccessful update' do + do_put(project, remote_mirrors_attributes: remote_mirror_attributes) + + expect(response).to redirect_to(project_settings_repository_path(project)) + expect(flash[:alert]).to match(/must be a valid URL/) + end + + it 'should not create a RemoteMirror object' do + expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.not_to change(RemoteMirror, :count) + end + end + end + + def do_put(project, options, extra_attrs = {}) + attrs = extra_attrs.merge(namespace_id: project.namespace.to_param, project_id: project.to_param) + attrs[:project] = options + + put :update, attrs + end +end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 9ab57af1c60..d129815aeac 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -183,6 +183,17 @@ FactoryBot.define do end end + trait :remote_mirror do + transient do + remote_name "remote_mirror_#{SecureRandom.hex}" + url "http://foo.com" + enabled true + end + after(:create) do |project, evaluator| + project.remote_mirrors.create!(url: evaluator.url, enabled: evaluator.enabled) + end + end + trait :stubbed_repository do after(:build) do |project| allow(project).to receive(:empty_repo?).and_return(false) diff --git a/spec/factories/remote_mirrors.rb b/spec/factories/remote_mirrors.rb new file mode 100644 index 00000000000..adc7da27522 --- /dev/null +++ b/spec/factories/remote_mirrors.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :remote_mirror, class: 'RemoteMirror' do + association :project, :repository + url "http://foo:bar@test.com" + end +end diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index dfe8e02dce0..fe334b531f0 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -184,4 +184,44 @@ feature 'Gcp Cluster', :js do expect(page).to have_css('.signin-with-google') end end + + context 'when user has not dismissed GCP signup offer' do + before do + visit project_clusters_path(project) + end + + it 'user sees offer on cluster index page' do + expect(page).to have_css('.gcp-signup-offer') + end + + it 'user sees offer on cluster create page' do + click_link 'Add Kubernetes cluster' + + expect(page).to have_css('.gcp-signup-offer') + end + + it 'user sees offer on cluster GCP login page' do + click_link 'Add Kubernetes cluster' + click_link 'Create on Google Kubernetes Engine' + + expect(page).to have_css('.gcp-signup-offer') + end + end + + context 'when user has dismissed GCP signup offer' do + before do + visit project_clusters_path(project) + end + + it 'user does not see offer after dismissing' do + expect(page).to have_css('.gcp-signup-offer') + + find('.gcp-signup-offer .close').click + wait_for_requests + + click_link 'Add Kubernetes cluster' + + expect(page).not_to have_css('.gcp-signup-offer') + end + end end diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb index 886c56e7163..43a23c42f83 100644 --- a/spec/features/projects/deploy_keys_spec.rb +++ b/spec/features/projects/deploy_keys_spec.rb @@ -18,12 +18,12 @@ describe 'Project deploy keys', :js do visit project_settings_repository_path(project) page.within(find('.deploy-keys')) do - expect(page).to have_selector('.deploy-keys li', count: 1) + expect(page).to have_selector('.deploy-key', count: 1) - accept_confirm { find(:button, text: 'Remove').send_keys(:return) } + accept_confirm { find('.ic-remove').click() } expect(page).not_to have_selector('.fa-spinner', count: 0) - expect(page).to have_selector('.deploy-keys li', count: 0) + expect(page).to have_selector('.deploy-key', count: 0) end end end diff --git a/spec/features/projects/remote_mirror_spec.rb b/spec/features/projects/remote_mirror_spec.rb new file mode 100644 index 00000000000..81a6b613cc8 --- /dev/null +++ b/spec/features/projects/remote_mirror_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +feature 'Project remote mirror', :feature do + let(:project) { create(:project, :repository, :remote_mirror) } + let(:remote_mirror) { project.remote_mirrors.first } + let(:user) { create(:user) } + + describe 'On a project', :js do + before do + project.add_master(user) + sign_in user + end + + context 'when last_error is present but last_update_at is not' do + it 'renders error message without timstamp' do + remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: nil) + + visit project_mirror_path(project) + + expect(page).to have_content('The remote repository failed to update.') + end + end + + context 'when last_error and last_update_at are present' do + it 'renders error message with timestamp' do + remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: Time.now - 5.minutes) + + visit project_mirror_path(project) + + expect(page).to have_content('The remote repository failed to update 5 minutes ago.') + end + end + end +end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index e1dfe617691..08b40653764 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -54,7 +54,7 @@ describe 'Projects > Settings > Repository settings' do project.deploy_keys << private_deploy_key visit project_settings_repository_path(project) - find('li', text: private_deploy_key.title).click_link('Edit') + find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click() fill_in 'deploy_key_title', with: 'updated_deploy_key' check 'deploy_key_deploy_keys_projects_attributes_0_can_push' @@ -71,11 +71,15 @@ describe 'Projects > Settings > Repository settings' do visit project_settings_repository_path(project) - find('li', text: private_deploy_key.title).click_link('Edit') + find('.js-deployKeys-tab-available_project_keys').click() + + find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click() fill_in 'deploy_key_title', with: 'updated_deploy_key' click_button 'Save changes' + find('.js-deployKeys-tab-available_project_keys').click() + expect(page).to have_content('updated_deploy_key') end @@ -83,7 +87,7 @@ describe 'Projects > Settings > Repository settings' do project.deploy_keys << private_deploy_key visit project_settings_repository_path(project) - accept_confirm { find('li', text: private_deploy_key.title).click_button('Remove') } + accept_confirm { find('.deploy-key', text: private_deploy_key.title).find('.ic-remove').click() } expect(page).not_to have_content(private_deploy_key.title) end @@ -115,5 +119,20 @@ describe 'Projects > Settings > Repository settings' do expect(page).to have_content('Your new project deploy token has been created') end end + + context 'remote mirror settings' do + let(:user2) { create(:user) } + + before do + project.add_master(user2) + + visit project_settings_repository_path(project) + end + + it 'shows push mirror settings' do + expect(page).to have_selector('#project_remote_mirrors_attributes_0_enabled') + expect(page).to have_selector('#project_remote_mirrors_attributes_0_url') + end + end end end diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb index 74890c86047..a9e815eaf4f 100644 --- a/spec/features/raven_js_spec.rb +++ b/spec/features/raven_js_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' feature 'RavenJS' do - let(:raven_path) { '/raven.bundle.js' } + let(:raven_path) { '/raven.chunk.js' } it 'should not load raven if sentry is disabled' do visit new_user_session_path diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index b396e103345..e0cd963fe39 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -15,7 +15,7 @@ feature 'Runners' do end scenario 'user can see a button to install runners on kubernetes clusters' do - visit runners_path(project) + visit project_runners_path(project) expect(page).to have_link('Install Runner on Kubernetes', href: project_clusters_path(project)) end @@ -36,7 +36,7 @@ feature 'Runners' do end scenario 'user sees the specific runner' do - visit runners_path(project) + visit project_runners_path(project) within '.activated-specific-runners' do expect(page).to have_content(specific_runner.display_name) @@ -48,7 +48,7 @@ feature 'Runners' do end scenario 'user can pause and resume the specific runner' do - visit runners_path(project) + visit project_runners_path(project) within '.activated-specific-runners' do expect(page).to have_content('Pause') @@ -68,7 +68,7 @@ feature 'Runners' do end scenario 'user removes an activated specific runner if this is last project for that runners' do - visit runners_path(project) + visit project_runners_path(project) within '.activated-specific-runners' do click_on 'Remove Runner' @@ -78,7 +78,7 @@ feature 'Runners' do end scenario 'user edits the runner to be protected' do - visit runners_path(project) + visit project_runners_path(project) within '.activated-specific-runners' do first('.edit-runner > a').click @@ -98,7 +98,7 @@ feature 'Runners' do end scenario 'user edits runner not to run untagged jobs' do - visit runners_path(project) + visit project_runners_path(project) within '.activated-specific-runners' do first('.edit-runner > a').click @@ -117,7 +117,7 @@ feature 'Runners' do given!(:shared_runner) { create(:ci_runner, :shared) } scenario 'user sees CI/CD setting page' do - visit runners_path(project) + visit project_runners_path(project) expect(page.find('.available-shared-runners')).to have_content(shared_runner.display_name) end @@ -134,7 +134,7 @@ feature 'Runners' do end scenario 'user enables and disables a specific runner' do - visit runners_path(project) + visit project_runners_path(project) within '.available-specific-runners' do click_on 'Enable for this project' @@ -159,7 +159,7 @@ feature 'Runners' do end scenario 'user sees shared runners description' do - visit runners_path(project) + visit project_runners_path(project) expect(page.find('.shared-runners-description')).to have_content(shared_runners_html) end @@ -174,7 +174,7 @@ feature 'Runners' do end scenario 'user enables shared runners' do - visit runners_path(project) + visit project_runners_path(project) click_on 'Enable shared Runners' @@ -182,7 +182,7 @@ feature 'Runners' do end end - context 'group runners' do + context 'group runners in project settings' do background do project.add_master(user) end @@ -198,12 +198,12 @@ feature 'Runners' do given(:project) { create :project, group: group } scenario 'group runners are not available' do - visit runners_path(project) + visit project_runners_path(project) - expect(page).to have_content 'This group does not provide any group Runners yet.' + expect(page).to have_content 'This group does not provide any group Runners yet' - expect(page).to have_content 'Setup a group Runner manually' - expect(page).not_to have_content 'Ask your group master to setup a group Runner.' + expect(page).to have_content 'Group masters can register group runners in the Group CI/CD settings' + expect(page).not_to have_content 'Ask your group master to setup a group Runner' end end end @@ -213,7 +213,7 @@ feature 'Runners' do given(:project) { create :project } scenario 'group runners are not available' do - visit runners_path(project) + visit project_runners_path(project) expect(page).to have_content 'This project does not belong to a group and can therefore not make use of group Runners.' end @@ -224,11 +224,11 @@ feature 'Runners' do given(:project) { create :project, group: group } scenario 'group runners are not available' do - visit runners_path(project) + visit project_runners_path(project) expect(page).to have_content 'This group does not provide any group Runners yet.' - expect(page).not_to have_content 'Setup a group Runner manually' + expect(page).not_to have_content 'Group masters can register group runners in the Group CI/CD settings' expect(page).to have_content 'Ask your group master to setup a group Runner.' end end @@ -239,14 +239,14 @@ feature 'Runners' do given!(:ci_runner) { create :ci_runner, groups: [group], description: 'group-runner' } scenario 'group runners are available' do - visit runners_path(project) + visit project_runners_path(project) expect(page).to have_content 'Available group Runners : 1' expect(page).to have_content 'group-runner' end scenario 'group runners may be disabled for a project' do - visit runners_path(project) + visit project_runners_path(project) click_on 'Disable group Runners' @@ -261,4 +261,98 @@ feature 'Runners' do end end end + + context 'group runners in group settings' do + given(:group) { create :group } + background do + group.add_master(user) + end + + context 'group with no runners' do + scenario 'there are no runners displayed' do + visit group_settings_ci_cd_path(group) + + expect(page).to have_content 'This group does not provide any group Runners yet' + end + end + + context 'group with a runner' do + let!(:runner) { create :ci_runner, groups: [group], description: 'group-runner' } + + scenario 'the runner is visible' do + visit group_settings_ci_cd_path(group) + + expect(page).not_to have_content 'This group does not provide any group Runners yet' + expect(page).to have_content 'Available group Runners : 1' + expect(page).to have_content 'group-runner' + end + + scenario 'user can pause and resume the group runner' do + visit group_settings_ci_cd_path(group) + + expect(page).to have_content('Pause') + expect(page).not_to have_content('Resume') + + click_on 'Pause' + + expect(page).not_to have_content('Pause') + expect(page).to have_content('Resume') + + click_on 'Resume' + + expect(page).to have_content('Pause') + expect(page).not_to have_content('Resume') + end + + scenario 'user can view runner details' do + visit group_settings_ci_cd_path(group) + + expect(page).to have_content(runner.display_name) + + click_on runner.short_sha + + expect(page).to have_content(runner.platform) + end + + scenario 'user can remove a group runner' do + visit group_settings_ci_cd_path(group) + + click_on 'Remove Runner' + + expect(page).not_to have_content(runner.display_name) + end + + scenario 'user edits the runner to be protected' do + visit group_settings_ci_cd_path(group) + + first('.edit-runner > a').click + + expect(page.find_field('runner[access_level]')).not_to be_checked + + check 'runner_access_level' + click_button 'Save changes' + + expect(page).to have_content 'Protected Yes' + end + + context 'when a runner has a tag' do + background do + runner.update(tag_list: ['tag']) + end + + scenario 'user edits runner not to run untagged jobs' do + visit group_settings_ci_cd_path(group) + + first('.edit-runner > a').click + + expect(page.find_field('runner[run_untagged]')).to be_checked + + uncheck 'runner_run_untagged' + click_button 'Save changes' + + expect(page).to have_content 'Can run untagged jobs No' + end + end + end + end end diff --git a/spec/javascripts/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js index 7025c3d836c..5bf72cc0018 100644 --- a/spec/javascripts/deploy_keys/components/action_btn_spec.js +++ b/spec/javascripts/deploy_keys/components/action_btn_spec.js @@ -7,62 +7,64 @@ describe('Deploy keys action btn', () => { const deployKey = data.enabled_keys[0]; let vm; - beforeEach((done) => { - const ActionBtnComponent = Vue.extend(actionBtn); - - vm = new ActionBtnComponent({ - propsData: { - deployKey, - type: 'enable', + beforeEach(done => { + const ActionBtnComponent = Vue.extend({ + components: { + actionBtn, + }, + data() { + return { + deployKey, + }; }, - }).$mount(); + template: ` + <action-btn + :deploy-key="deployKey" + type="enable"> + Enable + </action-btn>`, + }); + + vm = new ActionBtnComponent().$mount(); - setTimeout(done); + Vue.nextTick() + .then(done) + .catch(done.fail); }); - it('renders the type as uppercase', () => { - expect( - vm.$el.textContent.trim(), - ).toBe('Enable'); + it('renders the default slot', () => { + expect(vm.$el.textContent.trim()).toBe('Enable'); }); - it('sends eventHub event with btn type', (done) => { + it('sends eventHub event with btn type', done => { spyOn(eventHub, '$emit'); vm.$el.click(); - setTimeout(() => { - expect( - eventHub.$emit, - ).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything()); + Vue.nextTick(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything()); done(); }); }); - it('shows loading spinner after click', (done) => { + it('shows loading spinner after click', done => { vm.$el.click(); - setTimeout(() => { - expect( - vm.$el.querySelector('.fa'), - ).toBeDefined(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.fa')).toBeDefined(); done(); }); }); - it('disables button after click', (done) => { + it('disables button after click', done => { vm.$el.click(); - setTimeout(() => { - expect( - vm.$el.classList.contains('disabled'), - ).toBeTruthy(); + Vue.nextTick(() => { + expect(vm.$el.classList.contains('disabled')).toBeTruthy(); - expect( - vm.$el.getAttribute('disabled'), - ).toBe('disabled'); + expect(vm.$el.getAttribute('disabled')).toBe('disabled'); done(); }); diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js index b870f87eab9..3f9e25a8862 100644 --- a/spec/javascripts/deploy_keys/components/app_spec.js +++ b/spec/javascripts/deploy_keys/components/app_spec.js @@ -8,12 +8,14 @@ describe('Deploy keys app component', () => { let vm; const deployKeysResponse = (request, next) => { - next(request.respondWith(JSON.stringify(data), { - status: 200, - })); + next( + request.respondWith(JSON.stringify(data), { + status: 200, + }), + ); }; - beforeEach((done) => { + beforeEach(done => { const Component = Vue.extend(deployKeysApp); Vue.http.interceptors.push(deployKeysResponse); @@ -21,6 +23,7 @@ describe('Deploy keys app component', () => { vm = new Component({ propsData: { endpoint: '/test', + projectId: '8', }, }).$mount(); @@ -31,117 +34,112 @@ describe('Deploy keys app component', () => { Vue.http.interceptors = _.without(Vue.http.interceptors, deployKeysResponse); }); - it('renders loading icon', (done) => { + it('renders loading icon', done => { vm.store.keys = {}; vm.isLoading = false; Vue.nextTick(() => { - expect( - vm.$el.querySelectorAll('.deploy-keys-panel').length, - ).toBe(0); + expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0); - expect( - vm.$el.querySelector('.fa-spinner'), - ).toBeDefined(); + expect(vm.$el.querySelector('.fa-spinner')).toBeDefined(); done(); }); }); it('renders keys panels', () => { - expect( - vm.$el.querySelectorAll('.deploy-keys-panel').length, - ).toBe(3); + expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(3); }); - it('does not render key panels when keys object is empty', (done) => { - vm.store.keys = {}; - - Vue.nextTick(() => { - expect( - vm.$el.querySelectorAll('.deploy-keys-panel').length, - ).toBe(0); - - done(); - }); + it('renders the titles with keys count', () => { + const textContent = selector => { + const element = vm.$el.querySelector(`${selector}`); + + expect(element).not.toBeNull(); + return element.textContent.trim(); + }; + + expect(textContent('.js-deployKeys-tab-enabled_keys')).toContain('Enabled deploy keys'); + expect(textContent('.js-deployKeys-tab-available_project_keys')).toContain( + 'Privately accessible deploy keys', + ); + expect(textContent('.js-deployKeys-tab-public_keys')).toContain( + 'Publicly accessible deploy keys', + ); + + expect(textContent('.js-deployKeys-tab-enabled_keys .badge')).toBe( + `${vm.store.keys.enabled_keys.length}`, + ); + expect(textContent('.js-deployKeys-tab-available_project_keys .badge')).toBe( + `${vm.store.keys.available_project_keys.length}`, + ); + expect(textContent('.js-deployKeys-tab-public_keys .badge')).toBe( + `${vm.store.keys.public_keys.length}`, + ); }); - it('does not render public panel when empty', (done) => { - vm.store.keys.public_keys = []; + it('does not render key panels when keys object is empty', done => { + vm.store.keys = {}; Vue.nextTick(() => { - expect( - vm.$el.querySelectorAll('.deploy-keys-panel').length, - ).toBe(2); + expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0); done(); }); }); - it('re-fetches deploy keys when enabling a key', (done) => { + it('re-fetches deploy keys when enabling a key', done => { const key = data.public_keys[0]; spyOn(vm.service, 'getKeys'); - spyOn(vm.service, 'enableKey').and.callFake(() => new Promise((resolve) => { - resolve(); - - setTimeout(() => { - expect(vm.service.getKeys).toHaveBeenCalled(); - - done(); - }); - })); + spyOn(vm.service, 'enableKey').and.callFake(() => Promise.resolve()); eventHub.$emit('enable.key', key); - expect(vm.service.enableKey).toHaveBeenCalledWith(key.id); + Vue.nextTick(() => { + expect(vm.service.enableKey).toHaveBeenCalledWith(key.id); + expect(vm.service.getKeys).toHaveBeenCalled(); + done(); + }); }); - it('re-fetches deploy keys when disabling a key', (done) => { + it('re-fetches deploy keys when disabling a key', done => { const key = data.public_keys[0]; spyOn(window, 'confirm').and.returnValue(true); spyOn(vm.service, 'getKeys'); - spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => { - resolve(); - - setTimeout(() => { - expect(vm.service.getKeys).toHaveBeenCalled(); - - done(); - }); - })); + spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve()); eventHub.$emit('disable.key', key); - expect(vm.service.disableKey).toHaveBeenCalledWith(key.id); + Vue.nextTick(() => { + expect(vm.service.disableKey).toHaveBeenCalledWith(key.id); + expect(vm.service.getKeys).toHaveBeenCalled(); + done(); + }); }); - it('calls disableKey when removing a key', (done) => { + it('calls disableKey when removing a key', done => { const key = data.public_keys[0]; spyOn(window, 'confirm').and.returnValue(true); spyOn(vm.service, 'getKeys'); - spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => { - resolve(); - - setTimeout(() => { - expect(vm.service.getKeys).toHaveBeenCalled(); - - done(); - }); - })); + spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve()); eventHub.$emit('remove.key', key); - expect(vm.service.disableKey).toHaveBeenCalledWith(key.id); + Vue.nextTick(() => { + expect(vm.service.disableKey).toHaveBeenCalledWith(key.id); + expect(vm.service.getKeys).toHaveBeenCalled(); + done(); + }); }); it('hasKeys returns true when there are keys', () => { expect(vm.hasKeys).toEqual(3); }); - it('resets remove button loading state', (done) => { + it('resets disable button loading state', done => { spyOn(window, 'confirm').and.returnValue(false); const btn = vm.$el.querySelector('.btn-warning'); @@ -149,7 +147,7 @@ describe('Deploy keys app component', () => { btn.click(); Vue.nextTick(() => { - expect(btn.querySelector('.fa')).toBeNull(); + expect(btn.querySelector('.btn-warning')).not.toExist(); done(); }); diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index b7aadf604a4..4279add21d1 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -7,7 +7,7 @@ describe('Deploy keys key', () => { let vm; const KeyComponent = Vue.extend(key); const data = getJSONFixture('deploy_keys/keys.json'); - const createComponent = (deployKey) => { + const createComponent = deployKey => { const store = new DeployKeysStore(); store.keys = data; @@ -23,37 +23,42 @@ describe('Deploy keys key', () => { describe('enabled key', () => { const deployKey = data.enabled_keys[0]; - beforeEach((done) => { + beforeEach(done => { createComponent(deployKey); setTimeout(done); }); it('renders the keys title', () => { - expect( - vm.$el.querySelector('.title').textContent.trim(), - ).toContain('My title'); + expect(vm.$el.querySelector('.title').textContent.trim()).toContain('My title'); }); it('renders human friendly formatted created date', () => { - expect( - vm.$el.querySelector('.key-created-at').textContent.trim(), - ).toBe(`created ${getTimeago().format(deployKey.created_at)}`); + expect(vm.$el.querySelector('.key-created-at').textContent.trim()).toBe( + `${getTimeago().format(deployKey.created_at)}`, + ); }); - it('shows edit button', () => { - expect( - vm.$el.querySelectorAll('.btn')[0].textContent.trim(), - ).toBe('Edit'); + it('shows pencil button for editing', () => { + expect(vm.$el.querySelector('.btn .ic-pencil')).toExist(); }); - it('shows remove button', () => { - expect( - vm.$el.querySelectorAll('.btn')[1].textContent.trim(), - ).toBe('Remove'); + it('shows disable button when the project is not deletable', () => { + expect(vm.$el.querySelector('.btn .ic-cancel')).toExist(); }); - it('shows write access title when key has write access', (done) => { + it('shows remove button when the project is deletable', done => { + vm.deployKey.destroyed_when_orphaned = true; + vm.deployKey.almost_orphaned = true; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn .ic-remove')).toExist(); + done(); + }); + }); + }); + + describe('deploy key labels', () => { + it('shows write access title when key has write access', done => { vm.deployKey.deploy_keys_projects[0].can_push = true; Vue.nextTick(() => { @@ -64,7 +69,7 @@ describe('Deploy keys key', () => { }); }); - it('does not show write access title when key has write access', (done) => { + it('does not show write access title when key has write access', done => { vm.deployKey.deploy_keys_projects[0].can_push = false; Vue.nextTick(() => { @@ -74,36 +79,73 @@ describe('Deploy keys key', () => { done(); }); }); + + it('shows expandable button if more than two projects', () => { + const labels = vm.$el.querySelectorAll('.deploy-project-label'); + expect(labels.length).toBe(2); + expect(labels[1].textContent).toContain('others'); + expect(labels[1].getAttribute('data-original-title')).toContain('Expand'); + }); + + it('expands all project labels after click', done => { + const length = vm.deployKey.deploy_keys_projects.length; + vm.$el.querySelectorAll('.deploy-project-label')[1].click(); + + Vue.nextTick(() => { + const labels = vm.$el.querySelectorAll('.deploy-project-label'); + expect(labels.length).toBe(length); + expect(labels[1].textContent).not.toContain(`+${length} others`); + expect(labels[1].getAttribute('data-original-title')).not.toContain('Expand'); + done(); + }); + }); + + it('shows two projects', done => { + vm.deployKey.deploy_keys_projects = [...vm.deployKey.deploy_keys_projects].slice(0, 2); + + Vue.nextTick(() => { + const labels = vm.$el.querySelectorAll('.deploy-project-label'); + expect(labels.length).toBe(2); + expect(labels[1].textContent).toContain( + vm.deployKey.deploy_keys_projects[1].project.full_name, + ); + done(); + }); + }); }); describe('public keys', () => { const deployKey = data.public_keys[0]; - beforeEach((done) => { + beforeEach(done => { createComponent(deployKey); setTimeout(done); }); - it('shows edit button', () => { - expect( - vm.$el.querySelectorAll('.btn')[0].textContent.trim(), - ).toBe('Edit'); + it('renders deploy keys without any enabled projects', done => { + vm.deployKey.deploy_keys_projects = []; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.deploy-project-list').textContent.trim()).toBe('None'); + + done(); + }); }); it('shows enable button', () => { - expect( - vm.$el.querySelectorAll('.btn')[1].textContent.trim(), - ).toBe('Enable'); + expect(vm.$el.querySelectorAll('.btn')[0].textContent.trim()).toBe('Enable'); }); - it('shows disable button when key is enabled', (done) => { + it('shows pencil button for editing', () => { + expect(vm.$el.querySelector('.btn .ic-pencil')).toExist(); + }); + + it('shows disable button when key is enabled', done => { vm.store.keys.enabled_keys.push(deployKey); Vue.nextTick(() => { - expect( - vm.$el.querySelectorAll('.btn')[1].textContent.trim(), - ).toBe('Disable'); + expect(vm.$el.querySelector('.btn .ic-cancel')).toExist(); done(); }); diff --git a/spec/javascripts/deploy_keys/components/keys_panel_spec.js b/spec/javascripts/deploy_keys/components/keys_panel_spec.js index 08357d2b547..f71f5ccf082 100644 --- a/spec/javascripts/deploy_keys/components/keys_panel_spec.js +++ b/spec/javascripts/deploy_keys/components/keys_panel_spec.js @@ -6,7 +6,7 @@ describe('Deploy keys panel', () => { const data = getJSONFixture('deploy_keys/keys.json'); let vm; - beforeEach((done) => { + beforeEach(done => { const DeployKeysPanelComponent = Vue.extend(deployKeysPanel); const store = new DeployKeysStore(); store.keys = data; @@ -24,46 +24,38 @@ describe('Deploy keys panel', () => { setTimeout(done); }); - it('renders the title with keys count', () => { - expect( - vm.$el.querySelector('h5').textContent.trim(), - ).toContain('test'); - - expect( - vm.$el.querySelector('h5').textContent.trim(), - ).toContain(`(${vm.keys.length})`); + it('renders list of keys', () => { + expect(vm.$el.querySelectorAll('.deploy-key').length).toBe(vm.keys.length); }); - it('renders list of keys', () => { - expect( - vm.$el.querySelectorAll('li').length, - ).toBe(vm.keys.length); + it('renders table header', () => { + const tableHeader = vm.$el.querySelector('.table-row-header'); + + expect(tableHeader).toExist(); + expect(tableHeader.textContent).toContain('Deploy key'); + expect(tableHeader.textContent).toContain('Project usage'); + expect(tableHeader.textContent).toContain('Created'); }); - it('renders help box if keys are empty', (done) => { + it('renders help box if keys are empty', done => { vm.keys = []; Vue.nextTick(() => { - expect( - vm.$el.querySelector('.settings-message'), - ).toBeDefined(); + expect(vm.$el.querySelector('.settings-message')).toBeDefined(); - expect( - vm.$el.querySelector('.settings-message').textContent.trim(), - ).toBe('No deploy keys found. Create one with the form above.'); + expect(vm.$el.querySelector('.settings-message').textContent.trim()).toBe( + 'No deploy keys found. Create one with the form above.', + ); done(); }); }); - it('does not render help box if keys are empty & showHelpBox is false', (done) => { + it('renders no table header if keys are empty', done => { vm.keys = []; - vm.showHelpBox = false; Vue.nextTick(() => { - expect( - vm.$el.querySelector('.settings-message'), - ).toBeNull(); + expect(vm.$el.querySelector('.table-row-header')).not.toExist(); done(); }); diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb index 580894ceaf9..24699c3043a 100644 --- a/spec/javascripts/fixtures/deploy_keys.rb +++ b/spec/javascripts/fixtures/deploy_keys.rb @@ -7,6 +7,8 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') } let(:project2) { create(:project, :internal)} + let(:project3) { create(:project, :internal)} + let(:project4) { create(:project, :internal)} before(:all) do clean_frontend_fixtures('deploy_keys/') @@ -28,6 +30,8 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com') create(:deploy_keys_project, project: project, deploy_key: project_key) create(:deploy_keys_project, project: project2, deploy_key: internal_key) + create(:deploy_keys_project, project: project3, deploy_key: project_key) + create(:deploy_keys_project, project: project4, deploy_key: project_key) get :index, namespace_id: project.namespace.to_param, diff --git a/spec/javascripts/ide/components/ide_status_bar_spec.js b/spec/javascripts/ide/components/ide_status_bar_spec.js new file mode 100644 index 00000000000..770dca9cb0f --- /dev/null +++ b/spec/javascripts/ide/components/ide_status_bar_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import ideStatusBar from '~/ide/components/ide_status_bar.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { resetStore } from '../helpers'; +import { projectData } from '../mock_data'; + +describe('ideStatusBar', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(ideStatusBar); + + store.state.currentProjectId = 'abcproject'; + store.state.projects.abcproject = projectData; + + vm = createComponentWithStore(Component, store).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders the statusbar', () => { + expect(vm.$el.className).toBe('ide-status-bar'); + }); + + describe('mounted', () => { + it('triggers a setInterval', () => { + expect(vm.intervalId).not.toBe(null); + }); + }); + + describe('commitAgeUpdate', () => { + beforeEach(function() { + jasmine.clock().install(); + spyOn(vm, 'commitAgeUpdate').and.callFake(() => {}); + vm.startTimer(); + }); + + afterEach(function() { + jasmine.clock().uninstall(); + }); + + it('gets called every second', () => { + expect(vm.commitAgeUpdate).not.toHaveBeenCalled(); + + jasmine.clock().tick(1100); + expect(vm.commitAgeUpdate.calls.count()).toEqual(1); + + jasmine.clock().tick(1000); + expect(vm.commitAgeUpdate.calls.count()).toEqual(2); + }); + }); + + describe('getCommitPath', () => { + it('returns the path to the commit details', () => { + expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de'); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js new file mode 100644 index 00000000000..ebd08d95810 --- /dev/null +++ b/spec/javascripts/ide/stores/actions/project_spec.js @@ -0,0 +1,71 @@ +import { + refreshLastCommitData, +} from '~/ide/stores/actions'; +import store from '~/ide/stores'; +import service from '~/ide/services'; +import { resetStore } from '../../helpers'; +import testAction from '../../../helpers/vuex_action_helper'; + +describe('IDE store project actions', () => { + beforeEach(() => { + store.state.projects.abcproject = {}; + }); + + afterEach(() => { + resetStore(store); + }); + + describe('refreshLastCommitData', () => { + beforeEach(() => { + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = { + branches: { + master: { + commit: null, + }, + }, + }; + }); + + it('calls the service', done => { + spyOn(service, 'getBranchData').and.returnValue( + Promise.resolve({ + data: { + commit: { id: '123' }, + }, + }), + ); + + store + .dispatch('refreshLastCommitData', { + projectId: store.state.currentProjectId, + branchId: store.state.currentBranchId, + }) + .then(() => { + expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master'); + + done(); + }) + .catch(done.fail); + }); + + it('commits getBranchData', done => { + testAction( + refreshLastCommitData, + {}, + {}, + [{ + type: 'SET_BRANCH_COMMIT', + payload: { + projectId: 'abcproject', + branchId: 'master', + commit: { id: '123' }, + }, + }], // mutations + [], // action + done, + ); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js index 67a848e8edd..4833ba3edfd 100644 --- a/spec/javascripts/ide/stores/getters_spec.js +++ b/spec/javascripts/ide/stores/getters_spec.js @@ -141,4 +141,24 @@ describe('IDE store getters', () => { expect(getters.getChangesInFolder(localState)('test')).toBe(2); }); }); + + describe('lastCommit', () => { + it('returns the last commit of the current branch on the current project', () => { + const commitTitle = 'Example commit title'; + const localGetters = { + currentProject: { + branches: { + 'example-branch': { + commit: { + title: commitTitle, + }, + }, + }, + }, + }; + localState.currentBranchId = 'example-branch'; + + expect(getters.lastCommit(localState, localGetters).title).toBe(commitTitle); + }); + }); }); diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js index a7167537ef2..29eb859ddaf 100644 --- a/spec/javascripts/ide/stores/mutations/branch_spec.js +++ b/spec/javascripts/ide/stores/mutations/branch_spec.js @@ -15,4 +15,26 @@ describe('Multi-file store branch mutations', () => { expect(localState.currentBranchId).toBe('master'); }); }); + + describe('SET_BRANCH_COMMIT', () => { + it('sets the last commit on current project', () => { + localState.projects = { + Example: { + branches: { + master: {}, + }, + }, + }; + + mutations.SET_BRANCH_COMMIT(localState, { + projectId: 'Example', + branchId: 'master', + commit: { + title: 'Example commit', + }, + }); + + expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit'); + }); + }); }); diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index a08e0c93df9..ad76adcc2e5 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -268,6 +268,7 @@ project: - pages_domains - authorized_users - project_authorizations +- remote_mirrors - route - redirect_routes - statistics diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index c959add7a36..ad087f42e06 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::IncomingEmail do end describe 'self.supports_wildcard?' do - context 'address contains the wildard placeholder' do + context 'address contains the wildcard placeholder' do before do stub_incoming_email_setting(address: 'replies+%{key}@example.com') end @@ -49,7 +49,7 @@ describe Gitlab::IncomingEmail do stub_incoming_email_setting(address: nil) end - it 'returns that wildard is not supported' do + it 'returns that wildcard is not supported' do expect(described_class.supports_wildcard?).to be_falsey end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 9e6aa109a4b..a716e6f5434 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -96,6 +96,7 @@ describe Gitlab::UsageData do pages_domains protected_branches releases + remote_mirrors snippets todos uploads diff --git a/spec/migrations/cleanup_build_stage_migration_spec.rb b/spec/migrations/cleanup_build_stage_migration_spec.rb new file mode 100644 index 00000000000..4d4d02aaa94 --- /dev/null +++ b/spec/migrations/cleanup_build_stage_migration_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20180420010616_cleanup_build_stage_migration.rb') + +describe CleanupBuildStageMigration, :migration, :sidekiq, :redis do + let(:migration) { spy('migration') } + + before do + allow(Gitlab::BackgroundMigration::MigrateBuildStage) + .to receive(:new).and_return(migration) + end + + context 'when there are pending background migrations' do + it 'processes pending jobs synchronously' do + Sidekiq::Testing.disable! do + BackgroundMigrationWorker + .perform_in(2.minutes, 'MigrateBuildStage', [1, 1]) + BackgroundMigrationWorker + .perform_async('MigrateBuildStage', [1, 1]) + + migrate! + + expect(migration).to have_received(:perform).with(1, 1).twice + end + end + end + + context 'when there are no background migrations pending' do + it 'does nothing' do + Sidekiq::Testing.disable! do + migrate! + + expect(migration).not_to have_received(:perform) + end + end + end + + context 'when there are still unmigrated builds present' do + let(:builds) { table('ci_builds') } + + before do + builds.create!(name: 'test:1', ref: 'master') + builds.create!(name: 'test:2', ref: 'master') + end + + it 'migrates stages sequentially in batches' do + expect(builds.all).to all(have_attributes(stage_id: nil)) + + migrate! + + expect(migration).to have_received(:perform).once + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 3158e006720..dc810489011 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1518,7 +1518,10 @@ describe Ci::Build do { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true }, { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true }, - { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true } + { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true }, + { key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true }, + { key: 'CI_COMMIT_TITLE', value: pipeline.git_commit_title, public: true }, + { key: 'CI_COMMIT_DESCRIPTION', value: pipeline.git_commit_description, public: true } ] end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index dd94515b0a4..ddd66a6be87 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -173,7 +173,7 @@ describe Ci::Pipeline, :mailer do it 'includes all predefined variables in a valid order' do keys = subject.map { |variable| variable[:key] } - expect(keys).to eq %w[CI_PIPELINE_ID CI_CONFIG_PATH CI_PIPELINE_SOURCE] + expect(keys).to eq %w[CI_PIPELINE_ID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION] end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f3cf21cf279..41622fbbb6f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1852,6 +1852,85 @@ describe Project do it { expect(project.gitea_import?).to be true } end + describe '#has_remote_mirror?' do + let(:project) { create(:project, :remote_mirror, :import_started) } + subject { project.has_remote_mirror? } + + before do + allow_any_instance_of(RemoteMirror).to receive(:refresh_remote) + end + + it 'returns true when a remote mirror is enabled' do + is_expected.to be_truthy + end + + it 'returns false when remote mirror is disabled' do + project.remote_mirrors.first.update_attributes(enabled: false) + + is_expected.to be_falsy + end + end + + describe '#update_remote_mirrors' do + let(:project) { create(:project, :remote_mirror, :import_started) } + delegate :update_remote_mirrors, to: :project + + before do + allow_any_instance_of(RemoteMirror).to receive(:refresh_remote) + end + + it 'syncs enabled remote mirror' do + expect_any_instance_of(RemoteMirror).to receive(:sync) + + update_remote_mirrors + end + + # TODO: study if remote_mirror_available_overridden is still a necessary attribute considering that + # it is no longer under any license + it 'does nothing when remote mirror is disabled globally and not overridden' do + stub_application_setting(mirror_available: false) + project.remote_mirror_available_overridden = false + + expect_any_instance_of(RemoteMirror).not_to receive(:sync) + + update_remote_mirrors + end + + it 'does not sync disabled remote mirrors' do + project.remote_mirrors.first.update_attributes(enabled: false) + + expect_any_instance_of(RemoteMirror).not_to receive(:sync) + + update_remote_mirrors + end + end + + describe '#remote_mirror_available?' do + let(:project) { create(:project) } + + context 'when remote mirror global setting is enabled' do + it 'returns true' do + expect(project.remote_mirror_available?).to be(true) + end + end + + context 'when remote mirror global setting is disabled' do + before do + stub_application_setting(mirror_available: false) + end + + it 'returns true when overridden' do + project.remote_mirror_available_overridden = true + + expect(project.remote_mirror_available?).to be(true) + end + + it 'returns false when not overridden' do + expect(project.remote_mirror_available?).to be(false) + end + end + end + describe '#ancestors_upto', :nested_groups do let(:parent) { create(:group) } let(:child) { create(:group, parent: parent) } diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb new file mode 100644 index 00000000000..a80800c6c92 --- /dev/null +++ b/spec/models/remote_mirror_spec.rb @@ -0,0 +1,267 @@ +require 'rails_helper' + +describe RemoteMirror do + describe 'URL validation' do + context 'with a valid URL' do + it 'should be valid' do + remote_mirror = build(:remote_mirror) + expect(remote_mirror).to be_valid + end + end + + context 'with an invalid URL' do + it 'should not be valid' do + remote_mirror = build(:remote_mirror, url: 'ftp://invalid.invalid') + expect(remote_mirror).not_to be_valid + expect(remote_mirror.errors[:url].size).to eq(2) + end + end + end + + describe 'encrypting credentials' do + context 'when setting URL for a first time' do + it 'stores the URL without credentials' do + mirror = create_mirror(url: 'http://foo:bar@test.com') + + expect(mirror.read_attribute(:url)).to eq('http://test.com') + end + + it 'stores the credentials on a separate field' do + mirror = create_mirror(url: 'http://foo:bar@test.com') + + expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' }) + end + + it 'handles credentials with large content' do + mirror = create_mirror(url: 'http://bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif:9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75@test.com') + + expect(mirror.credentials).to eq({ + user: 'bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif', + password: '9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75' + }) + end + end + + context 'when updating the URL' do + it 'allows a new URL without credentials' do + mirror = create_mirror(url: 'http://foo:bar@test.com') + + mirror.update_attribute(:url, 'http://test.com') + + expect(mirror.url).to eq('http://test.com') + expect(mirror.credentials).to eq({ user: nil, password: nil }) + end + + it 'allows a new URL with credentials' do + mirror = create_mirror(url: 'http://test.com') + + mirror.update_attribute(:url, 'http://foo:bar@test.com') + + expect(mirror.url).to eq('http://foo:bar@test.com') + expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' }) + end + + it 'updates the remote config if credentials changed' do + mirror = create_mirror(url: 'http://foo:bar@test.com') + repo = mirror.project.repository + + mirror.update_attribute(:url, 'http://foo:baz@test.com') + + config = repo.raw_repository.rugged.config + expect(config["remote.#{mirror.remote_name}.url"]).to eq('http://foo:baz@test.com') + end + + it 'removes previous remote' do + mirror = create_mirror(url: 'http://foo:bar@test.com') + + expect(RepositoryRemoveRemoteWorker).to receive(:perform_async).with(mirror.project.id, mirror.remote_name).and_call_original + + mirror.update_attributes(url: 'http://test.com') + end + end + end + + describe '#remote_name' do + context 'when remote name is persisted in the database' do + it 'returns remote name with random value' do + allow(SecureRandom).to receive(:hex).and_return('secret') + + remote_mirror = create(:remote_mirror) + + expect(remote_mirror.remote_name).to eq("remote_mirror_secret") + end + end + + context 'when remote name is not persisted in the database' do + it 'returns remote name with remote mirror id' do + remote_mirror = create(:remote_mirror) + remote_mirror.remote_name = nil + + expect(remote_mirror.remote_name).to eq("remote_mirror_#{remote_mirror.id}") + end + end + + context 'when remote is not persisted in the database' do + it 'returns nil' do + remote_mirror = build(:remote_mirror, remote_name: nil) + + expect(remote_mirror.remote_name).to be_nil + end + end + end + + describe '#safe_url' do + context 'when URL contains credentials' do + it 'masks the credentials' do + mirror = create_mirror(url: 'http://foo:bar@test.com') + + expect(mirror.safe_url).to eq('http://*****:*****@test.com') + end + end + + context 'when URL does not contain credentials' do + it 'shows the full URL' do + mirror = create_mirror(url: 'http://test.com') + + expect(mirror.safe_url).to eq('http://test.com') + end + end + end + + context 'when remote mirror gets destroyed' do + it 'removes remote' do + mirror = create_mirror(url: 'http://foo:bar@test.com') + + expect(RepositoryRemoveRemoteWorker).to receive(:perform_async).with(mirror.project.id, mirror.remote_name).and_call_original + + mirror.destroy! + end + end + + context 'stuck mirrors' do + it 'includes mirrors stuck in started with no last_update_at set' do + mirror = create_mirror(url: 'http://cantbeblank', + update_status: 'started', + last_update_at: nil, + updated_at: 25.hours.ago) + + expect(described_class.stuck.last).to eq(mirror) + end + end + + context '#sync' do + let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } + + around do |example| + Timecop.freeze { example.run } + end + + context 'with remote mirroring disabled' do + it 'returns nil' do + remote_mirror.update_attributes(enabled: false) + + expect(remote_mirror.sync).to be_nil + end + end + + context 'with remote mirroring enabled' do + context 'with only protected branches enabled' do + context 'when it did not update in the last minute' do + it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do + expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.now) + + remote_mirror.sync + end + end + + context 'when it did update in the last minute' do + it 'schedules a RepositoryUpdateRemoteMirrorWorker to run in the next minute' do + remote_mirror.last_update_started_at = Time.now - 30.seconds + + expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::PROTECTED_BACKOFF_DELAY, remote_mirror.id, Time.now) + + remote_mirror.sync + end + end + end + + context 'with only protected branches disabled' do + before do + remote_mirror.only_protected_branches = false + end + + context 'when it did not update in the last 5 minutes' do + it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do + expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.now) + + remote_mirror.sync + end + end + + context 'when it did update within the last 5 minutes' do + it 'schedules a RepositoryUpdateRemoteMirrorWorker to run in the next 5 minutes' do + remote_mirror.last_update_started_at = Time.now - 30.seconds + + expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::UNPROTECTED_BACKOFF_DELAY, remote_mirror.id, Time.now) + + remote_mirror.sync + end + end + end + end + end + + context '#updated_since?' do + let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } + let(:timestamp) { Time.now - 5.minutes } + + around do |example| + Timecop.freeze { example.run } + end + + before do + remote_mirror.update_attributes(last_update_started_at: Time.now) + end + + context 'when remote mirror does not have status failed' do + it 'returns true when last update started after the timestamp' do + expect(remote_mirror.updated_since?(timestamp)).to be true + end + + it 'returns false when last update started before the timestamp' do + expect(remote_mirror.updated_since?(Time.now + 5.minutes)).to be false + end + end + + context 'when remote mirror has status failed' do + it 'returns false when last update started after the timestamp' do + remote_mirror.update_attributes(update_status: 'failed') + + expect(remote_mirror.updated_since?(timestamp)).to be false + end + end + end + + context 'no project' do + it 'includes mirror with a project in pending_delete' do + mirror = create_mirror(url: 'http://cantbeblank', + update_status: 'finished', + enabled: true, + last_update_at: nil, + updated_at: 25.hours.ago) + project = mirror.project + project.pending_delete = true + project.save + mirror.reload + + expect(mirror.sync).to be_nil + expect(mirror.valid?).to be_truthy + expect(mirror.update_status).to eq('finished') + end + end + + def create_mirror(params) + project = FactoryBot.create(:project, :repository) + project.remote_mirrors.create!(params) + end +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 630b9e0519f..4b736b02b7d 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -758,6 +758,38 @@ describe Repository do end end + describe '#async_remove_remote' do + before do + masterrev = repository.find_branch('master').dereferenced_target + create_remote_branch('joe', 'remote_branch', masterrev) + end + + context 'when worker is scheduled successfully' do + before do + masterrev = repository.find_branch('master').dereferenced_target + create_remote_branch('remote_name', 'remote_branch', masterrev) + + allow(RepositoryRemoveRemoteWorker).to receive(:perform_async).and_return('1234') + end + + it 'returns job_id' do + expect(repository.async_remove_remote('joe')).to eq('1234') + end + end + + context 'when worker does not schedule successfully' do + before do + allow(RepositoryRemoveRemoteWorker).to receive(:perform_async).and_return(nil) + end + + it 'returns nil' do + expect(Rails.logger).to receive(:info).with("Remove remote job failed to create for #{project.id} with remote name joe.") + + expect(repository.async_remove_remote('joe')).to be_nil + end + end + end + describe '#fetch_ref' do let(:broken_repository) { create(:project, :broken_storage).repository } @@ -2338,6 +2370,11 @@ describe Repository do end end + def create_remote_branch(remote_name, branch_name, target) + rugged = repository.rugged + rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id) + end + describe '#ancestor?' do let(:commit) { repository.commit } let(:ancestor) { commit.parents.first } diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 26fdf8d4b24..35826de5814 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -14,6 +14,72 @@ describe GitPushService, services: true do project.add_master(user) end + describe 'with remote mirrors' do + let(:project) { create(:project, :repository, :remote_mirror) } + + subject do + described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) + end + + context 'when remote mirror feature is enabled' do + it 'fails stuck remote mirrors' do + allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors) + expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!) + + subject.execute + end + + it 'updates remote mirrors' do + expect(project).to receive(:update_remote_mirrors) + + subject.execute + end + end + + context 'when remote mirror feature is disabled' do + before do + stub_application_setting(mirror_available: false) + end + + context 'with remote mirrors global setting overridden' do + before do + project.remote_mirror_available_overridden = true + end + + it 'fails stuck remote mirrors' do + allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors) + expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!) + + subject.execute + end + + it 'updates remote mirrors' do + expect(project).to receive(:update_remote_mirrors) + + subject.execute + end + end + + context 'without remote mirrors global setting overridden' do + before do + project.remote_mirror_available_overridden = false + end + + it 'does not fails stuck remote mirrors' do + expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!) + + subject.execute + end + + it 'does not updates remote mirrors' do + expect(project).not_to receive(:update_remote_mirrors) + + subject.execute + end + end + end + end + describe 'Push branches' do subject do execute_service(project, user, oldrev, newrev, ref) diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb index b8fa3e3d124..dcf4503ef9c 100644 --- a/spec/services/issuable/common_system_notes_service_spec.rb +++ b/spec/services/issuable/common_system_notes_service_spec.rb @@ -5,34 +5,6 @@ describe Issuable::CommonSystemNotesService do let(:project) { create(:project) } let(:issuable) { create(:issue) } - shared_examples 'system note creation' do |update_params, note_text| - subject { described_class.new(project, user).execute(issuable, [])} - - before do - issuable.assign_attributes(update_params) - issuable.save - end - - it 'creates 1 system note with the correct content' do - expect { subject }.to change { Note.count }.from(0).to(1) - - note = Note.last - expect(note.note).to match(note_text) - expect(note.noteable_type).to eq(issuable.class.name) - end - end - - shared_examples 'WIP notes creation' do |wip_action| - subject { described_class.new(project, user).execute(issuable, []) } - - it 'creates WIP toggle and title change notes' do - expect { subject }.to change { Note.count }.from(0).to(2) - - expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**") - expect(Note.second.note).to match('changed title') - end - end - describe '#execute' do it_behaves_like 'system note creation', { title: 'New title' }, 'changed title' it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description' diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index b2c52214f48..b63f409579e 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -65,6 +65,19 @@ describe Projects::DestroyService do Sidekiq::Testing.inline! { destroy_project(project, user, {}) } end + context 'when has remote mirrors' do + let!(:project) do + create(:project, :repository, namespace: user.namespace).tap do |project| + project.remote_mirrors.create(url: 'http://test.com') + end + end + let!(:async) { true } + + it 'destroys them' do + expect(RemoteMirror.count).to eq(0) + end + end + it_behaves_like 'deleting the project' it 'invalidates personal_project_count cache' do diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb new file mode 100644 index 00000000000..be09afd9f36 --- /dev/null +++ b/spec/services/projects/update_remote_mirror_service_spec.rb @@ -0,0 +1,355 @@ +require 'spec_helper' + +describe Projects::UpdateRemoteMirrorService do + let(:project) { create(:project, :repository) } + let(:remote_project) { create(:forked_project_with_submodules) } + let(:repository) { project.repository } + let(:raw_repository) { repository.raw } + let(:remote_mirror) { project.remote_mirrors.create!(url: remote_project.http_url_to_repo, enabled: true, only_protected_branches: false) } + + subject { described_class.new(project, project.creator) } + + describe "#execute", :skip_gitaly_mock do + before do + create_branch(repository, 'existing-branch') + allow(raw_repository).to receive(:remote_tags) do + generate_tags(repository, 'v1.0.0', 'v1.1.0') + end + allow(raw_repository).to receive(:push_remote_branches).and_return(true) + end + + it "fetches the remote repository" do + expect(repository).to receive(:fetch_remote).with(remote_mirror.remote_name, no_tags: true) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + end + + subject.execute(remote_mirror) + end + + it "succeeds" do + allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) } + + result = subject.execute(remote_mirror) + + expect(result[:status]).to eq(:success) + end + + describe 'Syncing branches' do + it "push all the branches the first time" do + allow(repository).to receive(:fetch_remote) + + expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, local_branch_names) + + subject.execute(remote_mirror) + end + + it "does not push anything is remote is up to date" do + allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) } + + expect(raw_repository).not_to receive(:push_remote_branches) + + subject.execute(remote_mirror) + end + + it "sync new branches" do + # call local_branch_names early so it is not called after the new branch has been created + current_branches = local_branch_names + allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, current_branches) } + create_branch(repository, 'my-new-branch') + + expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['my-new-branch']) + + subject.execute(remote_mirror) + end + + it "sync updated branches" do + allow(repository).to receive(:fetch_remote) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + update_branch(repository, 'existing-branch') + end + + expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['existing-branch']) + + subject.execute(remote_mirror) + end + + context 'when push only protected branches option is set' do + let(:unprotected_branch_name) { 'existing-branch' } + let(:protected_branch_name) do + project.repository.branch_names.find { |n| n != unprotected_branch_name } + end + let!(:protected_branch) do + create(:protected_branch, project: project, name: protected_branch_name) + end + + before do + project.reload + remote_mirror.only_protected_branches = true + end + + it "sync updated protected branches" do + allow(repository).to receive(:fetch_remote) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + update_branch(repository, protected_branch_name) + end + + expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, [protected_branch_name]) + + subject.execute(remote_mirror) + end + + it 'does not sync unprotected branches' do + allow(repository).to receive(:fetch_remote) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + update_branch(repository, unprotected_branch_name) + end + + expect(raw_repository).not_to receive(:push_remote_branches).with(remote_mirror.remote_name, [unprotected_branch_name]) + + subject.execute(remote_mirror) + end + end + + context 'when branch exists in local and remote repo' do + context 'when it has diverged' do + it 'syncs branches' do + allow(repository).to receive(:fetch_remote) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + update_remote_branch(repository, remote_mirror.remote_name, 'markdown') + end + + expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['markdown']) + + subject.execute(remote_mirror) + end + end + end + + describe 'for delete' do + context 'when branch exists in local and remote repo' do + it 'deletes the branch from remote repo' do + allow(repository).to receive(:fetch_remote) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + delete_branch(repository, 'existing-branch') + end + + expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch']) + + subject.execute(remote_mirror) + end + end + + context 'when push only protected branches option is set' do + before do + remote_mirror.only_protected_branches = true + end + + context 'when branch exists in local and remote repo' do + let!(:protected_branch_name) { local_branch_names.first } + + before do + create(:protected_branch, project: project, name: protected_branch_name) + project.reload + end + + it 'deletes the protected branch from remote repo' do + allow(repository).to receive(:fetch_remote) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + delete_branch(repository, protected_branch_name) + end + + expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name]) + + subject.execute(remote_mirror) + end + + it 'does not delete the unprotected branch from remote repo' do + allow(repository).to receive(:fetch_remote) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + delete_branch(repository, 'existing-branch') + end + + expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch']) + + subject.execute(remote_mirror) + end + end + + context 'when branch only exists on remote repo' do + let!(:protected_branch_name) { 'remote-branch' } + + before do + create(:protected_branch, project: project, name: protected_branch_name) + end + + context 'when it has diverged' do + it 'does not delete the remote branch' do + allow(repository).to receive(:fetch_remote) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + + rev = repository.find_branch('markdown').dereferenced_target + create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id) + end + + expect(raw_repository).not_to receive(:delete_remote_branches) + + subject.execute(remote_mirror) + end + end + + context 'when it has not diverged' do + it 'deletes the remote branch' do + allow(repository).to receive(:fetch_remote) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + + masterrev = repository.find_branch('master').dereferenced_target + create_remote_branch(repository, remote_mirror.remote_name, protected_branch_name, masterrev.id) + end + + expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name]) + + subject.execute(remote_mirror) + end + end + end + end + + context 'when branch only exists on remote repo' do + context 'when it has diverged' do + it 'does not delete the remote branch' do + allow(repository).to receive(:fetch_remote) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + + rev = repository.find_branch('markdown').dereferenced_target + create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id) + end + + expect(raw_repository).not_to receive(:delete_remote_branches) + + subject.execute(remote_mirror) + end + end + + context 'when it has not diverged' do + it 'deletes the remote branch' do + allow(repository).to receive(:fetch_remote) do + sync_remote(repository, remote_mirror.remote_name, local_branch_names) + + masterrev = repository.find_branch('master').dereferenced_target + create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', masterrev.id) + end + + expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['remote-branch']) + + subject.execute(remote_mirror) + end + end + end + end + end + + describe 'Syncing tags' do + before do + allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) } + end + + context 'when there are not tags to push' do + it 'does not try to push tags' do + allow(repository).to receive(:remote_tags) { {} } + allow(repository).to receive(:tags) { [] } + + expect(repository).not_to receive(:push_tags) + + subject.execute(remote_mirror) + end + end + + context 'when there are some tags to push' do + it 'pushes tags to remote' do + allow(raw_repository).to receive(:remote_tags) { {} } + + expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['v1.0.0', 'v1.1.0']) + + subject.execute(remote_mirror) + end + end + + context 'when there are some tags to delete' do + it 'deletes tags from remote' do + remote_tags = generate_tags(repository, 'v1.0.0', 'v1.1.0') + allow(raw_repository).to receive(:remote_tags) { remote_tags } + + repository.rm_tag(create(:user), 'v1.0.0') + + expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['v1.0.0']) + + subject.execute(remote_mirror) + end + end + end + end + + def create_branch(repository, branch_name) + rugged = repository.rugged + masterrev = repository.find_branch('master').dereferenced_target + parentrev = repository.commit(masterrev).parent_id + + rugged.references.create("refs/heads/#{branch_name}", parentrev) + + repository.expire_branches_cache + end + + def create_remote_branch(repository, remote_name, branch_name, source_id) + rugged = repository.rugged + + rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_id) + end + + def sync_remote(repository, remote_name, local_branch_names) + rugged = repository.rugged + + local_branch_names.each do |branch| + target = repository.find_branch(branch).try(:dereferenced_target) + rugged.references.create("refs/remotes/#{remote_name}/#{branch}", target.id) if target + end + end + + def update_remote_branch(repository, remote_name, branch) + rugged = repository.rugged + masterrev = repository.find_branch('master').dereferenced_target.id + + rugged.references.create("refs/remotes/#{remote_name}/#{branch}", masterrev, force: true) + repository.expire_branches_cache + end + + def update_branch(repository, branch) + rugged = repository.rugged + masterrev = repository.find_branch('master').dereferenced_target.id + + # Updated existing branch + rugged.references.create("refs/heads/#{branch}", masterrev, force: true) + repository.expire_branches_cache + end + + def delete_branch(repository, branch) + rugged = repository.rugged + + rugged.references.delete("refs/heads/#{branch}") + repository.expire_branches_cache + end + + def generate_tags(repository, *tag_names) + tag_names.each_with_object([]) do |name, tags| + tag = repository.find_tag(name) + target = tag.try(:target) + target_commit = tag.try(:dereferenced_target) + tags << Gitlab::Git::Tag.new(repository.raw_repository, name, target, target_commit) + end + end + + def local_branch_names + branch_names = repository.branches.map(&:name) + # we want the protected branch to be pushed first + branch_names.unshift(branch_names.delete('master')) + end +end diff --git a/spec/support/shared_examples/common_system_notes_examples.rb b/spec/support/shared_examples/common_system_notes_examples.rb new file mode 100644 index 00000000000..96ef30b7513 --- /dev/null +++ b/spec/support/shared_examples/common_system_notes_examples.rb @@ -0,0 +1,27 @@ +shared_examples 'system note creation' do |update_params, note_text| + subject { described_class.new(project, user).execute(issuable, [])} + + before do + issuable.assign_attributes(update_params) + issuable.save + end + + it 'creates 1 system note with the correct content' do + expect { subject }.to change { Note.count }.from(0).to(1) + + note = Note.last + expect(note.note).to match(note_text) + expect(note.noteable_type).to eq(issuable.class.name) + end +end + +shared_examples 'WIP notes creation' do |wip_action| + subject { described_class.new(project, user).execute(issuable, []) } + + it 'creates WIP toggle and title change notes' do + expect { subject }.to change { Note.count }.from(0).to(2) + + expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**") + expect(Note.second.note).to match('changed title') + end +end diff --git a/spec/workers/repository_remove_remote_worker_spec.rb b/spec/workers/repository_remove_remote_worker_spec.rb new file mode 100644 index 00000000000..f22d7c1d073 --- /dev/null +++ b/spec/workers/repository_remove_remote_worker_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +describe RepositoryRemoveRemoteWorker do + subject(:worker) { described_class.new } + + describe '#perform' do + let(:remote_name) { 'joe'} + let!(:project) { create(:project, :repository) } + + context 'when it cannot obtain lease' do + it 'logs error' do + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil } + + expect_any_instance_of(Repository).not_to receive(:remove_remote) + expect(worker).to receive(:log_error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.') + + worker.perform(project.id, remote_name) + end + end + + context 'when it gets the lease' do + before do + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(true) + end + + context 'when project does not exist' do + it 'returns nil' do + expect(worker.perform(-1, 'remote_name')).to be_nil + end + end + + context 'when project exists' do + it 'removes remote from repository' do + masterrev = project.repository.find_branch('master').dereferenced_target + + create_remote_branch(remote_name, 'remote_branch', masterrev) + + expect_any_instance_of(Repository).to receive(:remove_remote).with(remote_name).and_call_original + + worker.perform(project.id, remote_name) + end + end + end + end + + def create_remote_branch(remote_name, branch_name, target) + rugged = project.repository.rugged + rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id) + end +end diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb new file mode 100644 index 00000000000..152ba2509b9 --- /dev/null +++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb @@ -0,0 +1,84 @@ +require 'rails_helper' + +describe RepositoryUpdateRemoteMirrorWorker do + subject { described_class.new } + + let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } + let(:scheduled_time) { Time.now - 5.minutes } + + around do |example| + Timecop.freeze(Time.now) { example.run } + end + + describe '#perform' do + context 'with status none' do + before do + remote_mirror.update_attributes(update_status: 'none') + end + + it 'sets status as finished when update remote mirror service executes successfully' do + expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) + + expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.update_status }.to('finished') + end + + it 'sets status as failed when update remote mirror service executes with errors' do + error_message = 'fail!' + + expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :error, message: error_message) + expect do + subject.perform(remote_mirror.id, Time.now) + end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError, error_message) + + expect(remote_mirror.reload.update_status).to eq('failed') + end + + it 'does nothing if last_update_started_at is higher than the time the job was scheduled in' do + remote_mirror.update_attributes(last_update_started_at: Time.now) + + expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(true) + expect_any_instance_of(Projects::UpdateRemoteMirrorService).not_to receive(:execute).with(remote_mirror) + + expect(subject.perform(remote_mirror.id, scheduled_time)).to be_nil + end + end + + context 'with unexpected error' do + it 'marks mirror as failed' do + allow_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_raise(RuntimeError) + + expect do + subject.perform(remote_mirror.id, Time.now) + end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError) + expect(remote_mirror.reload.update_status).to eq('failed') + end + end + + context 'with another worker already running' do + before do + remote_mirror.update_attributes(update_status: 'started') + end + + it 'raises RemoteMirrorUpdateAlreadyInProgressError' do + expect do + subject.perform(remote_mirror.id, Time.now) + end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateAlreadyInProgressError) + end + end + + context 'with status failed' do + before do + remote_mirror.update_attributes(update_status: 'failed') + end + + it 'sets status as finished if last_update_started_at is higher than the time the job was scheduled in' do + remote_mirror.update_attributes(last_update_started_at: Time.now) + + expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(false) + expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) + + expect { subject.perform(remote_mirror.id, scheduled_time) }.to change { remote_mirror.reload.update_status }.to('finished') + end + end + end +end diff --git a/yarn.lock b/yarn.lock index 33e129a8e51..2d18a694e9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,9 +54,16 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.18.0": - version "1.18.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.18.0.tgz#7829f0e6de0647dace54c1fcd597ee3424afb233" +"@gitlab-org/gitlab-svgs@^1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.20.0.tgz#4c3fa3a91e0693114654b0066fb1ef04c0602047" + +"@mrmlnc/readdir-enhanced@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" + dependencies: + call-me-maybe "^1.0.1" + glob-to-regexp "^0.3.0" "@sindresorhus/is@^0.7.0": version "0.7.0" @@ -81,11 +88,11 @@ accepts@~1.3.3, accepts@~1.3.4: mime-types "~2.1.16" negotiator "0.6.1" -acorn-dynamic-import@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" +acorn-dynamic-import@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278" dependencies: - acorn "^4.0.3" + acorn "^5.0.0" acorn-jsx@^3.0.0: version "3.0.1" @@ -97,18 +104,10 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^4.0.3: - version "4.0.13" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" - acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0: version "5.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102" -address@1.0.3, address@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" - addressparser@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746" @@ -139,7 +138,7 @@ ajv@^4.7.0, ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.0.0, ajv@^5.1.0: +ajv@^5.1.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" dependencies: @@ -188,7 +187,7 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" -ansi-escapes@^1.1.0: +ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" @@ -218,6 +217,14 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" + +any-observable@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242" + anymatch@^1.3.0: version "1.3.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" @@ -273,9 +280,9 @@ arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" -array-filter@~0.0.0: - version "0.0.1" - resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" +array-differ@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" array-find-index@^1.0.1: version "1.0.2" @@ -300,14 +307,6 @@ array-includes@^3.0.3: define-properties "^1.1.2" es-abstract "^1.7.0" -array-map@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" - -array-reduce@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" - array-slice@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" @@ -368,6 +367,14 @@ assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" +ast-types@0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.10.1.tgz#f52fca9715579a14f841d67d7f8d25432ab6a3dd" + +ast-types@0.11.3: + version "0.11.3" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.3.tgz#c20757fe72ee71278ea0ff3d87e5c2ca30d9edf8" + ast-types@0.x.x: version "0.11.1" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.1.tgz#5bb3a8d5ba292c3f4ae94d46df37afc30300b990" @@ -380,16 +387,22 @@ async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" -async@1.x, async@^1.4.0, async@^1.5.2: +async@1.x, async@^1.4.0, async@^1.5.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.0.0, async@^2.1.2, async@^2.1.4, async@^2.4.1: +async@^2.0.0, async@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" dependencies: lodash "^4.14.0" +async@^2.1.4: + version "2.4.1" + resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7" + dependencies: + lodash "^4.14.0" + async@~2.1.2: version "2.1.5" resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" @@ -450,7 +463,7 @@ axios@^0.17.1: follow-redirects "^1.2.5" is-buffer "^1.1.5" -babel-code-frame@6.26.0, babel-code-frame@^6.16.0, babel-code-frame@^6.26.0: +babel-code-frame@^6.16.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: @@ -458,9 +471,9 @@ babel-code-frame@6.26.0, babel-code-frame@^6.16.0, babel-code-frame@^6.26.0: esutils "^2.0.2" js-tokens "^3.0.2" -babel-core@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" +babel-core@^6.26.0, babel-core@^6.26.3: + version "6.26.3" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" dependencies: babel-code-frame "^6.26.0" babel-generator "^6.26.0" @@ -472,15 +485,15 @@ babel-core@^6.26.0: babel-traverse "^6.26.0" babel-types "^6.26.0" babylon "^6.18.0" - convert-source-map "^1.5.0" - debug "^2.6.8" + convert-source-map "^1.5.1" + debug "^2.6.9" json5 "^0.5.1" lodash "^4.17.4" minimatch "^3.0.4" path-is-absolute "^1.0.1" - private "^0.1.7" + private "^0.1.8" slash "^1.0.0" - source-map "^0.5.6" + source-map "^0.5.7" babel-eslint@^8.0.2: version "8.0.2" @@ -622,9 +635,9 @@ babel-helpers@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-loader@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126" +babel-loader@^7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015" dependencies: find-cache-dir "^1.0.0" loader-utils "^1.0.2" @@ -663,6 +676,10 @@ babel-plugin-syntax-async-generators@^6.5.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" +babel-plugin-syntax-class-constructor-call@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416" + babel-plugin-syntax-class-properties@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" @@ -679,6 +696,14 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" +babel-plugin-syntax-export-extensions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721" + +babel-plugin-syntax-flow@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" + babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" @@ -703,6 +728,14 @@ babel-plugin-transform-async-to-generator@^6.24.1: babel-plugin-syntax-async-functions "^6.8.0" babel-runtime "^6.22.0" +babel-plugin-transform-class-constructor-call@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9" + dependencies: + babel-plugin-syntax-class-constructor-call "^6.18.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-plugin-transform-class-properties@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" @@ -905,6 +938,20 @@ babel-plugin-transform-exponentiation-operator@^6.24.1: babel-plugin-syntax-exponentiation-operator "^6.8.0" babel-runtime "^6.22.0" +babel-plugin-transform-export-extensions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653" + dependencies: + babel-plugin-syntax-export-extensions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-flow-strip-types@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf" + dependencies: + babel-plugin-syntax-flow "^6.18.0" + babel-runtime "^6.22.0" + babel-plugin-transform-object-rest-spread@^6.22.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921" @@ -925,7 +972,7 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-preset-es2015@^6.24.1: +babel-preset-es2015@^6.24.1, babel-preset-es2015@^6.9.0: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939" dependencies: @@ -975,6 +1022,14 @@ babel-preset-latest@^6.24.1: babel-preset-es2016 "^6.24.1" babel-preset-es2017 "^6.24.1" +babel-preset-stage-1@^6.5.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0" + dependencies: + babel-plugin-transform-class-constructor-call "^6.24.1" + babel-plugin-transform-export-extensions "^6.22.0" + babel-preset-stage-2 "^6.24.1" + babel-preset-stage-2@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1" @@ -994,7 +1049,7 @@ babel-preset-stage-3@^6.24.1: babel-plugin-transform-exponentiation-operator "^6.24.1" babel-plugin-transform-object-rest-spread "^6.22.0" -babel-register@^6.26.0: +babel-register@^6.26.0, babel-register@^6.9.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" dependencies: @@ -1050,10 +1105,14 @@ babylon@7.0.0-beta.32, babylon@^7.0.0-beta.31: version "7.0.0-beta.32" resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.32.tgz#e9033cb077f64d6895f4125968b37dc0a8c3bc6e" -babylon@^6.18.0: +babylon@^6.17.3, babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" +babylon@^7.0.0-beta.30: + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d" + backo2@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" @@ -1122,6 +1181,10 @@ binary-extensions@^1.0.0: version "1.11.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" +binaryextensions@2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935" + bitsyntax@~0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.0.4.tgz#eb10cc6f82b8c490e3e85698f07e83d46e0cba82" @@ -1223,7 +1286,7 @@ boxen@^1.2.1: term-size "^1.2.0" widest-line "^2.0.0" -brace-expansion@^1.0.0, brace-expansion@^1.1.7, brace-expansion@^1.1.8: +brace-expansion@^1.1.7, brace-expansion@^1.1.8: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" dependencies: @@ -1372,7 +1435,7 @@ bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" -cacache@^10.0.1: +cacache@^10.0.1, cacache@^10.0.4: version "10.0.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" dependencies: @@ -1416,6 +1479,10 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" +call-me-maybe@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -1445,10 +1512,6 @@ camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - camelcase@^4.0.0, camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -1485,7 +1548,7 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -1495,7 +1558,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" dependencies: @@ -1503,6 +1566,22 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3 escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" + dependencies: + ansi-styles "~1.0.0" + has-color "~0.1.0" + strip-ansi "~0.1.0" + chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" @@ -1515,7 +1594,7 @@ check-types@^7.3.0: version "7.3.0" resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d" -chokidar@^1.4.1, chokidar@^1.7.0: +chokidar@^1.4.1: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -1552,6 +1631,10 @@ chownr@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" +chrome-trace-event@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-0.1.2.tgz#90f36885d5345a50621332f0717b595883d5d982" + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -1590,7 +1673,7 @@ cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" -cli-cursor@^1.0.1: +cli-cursor@^1.0.1, cli-cursor@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" dependencies: @@ -1602,6 +1685,23 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-spinners@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" + +cli-table@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" + dependencies: + colors "1.0.3" + +cli-truncate@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" + dependencies: + slice-ansi "0.0.4" + string-width "^1.0.1" + cli-width@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" @@ -1622,24 +1722,52 @@ cliui@^2.1.0: right-align "^0.1.1" wordwrap "0.0.2" -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" +cliui@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc" dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" + string-width "^2.1.1" + strip-ansi "^4.0.0" wrap-ansi "^2.0.0" +clone-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" + clone-response@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" dependencies: mimic-response "^1.0.0" +clone-stats@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" + +clone-stats@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" + +clone@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" + clone@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" +clone@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" + +cloneable-readable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.0.0.tgz#a6290d413f217a61232f95e458ff38418cfb0117" + dependencies: + inherits "^2.0.1" + process-nextick-args "^1.0.6" + through2 "^2.0.1" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -1697,7 +1825,11 @@ colormin@^1.0.5: css-color-names "0.0.4" has "^1.0.1" -colors@^1.1.0, colors@~1.1.2: +colors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + +colors@^1.1.0, colors@^1.1.2, colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -1717,6 +1849,10 @@ commander@^2.13.0, commander@^2.15.1, commander@^2.9.0: version "2.15.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" +commander@~2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -1739,13 +1875,13 @@ compressible@~2.0.10: dependencies: mime-db ">= 1.29.0 < 2" -compression-webpack-plugin@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.1.7.tgz#b0dfb97cf1d26baab997b584b8c36fe91872abe2" +compression-webpack-plugin@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.1.11.tgz#8384c7a6ead1d2e2efb190bdfcdcf35878ed8266" dependencies: - async "^2.4.1" cacache "^10.0.1" find-cache-dir "^1.0.0" + neo-async "^2.5.0" serialize-javascript "^1.4.0" webpack-sources "^1.0.1" @@ -1829,9 +1965,9 @@ content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" -convert-source-map@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" +convert-source-map@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" cookie-signature@1.0.6: version "1.0.6" @@ -1856,15 +1992,15 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" -copy-webpack-plugin@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.4.1.tgz#1e8c366211db6dc2ddee40e5a3e4fc661dd149e8" +copy-webpack-plugin@^4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz#fc4f68f4add837cc5e13d111b20715793225d29c" dependencies: - cacache "^10.0.1" + cacache "^10.0.4" find-cache-dir "^1.0.0" globby "^7.1.1" is-glob "^4.0.0" - loader-utils "^0.2.15" + loader-utils "^1.1.0" minimatch "^3.0.4" p-limit "^1.0.0" serialize-javascript "^1.4.0" @@ -1932,7 +2068,7 @@ cropper@^2.3.0: dependencies: jquery ">= 1.9.1" -cross-spawn@5.1.0, cross-spawn@^5.0.1: +cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" dependencies: @@ -1940,6 +2076,16 @@ cross-spawn@5.1.0, cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -1976,9 +2122,9 @@ css-color-names@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" -css-loader@^0.28.9: - version "0.28.9" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.9.tgz#68064b85f4e271d7ce4c48a58300928e535d1c95" +css-loader@^0.28.11: + version "0.28.11" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7" dependencies: babel-code-frame "^6.26.0" css-selector-tokenizer "^0.7.0" @@ -2199,6 +2345,10 @@ dagre-layout@^0.8.0: graphlib "^2.1.1" lodash "^4.17.4" +dargs@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -2209,6 +2359,10 @@ data-uri-to-buffer@1: version "1.2.0" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835" +date-fns@^1.27.2: + version "1.29.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" + date-format@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8" @@ -2217,11 +2371,15 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" +dateformat@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" + de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" -debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@~2.6.4, debug@~2.6.6: +debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -2257,7 +2415,7 @@ decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" -decompress-response@^3.3.0: +decompress-response@^3.2.0, decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" dependencies: @@ -2267,6 +2425,10 @@ deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" +deep-extend@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f" + deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" @@ -2369,6 +2531,10 @@ destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" +detect-conflict@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/detect-conflict/-/detect-conflict-1.0.1.tgz#088657a66a961c05019db7c4230883b1c6b4176e" + detect-indent@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" @@ -2383,21 +2549,18 @@ detect-node@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" -detect-port-alt@1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.5.tgz#a1aa8fc805a4a5df9b905b7ddc7eed036bcce889" - dependencies: - address "^1.0.1" - debug "^2.6.0" - di@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" -diff@^3.4.0: +diff@^3.3.1, diff@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" +diff@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + diffie-hellman@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" @@ -2526,6 +2689,10 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +editions@^1.3.3: + version "1.3.4" + resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -2534,10 +2701,18 @@ ejs@^2.5.7: version "2.5.7" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a" +ejs@^2.5.9: + version "2.5.9" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5" + electron-to-chromium@^1.2.7: version "1.3.3" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.3.tgz#651eb63fe89f39db70ffc8dbd5d9b66958bc6a0e" +elegant-spinner@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" + elliptic@^6.0.0: version "6.4.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" @@ -2607,14 +2782,13 @@ engine.io@~3.1.0: optionalDependencies: uws "~9.14.0" -enhanced-resolve@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" +enhanced-resolve@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz#e34a6eaa790f62fccd71d93959f56b2b432db10a" dependencies: graceful-fs "^4.1.2" memory-fs "^0.4.0" - object-assign "^4.0.1" - tapable "^0.2.7" + tapable "^1.0.0" enhanced-resolve@~0.9.0: version "0.9.1" @@ -2632,18 +2806,41 @@ entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" +envinfo@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-4.4.2.tgz#472c49f3a8b9bca73962641ce7cb692bf623cd1c" + errno@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" dependencies: prr "~0.0.0" +errno@^0.1.4: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + dependencies: + prr "~1.0.1" + error-ex@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.0.tgz#e67b43f3e82c96ea3a584ffee0b9fc3325d802d9" dependencies: is-arrayish "^0.2.1" +error-ex@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +error@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02" + dependencies: + string-template "~0.2.1" + xtend "~4.0.0" + es-abstract@^1.7.0: version "1.10.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" @@ -2702,7 +2899,7 @@ es6-set@~0.1.5: es6-symbol "3.1.1" event-emitter "~0.3.5" -es6-symbol@3, es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1: +es6-symbol@3, es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@~3.1, es6-symbol@~3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" dependencies: @@ -2722,7 +2919,7 @@ escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -2902,7 +3099,7 @@ esprima@3.x.x, esprima@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" -esprima@^4.0.0: +esprima@^4.0.0, esprima@~4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" @@ -3111,6 +3308,14 @@ external-editor@^2.0.4: iconv-lite "^0.4.17" tmp "^0.0.33" +external-editor@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" + dependencies: + chardet "^0.4.0" + iconv-lite "^0.4.17" + tmp "^0.0.33" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" @@ -3142,6 +3347,16 @@ fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" +fast-glob@^2.0.2: + version "2.2.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.1.tgz#686c2345be88f3741e174add0be6f2e5b6078889" + dependencies: + "@mrmlnc/readdir-enhanced" "^2.2.1" + glob-parent "^3.1.0" + is-glob "^4.0.0" + merge2 "^1.2.1" + micromatch "^3.1.10" + fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -3166,7 +3381,7 @@ faye-websocket@~0.11.0: dependencies: websocket-driver ">=0.5.1" -figures@^1.3.5: +figures@^1.3.5, figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" dependencies: @@ -3186,9 +3401,9 @@ file-entry-cache@^2.0.0: flat-cache "^1.2.1" object-assign "^4.0.1" -file-loader@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.8.tgz#a62592ed732667d7482dc3268c381c7f0c913086" +file-loader@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8" dependencies: loader-utils "^1.0.2" schema-utils "^0.4.5" @@ -3208,10 +3423,6 @@ fileset@^2.0.2: glob "^7.0.3" minimatch "^3.0.3" -filesize@3.5.11: - version "3.5.11" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.11.tgz#1919326749433bb3cf77368bd158caabcc19e9ee" - filesize@^3.5.11: version "3.6.0" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.0.tgz#22d079615624bb6fd3c04026120628a41b3f4efa" @@ -3272,6 +3483,12 @@ find-up@^2.0.0, find-up@^2.1.0: dependencies: locate-path "^2.0.0" +first-chunk-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70" + dependencies: + readable-stream "^2.0.2" + flat-cache@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" @@ -3285,6 +3502,10 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" +flow-parser@^0.*: + version "0.66.0" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.66.0.tgz#be583fefb01192aa5164415d31a6241b35718983" + flush-write-stream@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" @@ -3485,6 +3706,26 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +gh-got@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gh-got/-/gh-got-6.0.0.tgz#d74353004c6ec466647520a10bd46f7299d268d0" + dependencies: + got "^7.0.0" + is-plain-obj "^1.1.0" + +github-username@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/github-username/-/github-username-4.1.0.tgz#cbe280041883206da4212ae9e4b5f169c30bf417" + dependencies: + gh-got "^6.0.0" + +glob-all@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.1.0.tgz#8913ddfb5ee1ac7812656241b03d5217c64b02ab" + dependencies: + glob "^7.0.5" + yargs "~1.2.6" + glob-base@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" @@ -3505,6 +3746,10 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" +glob-to-regexp@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + glob@^5.0.15: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" @@ -3515,7 +3760,18 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: +glob@^7.0.0, glob@^7.0.3: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -3532,7 +3788,7 @@ global-dirs@^0.1.0: dependencies: ini "^1.3.4" -global-modules@1.0.0, global-modules@^1.0.0: +global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" dependencies: @@ -3590,6 +3846,18 @@ globby@^7.1.1: pify "^3.0.0" slash "^1.0.0" +globby@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50" + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + fast-glob "^2.0.2" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + good-listener@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" @@ -3612,7 +3880,26 @@ got@^6.7.1: unzip-response "^2.0.1" url-parse-lax "^1.0.0" -got@^8.0.3: +got@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a" + dependencies: + decompress-response "^3.2.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-plain-obj "^1.1.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + isurl "^1.0.0-alpha5" + lowercase-keys "^1.0.0" + p-cancelable "^0.3.0" + p-timeout "^1.1.1" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + url-parse-lax "^1.0.0" + url-to-options "^1.0.1" + +got@^8.0.3, got@^8.2.0: version "8.3.0" resolved "https://registry.yarnpkg.com/got/-/got-8.3.0.tgz#6ba26e75f8a6cc4c6b3eb1fe7ce4fec7abac8533" dependencies: @@ -3644,11 +3931,11 @@ graphlib@^2.1.1: dependencies: lodash "^4.11.1" -gzip-size@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520" +grouped-queue@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/grouped-queue/-/grouped-queue-0.3.3.tgz#c167d2a5319c5a0e0964ef6a25b7c2df8996c85c" dependencies: - duplexer "^0.1.1" + lodash "^4.17.2" gzip-size@^4.1.0: version "4.1.0" @@ -3714,6 +4001,10 @@ has-binary2@~1.0.2: dependencies: isarray "2.0.1" +has-color@~0.1.0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" + has-cors@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" @@ -3734,10 +4025,6 @@ has-symbol-support-x@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.3.0.tgz#588bd6927eaa0e296afae24160659167fc2be4f8" -has-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" - has-to-string-tag-x@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.3.0.tgz#78e3d98c3c0ec9413e970eb8d766249a1e13058f" @@ -3920,14 +4207,14 @@ http-proxy-agent@1: debug "2" extend "3" -http-proxy-middleware@~0.17.4: - version "0.17.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833" +http-proxy-middleware@~0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz#0987e6bb5a5606e5a69168d8f967a87f15dd8aab" dependencies: http-proxy "^1.16.2" - is-glob "^3.1.0" - lodash "^4.17.2" - micromatch "^2.3.11" + is-glob "^4.0.0" + lodash "^4.17.5" + micromatch "^3.1.9" http-proxy@^1.13.0, http-proxy@^1.16.2: version "1.16.2" @@ -4041,6 +4328,10 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -4076,7 +4367,25 @@ ini@^1.3.4, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" -inquirer@3.3.0: +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +inquirer@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" dependencies: @@ -4095,22 +4404,22 @@ inquirer@3.3.0: strip-ansi "^4.0.0" through "^2.3.6" -inquirer@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" +inquirer@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-5.2.0.tgz#db350c2b73daca77ff1243962e9f22f099685726" dependencies: - ansi-escapes "^1.1.0" - ansi-regex "^2.0.0" - chalk "^1.0.0" - cli-cursor "^1.0.1" + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" cli-width "^2.0.0" - figures "^1.3.5" + external-editor "^2.1.0" + figures "^2.0.0" lodash "^4.3.0" - readline2 "^1.0.1" - run-async "^0.1.0" - rx-lite "^3.1.2" - string-width "^1.0.1" - strip-ansi "^3.0.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^5.5.2" + string-width "^2.1.0" + strip-ansi "^4.0.0" through "^2.3.6" internal-ip@1.2.0: @@ -4123,6 +4432,10 @@ interpret@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" +interpret@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" + into-stream@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" @@ -4350,6 +4663,12 @@ is-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" +is-observable@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2" + dependencies: + symbol-observable "^0.2.2" + is-odd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24" @@ -4372,7 +4691,7 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" -is-plain-obj@^1.0.0: +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -4424,9 +4743,11 @@ is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" -is-root@1.0.0: +is-scoped@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-1.0.0.tgz#07b6c233bc394cd9d02ba15c966bd6660d6342d5" + resolved "https://registry.yarnpkg.com/is-scoped/-/is-scoped-1.0.0.tgz#449ca98299e713038256289ecb2b540dc437cb30" + dependencies: + scoped-regex "^1.0.0" is-stream@^1.0.0, is-stream@^1.1.0: version "1.1.0" @@ -4480,7 +4801,7 @@ isarray@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" -isbinaryfile@^3.0.0: +isbinaryfile@^3.0.0, isbinaryfile@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" @@ -4600,6 +4921,14 @@ istanbul@^0.4.5: which "^1.1.1" wordwrap "^1.0.0" +istextorbinary@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.2.1.tgz#a5231a08ef6dd22b268d0895084cf8d58b5bec53" + dependencies: + binaryextensions "2" + editions "^1.3.3" + textextensions "2" + isurl@^1.0.0-alpha5: version "1.0.0" resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" @@ -4670,6 +4999,46 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" +jscodeshift@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.4.1.tgz#da91a1c2eccfa03a3387a21d39948e251ced444a" + dependencies: + async "^1.5.0" + babel-plugin-transform-flow-strip-types "^6.8.0" + babel-preset-es2015 "^6.9.0" + babel-preset-stage-1 "^6.5.0" + babel-register "^6.9.0" + babylon "^6.17.3" + colors "^1.1.2" + flow-parser "^0.*" + lodash "^4.13.1" + micromatch "^2.3.7" + node-dir "0.1.8" + nomnom "^1.8.1" + recast "^0.12.5" + temp "^0.8.1" + write-file-atomic "^1.2.0" + +jscodeshift@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.5.0.tgz#bdb7b6cc20dd62c16aa728c3fa2d2fe66ca7c748" + dependencies: + babel-plugin-transform-flow-strip-types "^6.8.0" + babel-preset-es2015 "^6.9.0" + babel-preset-stage-1 "^6.5.0" + babel-register "^6.9.0" + babylon "^7.0.0-beta.30" + colors "^1.1.2" + flow-parser "^0.*" + lodash "^4.13.1" + micromatch "^2.3.7" + neo-async "^2.5.0" + node-dir "0.1.8" + nomnom "^1.8.1" + recast "^0.14.1" + temp "^0.8.1" + write-file-atomic "^1.2.0" + jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" @@ -4682,9 +5051,9 @@ json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" -json-loader@^0.5.4: - version "0.5.7" - resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" json-schema-traverse@^0.3.0: version "0.3.1" @@ -4905,6 +5274,54 @@ lie@~3.1.0: dependencies: immediate "~3.0.5" +listr-silent-renderer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" + +listr-update-renderer@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7" + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + elegant-spinner "^1.0.1" + figures "^1.7.0" + indent-string "^3.0.0" + log-symbols "^1.0.2" + log-update "^1.0.2" + strip-ansi "^3.0.1" + +listr-verbose-renderer@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35" + dependencies: + chalk "^1.1.3" + cli-cursor "^1.0.2" + date-fns "^1.27.2" + figures "^1.7.0" + +listr@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/listr/-/listr-0.13.0.tgz#20bb0ba30bae660ee84cc0503df4be3d5623887d" + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + figures "^1.7.0" + indent-string "^2.1.0" + is-observable "^0.2.0" + is-promise "^2.1.0" + is-stream "^1.1.0" + listr-silent-renderer "^1.1.1" + listr-update-renderer "^0.4.0" + listr-verbose-renderer "^0.4.0" + log-symbols "^1.0.2" + log-update "^1.0.2" + ora "^0.2.3" + p-map "^1.1.1" + rxjs "^5.4.2" + stream-to-observable "^0.2.0" + strip-ansi "^3.0.1" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -4915,28 +5332,19 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" dependencies: graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" + parse-json "^4.0.0" + pify "^3.0.0" strip-bom "^3.0.0" loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" -loader-utils@^0.2.15: - version "0.2.16" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d" - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - object-assign "^4.0.1" - loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" @@ -5055,16 +5463,39 @@ lodash@4.17.4: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: +lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: version "4.17.5" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" +lodash@^4.17.10: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + +log-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" + dependencies: + chalk "^1.0.0" + log-symbols@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.1.0.tgz#f35fa60e278832b538dc4dddcbb478a45d3e3be6" dependencies: chalk "^2.0.1" +log-symbols@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + dependencies: + chalk "^2.0.1" + +log-update@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1" + dependencies: + ansi-escapes "^1.0.0" + cli-cursor "^1.0.2" + log4js@^2.3.9: version "2.5.3" resolved "https://registry.yarnpkg.com/log4js/-/log4js-2.5.3.tgz#38bb7bde5e9c1c181bd75e8bc128c5cd0409caf1" @@ -5097,11 +5528,8 @@ loglevel@^1.4.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.4.1.tgz#95b383f91a3c2756fd4ab093667e4309161f2bcd" loglevelnext@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.5.tgz#36fc4f5996d6640f539ff203ba819641680d75a2" - dependencies: - es6-symbol "^3.1.1" - object.assign "^4.1.0" + version "1.0.3" + resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.3.tgz#0f69277e73bbbf2cd61b94d82313216bf87ac66e" longest@^1.0.1: version "1.0.1" @@ -5170,6 +5598,12 @@ make-dir@^1.0.0: dependencies: pify "^2.3.0" +make-dir@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b" + dependencies: + pify "^3.0.0" + map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -5211,6 +5645,30 @@ media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" +mem-fs-editor@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-4.0.1.tgz#27e6b59df91b37248e9be2145b1bea84695103ed" + dependencies: + commondir "^1.0.1" + deep-extend "^0.5.1" + ejs "^2.5.9" + glob "^7.0.3" + globby "^8.0.0" + isbinaryfile "^3.0.2" + mkdirp "^0.5.0" + multimatch "^2.0.0" + rimraf "^2.2.8" + through2 "^2.0.0" + vinyl "^2.0.1" + +mem-fs@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/mem-fs/-/mem-fs-1.1.3.tgz#b8ae8d2e3fcb6f5d3f9165c12d4551a065d989cc" + dependencies: + through2 "^2.0.0" + vinyl "^1.1.0" + vinyl-file "^2.0.0" + mem@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" @@ -5247,11 +5705,15 @@ merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" +merge2@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.2.tgz#03212e3da8d86c4d8523cebd6318193414f94e34" + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" -micromatch@^2.1.5, micromatch@^2.3.11: +micromatch@^2.1.5, micromatch@^2.3.7: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" dependencies: @@ -5269,6 +5731,24 @@ micromatch@^2.1.5, micromatch@^2.3.11: parse-glob "^3.0.4" regex-cache "^0.4.2" +micromatch@^3.1.10, micromatch@^3.1.9: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + micromatch@^3.1.4: version "3.1.6" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.6.tgz#8d7c043b48156f408ca07a4715182b79b99420bf" @@ -5288,8 +5768,8 @@ micromatch@^3.1.4: to-regex "^3.0.1" micromatch@^3.1.8: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + version "3.1.9" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.9.tgz#15dc93175ae39e52e93087847096effc73efcf89" dependencies: arr-diff "^4.0.0" array-unique "^0.3.2" @@ -5303,7 +5783,7 @@ micromatch@^3.1.8: object.pick "^1.3.0" regex-not "^1.0.0" snapdragon "^0.8.1" - to-regex "^3.0.2" + to-regex "^3.0.1" miller-rabin@^4.0.0: version "4.0.1" @@ -5326,14 +5806,18 @@ mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" -mime@^1.3.4, mime@^1.4.1, mime@^1.5.0: +mime@^1.3.4: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" -mime@^2.1.0: +mime@^2.0.3: version "2.3.1" resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" +mime@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.2.0.tgz#161e541965551d3b549fa1114391e3a3d55b923b" + mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -5356,16 +5840,14 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" -minimatch@3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" - dependencies: - brace-expansion "^1.0.0" - minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" +minimist@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de" + minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -5444,6 +5926,15 @@ multicast-dns@^6.0.1: dns-packet "^1.0.1" thunky "^0.1.0" +multimatch@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" + dependencies: + array-differ "^1.0.0" + array-union "^1.0.1" + arrify "^1.0.0" + minimatch "^3.0.0" + mute-stream@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" @@ -5452,10 +5943,6 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -name-all-modules-plugin@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/name-all-modules-plugin/-/name-all-modules-plugin-1.0.1.tgz#0abfb6ad835718b9fb4def0674e06657a954375c" - nan@^2.3.0: version "2.8.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" @@ -5485,10 +5972,22 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +neo-async@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.0.tgz#76b1c823130cca26acfbaccc8fbaf0a2fa33b18f" + netmask@~1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" +nice-try@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" + +node-dir@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.8.tgz#55fb8deb699070707fb67f91a460f0448294c77d" + node-forge@0.6.33: version "0.6.33" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc" @@ -5618,20 +6117,28 @@ nodemailer@^2.5.0: nodemailer-smtp-transport "2.7.2" socks "1.1.9" -nodemon@^1.15.1: - version "1.15.1" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.15.1.tgz#54daa72443d8d5a548f130866b92e65cded0ed58" +nodemon@^1.17.3: + version "1.17.3" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.17.3.tgz#3b0bbc2ee05ccb43b1aef15ba05c63c7bc9b8530" dependencies: chokidar "^2.0.2" debug "^3.1.0" ignore-by-default "^1.0.1" minimatch "^3.0.4" pstree.remy "^1.1.0" - semver "^5.4.1" + semver "^5.5.0" + supports-color "^5.2.0" touch "^3.1.0" - undefsafe "^2.0.1" + undefsafe "^2.0.2" update-notifier "^2.3.0" +nomnom@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" + dependencies: + chalk "~0.4.0" + underscore "~1.6.0" + nopt@3.x: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -5734,7 +6241,7 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-keys@^1.0.11, object-keys@^1.0.8: +object-keys@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" @@ -5744,15 +6251,6 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -5800,7 +6298,7 @@ opener@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" -opn@5.2.0, opn@^5.1.0: +opn@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225" dependencies: @@ -5824,6 +6322,15 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" +ora@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" + dependencies: + chalk "^1.1.1" + cli-cursor "^1.0.2" + cli-spinners "^0.1.2" + object-assign "^4.0.1" + original@>=0.0.5: version "1.0.0" resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b" @@ -5838,12 +6345,6 @@ os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - dependencies: - lcid "^1.0.0" - os-locale@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" @@ -5863,10 +6364,20 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-cancelable@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" + p-cancelable@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" +p-each-series@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" + dependencies: + p-reduce "^1.0.0" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -5875,6 +6386,10 @@ p-is-promise@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" +p-lazy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-lazy/-/p-lazy-1.0.0.tgz#ec53c802f2ee3ac28f166cc82d0b2b02de27a835" + p-limit@^1.0.0, p-limit@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" @@ -5891,6 +6406,16 @@ p-map@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a" +p-reduce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" + +p-timeout@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386" + dependencies: + p-finally "^1.0.0" + p-timeout@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" @@ -5975,6 +6500,13 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + parse-passwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" @@ -6025,7 +6557,7 @@ path-is-inside@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" -path-key@^2.0.0: +path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -6051,12 +6583,6 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - dependencies: - pify "^2.0.0" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -6442,9 +6968,17 @@ prettier@1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75" +prettier@^1.5.3: + version "1.10.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93" + prettier@^1.7.0: - version "1.12.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325" + version "1.8.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8" + +pretty-bytes@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" prismjs@^1.6.0: version "1.6.0" @@ -6452,11 +6986,11 @@ prismjs@^1.6.0: optionalDependencies: clipboard "^1.5.5" -private@^0.1.6, private@^0.1.7: +private@^0.1.6, private@^0.1.8, private@~0.1.5: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" -process-nextick-args@~1.0.6: +process-nextick-args@^1.0.6, process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" @@ -6500,6 +7034,10 @@ prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + ps-tree@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014" @@ -6660,32 +7198,12 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dev-utils@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.0.tgz#425ac7c9c40c2603bc4f7ab8836c1406e96bb473" - dependencies: - address "1.0.3" - babel-code-frame "6.26.0" - chalk "1.1.3" - cross-spawn "5.1.0" - detect-port-alt "1.1.5" - escape-string-regexp "1.0.5" - filesize "3.5.11" - global-modules "1.0.0" - gzip-size "3.0.0" - inquirer "3.3.0" - is-root "1.0.0" - opn "5.2.0" - react-error-overlay "^4.0.0" - recursive-readdir "2.2.1" - shell-quote "1.6.1" - sockjs-client "1.1.4" - strip-ansi "3.0.1" - text-table "0.2.0" - -react-error-overlay@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4" +read-chunk@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-2.1.0.tgz#6a04c0928005ed9d42e1a6ac5600e19cbc7ff655" + dependencies: + pify "^3.0.0" + safe-buffer "^5.1.1" read-pkg-up@^1.0.1: version "1.0.1" @@ -6694,12 +7212,12 @@ read-pkg-up@^1.0.1: find-up "^1.0.0" read-pkg "^1.0.0" -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" +read-pkg-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" dependencies: find-up "^2.0.0" - read-pkg "^2.0.0" + read-pkg "^3.0.0" read-pkg@^1.0.0: version "1.1.0" @@ -6709,13 +7227,13 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" dependencies: - load-json-file "^2.0.0" + load-json-file "^4.0.0" normalize-package-data "^2.3.2" - path-type "^2.0.0" + path-type "^3.0.0" "readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3: version "2.3.4" @@ -6766,18 +7284,31 @@ readline2@^1.0.1: is-fullwidth-code-point "^1.0.0" mute-stream "0.0.5" +recast@^0.12.5: + version "0.12.9" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.12.9.tgz#e8e52bdb9691af462ccbd7c15d5a5113647a15f1" + dependencies: + ast-types "0.10.1" + core-js "^2.4.1" + esprima "~4.0.0" + private "~0.1.5" + source-map "~0.6.1" + +recast@^0.14.1: + version "0.14.7" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d" + dependencies: + ast-types "0.11.3" + esprima "~4.0.0" + private "~0.1.5" + source-map "~0.6.1" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" dependencies: resolve "^1.1.6" -recursive-readdir@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99" - dependencies: - minimatch "3.0.3" - redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -6905,6 +7436,14 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" +replace-ext@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" + +replace-ext@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" + request@2.75.x: version "2.75.0" resolved "https://registry.yarnpkg.com/request/-/request-2.75.0.tgz#d2b8268a286da13eaa5d01adf5d18cc90f657d93" @@ -7092,12 +7631,22 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: +rimraf@2, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: glob "^7.0.5" +rimraf@^2.2.8: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + +rimraf@~2.2.6: + version "2.2.8" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" @@ -7111,7 +7660,7 @@ run-async@^0.1.0: dependencies: once "^1.3.0" -run-async@^2.2.0: +run-async@^2.0.0, run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" dependencies: @@ -7137,6 +7686,12 @@ rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" +rxjs@^5.4.2, rxjs@^5.5.2: + version "5.5.10" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.10.tgz#fde02d7a614f6c8683d0d1957827f492e09db045" + dependencies: + symbol-observable "1.0.1" + safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -7163,19 +7718,17 @@ sax@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828" -schema-utils@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" - dependencies: - ajv "^5.0.0" - -schema-utils@^0.4.3, schema-utils@^0.4.5: +schema-utils@^0.4.0, schema-utils@^0.4.3, schema-utils@^0.4.4, schema-utils@^0.4.5: version "0.4.5" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" dependencies: ajv "^6.1.0" ajv-keywords "^3.1.0" +scoped-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -7200,7 +7753,11 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1: +"semver@2 || 3 || 4 || 5", semver@^5.0.3: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +semver@^5.1.0, semver@^5.3.0, semver@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" @@ -7312,15 +7869,6 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" -shell-quote@1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" - dependencies: - array-filter "~0.0.0" - array-map "~0.0.0" - array-reduce "~0.0.0" - jsonify "~0.0.0" - shelljs@^0.7.5: version "0.7.8" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" @@ -7329,6 +7877,14 @@ shelljs@^0.7.5: interpret "^1.0.0" rechoir "^0.6.2" +shelljs@^0.8.0: + version "0.8.1" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.1.tgz#729e038c413a2254c4078b95ed46e0397154a9f1" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -7347,6 +7903,10 @@ slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" +slide@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + smart-buffer@^1.0.13, smart-buffer@^1.0.4: version "1.1.15" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16" @@ -7528,7 +8088,11 @@ source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" -source-map@^0.6.1: +source-map@^0.5.7, source-map@~0.5.3, source-map@~0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -7538,10 +8102,6 @@ source-map@~0.2.0: dependencies: amdefine ">=0.0.4" -source-map@~0.5.3, source-map@~0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - spdx-correct@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" @@ -7675,6 +8235,12 @@ stream-shift@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" +stream-to-observable@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.2.0.tgz#59d6ea393d87c2c0ddac10aa0d561bc6ba6f0e10" + dependencies: + any-observable "^0.2.0" + streamroller@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b" @@ -7688,6 +8254,10 @@ strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" +string-template@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -7717,7 +8287,7 @@ stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" -strip-ansi@3.0.1, strip-ansi@^3.0.0, strip-ansi@^3.0.1: +strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" dependencies: @@ -7729,6 +8299,17 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" + +strip-bom-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca" + dependencies: + first-chunk-stream "^2.0.0" + strip-bom "^2.0.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -7753,12 +8334,12 @@ strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" -style-loader@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.20.2.tgz#851b373c187890331776e9cde359eea9c95ecd00" +style-loader@^0.21.0: + version "0.21.0" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.21.0.tgz#68c52e5eb2afc9ca92b6274be277ee59aea3a852" dependencies: loader-utils "^1.1.0" - schema-utils "^0.4.3" + schema-utils "^0.4.5" supports-color@^2.0.0: version "2.0.0" @@ -7770,13 +8351,13 @@ supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3: dependencies: has-flag "^1.0.0" -supports-color@^4.2.1: - version "4.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" +supports-color@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5" dependencies: has-flag "^2.0.0" -supports-color@^5.1.0, supports-color@^5.2.0: +supports-color@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a" dependencies: @@ -7804,6 +8385,14 @@ svgo@^0.7.0: sax "~1.2.1" whet.extend "~0.9.9" +symbol-observable@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" + +symbol-observable@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" + table@^3.7.8: version "3.8.3" resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" @@ -7819,9 +8408,9 @@ tapable@^0.1.8: version "0.1.10" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" -tapable@^0.2.7: - version "0.2.8" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" +tapable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2" tar-pack@^3.4.0: version "3.4.1" @@ -7844,6 +8433,13 @@ tar@^2.2.1: fstream "^1.0.2" inherits "2" +temp@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" + dependencies: + os-tmpdir "^1.0.0" + rimraf "~2.2.6" + term-size@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" @@ -7860,10 +8456,14 @@ test-exclude@^4.2.1: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" -text-table@0.2.0, text-table@~0.2.0: +text-table@^0.2.0, text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" +textextensions@2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286" + three-orbit-controls@^82.1.0: version "82.1.0" resolved "https://registry.yarnpkg.com/three-orbit-controls/-/three-orbit-controls-82.1.0.tgz#11a7f33d0a20ecec98f098b37780f6537374fab4" @@ -7876,7 +8476,7 @@ three@^0.84.0: version "0.84.0" resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918" -through2@^2.0.0: +through2@^2.0.0, through2@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" dependencies: @@ -7895,10 +8495,6 @@ thunky@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e" -time-stamp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357" - timeago.js@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-3.0.2.tgz#32a67e7c0d887ea42ca588d3aae26f77de5e76cc" @@ -8052,7 +8648,14 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -uglify-js@^2.6, uglify-js@^2.8.29: +uglify-es@^3.3.4: + version "3.3.9" + resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" + dependencies: + commander "~2.13.0" + source-map "~0.6.1" + +uglify-js@^2.6: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" dependencies: @@ -8065,13 +8668,18 @@ uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" -uglifyjs-webpack-plugin@^0.4.6: - version "0.4.6" - resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" +uglifyjs-webpack-plugin@^1.2.4: + version "1.2.5" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.5.tgz#2ef8387c8f1a903ec5e44fa36f9f3cbdcea67641" dependencies: - source-map "^0.5.6" - uglify-js "^2.8.29" - webpack-sources "^1.0.1" + cacache "^10.0.4" + find-cache-dir "^1.0.0" + schema-utils "^0.4.5" + serialize-javascript "^1.4.0" + source-map "^0.6.1" + uglify-es "^3.3.4" + webpack-sources "^1.1.0" + worker-farm "^1.5.2" uid-number@^0.0.6: version "0.0.6" @@ -8085,7 +8693,7 @@ unc-path-regex@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" -undefsafe@^2.0.1: +undefsafe@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76" dependencies: @@ -8095,6 +8703,10 @@ underscore@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.0.tgz#31dbb314cfcc88f169cd3692d9149d81a00a73e4" +underscore@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" + underscore@~1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209" @@ -8151,6 +8763,10 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +untildify@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.2.tgz#7f1f302055b3fea0f3e81dc78eb36766cb65e3f1" + unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" @@ -8186,13 +8802,17 @@ url-join@^2.0.2: version "2.0.5" resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728" -url-loader@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7" +url-join@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a" + +url-loader@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.0.1.tgz#61bc53f1f184d7343da2728a1289ef8722ea45ee" dependencies: - loader-utils "^1.0.2" - mime "^1.4.1" - schema-utils "^0.3.0" + loader-utils "^1.1.0" + mime "^2.0.3" + schema-utils "^0.4.3" url-parse-lax@^1.0.0: version "1.0.0" @@ -8274,6 +8894,10 @@ uws@~9.14.0: version "9.14.0" resolved "https://registry.yarnpkg.com/uws/-/uws-9.14.0.tgz#fac8386befc33a7a3705cbd58dc47b430ca4dd95" +v8-compile-cache@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-1.1.2.tgz#8d32e4f16974654657e676e0e467a348e89b0dc4" + validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" @@ -8301,6 +8925,36 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vinyl-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a" + dependencies: + graceful-fs "^4.1.2" + pify "^2.3.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + strip-bom-stream "^2.0.0" + vinyl "^1.1.0" + +vinyl@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" + dependencies: + clone "^1.0.0" + clone-stats "^0.0.1" + replace-ext "0.0.1" + +vinyl@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.1.0.tgz#021f9c2cf951d6b939943c89eb5ee5add4fd924c" + dependencies: + clone "^2.1.1" + clone-buffer "^1.0.0" + clone-stats "^1.0.0" + cloneable-readable "^1.0.0" + remove-trailing-separator "^1.0.1" + replace-ext "^1.0.0" + visibilityjs@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/visibilityjs/-/visibilityjs-1.2.4.tgz#bff8663da62c8c10ad4ee5ae6a1ae6fac4259d63" @@ -8388,13 +9042,13 @@ vuex@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2" -watchpack@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" +watchpack@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.5.0.tgz#231e783af830a22f8966f65c4c4bacc814072eed" dependencies: - async "^2.1.2" - chokidar "^1.7.0" + chokidar "^2.0.2" graceful-fs "^4.1.2" + neo-async "^2.5.0" wbuf@^1.1.0, wbuf@^1.7.2: version "1.7.2" @@ -8402,9 +9056,15 @@ wbuf@^1.1.0, wbuf@^1.7.2: dependencies: minimalistic-assert "^1.0.0" -webpack-bundle-analyzer@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.10.0.tgz#d0646cda342939f6f05eb632a090abbd90317446" +webpack-addons@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/webpack-addons/-/webpack-addons-1.1.5.tgz#2b178dfe873fb6e75e40a819fa5c26e4a9bc837a" + dependencies: + jscodeshift "^0.4.0" + +webpack-bundle-analyzer@^2.11.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.11.1.tgz#b9fbfb6a32c0a8c1c3237223e90890796b950ab9" dependencies: acorn "^5.3.0" bfj-node4 "^5.2.0" @@ -8419,15 +9079,48 @@ webpack-bundle-analyzer@^2.10.0: opener "^1.4.3" ws "^4.0.0" -webpack-dev-middleware@1.12.2: - version "1.12.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e" +webpack-cli@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-2.1.2.tgz#9c9a4b90584f7b8acaf591238ef0667e04c817f6" + dependencies: + chalk "^2.3.2" + cross-spawn "^6.0.5" + diff "^3.5.0" + enhanced-resolve "^4.0.0" + envinfo "^4.4.2" + glob-all "^3.1.0" + global-modules "^1.0.0" + got "^8.2.0" + import-local "^1.0.0" + inquirer "^5.1.0" + interpret "^1.0.4" + jscodeshift "^0.5.0" + listr "^0.13.0" + loader-utils "^1.1.0" + lodash "^4.17.5" + log-symbols "^2.2.0" + mkdirp "^0.5.1" + p-each-series "^1.0.0" + p-lazy "^1.0.0" + prettier "^1.5.3" + supports-color "^5.3.0" + v8-compile-cache "^1.1.2" + webpack-addons "^1.1.5" + yargs "^11.1.0" + yeoman-environment "^2.0.0" + yeoman-generator "^2.0.4" + +webpack-dev-middleware@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.1.3.tgz#8b32aa43da9ae79368c1bf1183f2b6cf5e1f39ed" dependencies: + loud-rejection "^1.6.0" memory-fs "~0.4.1" - mime "^1.5.0" + mime "^2.1.0" path-is-absolute "^1.0.0" range-parser "^1.0.3" - time-stamp "^2.0.0" + url-join "^4.0.0" + webpack-log "^1.0.1" webpack-dev-middleware@^2.0.6: version "2.0.6" @@ -8441,9 +9134,9 @@ webpack-dev-middleware@^2.0.6: url-join "^2.0.2" webpack-log "^1.0.1" -webpack-dev-server@^2.11.2: - version "2.11.2" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.2.tgz#1f4f4c78bf1895378f376815910812daf79a216f" +webpack-dev-server@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.4.tgz#9a08d13c4addd1e3b6d8ace116e86715094ad5b4" dependencies: ansi-html "0.0.7" array-includes "^3.0.3" @@ -8455,7 +9148,7 @@ webpack-dev-server@^2.11.2: del "^3.0.0" express "^4.16.2" html-entities "^1.2.0" - http-proxy-middleware "~0.17.4" + http-proxy-middleware "~0.18.0" import-local "^1.0.0" internal-ip "1.2.0" ip "^1.1.5" @@ -8470,8 +9163,9 @@ webpack-dev-server@^2.11.2: spdy "^3.4.1" strip-ansi "^3.0.0" supports-color "^5.1.0" - webpack-dev-middleware "1.12.2" - yargs "6.6.0" + webpack-dev-middleware "3.1.3" + webpack-log "^1.1.2" + yargs "11.0.0" webpack-log@^1.0.1: version "1.2.0" @@ -8482,6 +9176,15 @@ webpack-log@^1.0.1: loglevelnext "^1.0.1" uuid "^3.1.0" +webpack-log@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-1.1.2.tgz#cdc76016537eed24708dc6aa3d1e52189efee107" + dependencies: + chalk "^2.1.0" + log-symbols "^2.1.0" + loglevelnext "^1.0.1" + uuid "^3.1.0" + webpack-sources@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" @@ -8489,36 +9192,40 @@ webpack-sources@^1.0.1: source-list-map "^2.0.0" source-map "~0.5.3" -webpack-stats-plugin@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.1.5.tgz#29e5f12ebfd53158d31d656a113ac1f7b86179d9" +webpack-sources@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" -webpack@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.11.0.tgz#77da451b1d7b4b117adaf41a1a93b5742f24d894" +webpack-stats-plugin@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.2.1.tgz#1f5bac13fc25d62cbb5fd0ff646757dc802b8595" + +webpack@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.7.0.tgz#a04f68dab86d5545fd0277d07ffc44e4078154c9" dependencies: acorn "^5.0.0" - acorn-dynamic-import "^2.0.0" + acorn-dynamic-import "^3.0.0" ajv "^6.1.0" ajv-keywords "^3.1.0" - async "^2.1.2" - enhanced-resolve "^3.4.0" - escope "^3.6.0" - interpret "^1.0.0" - json-loader "^0.5.4" - json5 "^0.5.1" + chrome-trace-event "^0.1.1" + enhanced-resolve "^4.0.0" + eslint-scope "^3.7.1" loader-runner "^2.3.0" loader-utils "^1.1.0" memory-fs "~0.4.1" + micromatch "^3.1.8" mkdirp "~0.5.0" + neo-async "^2.5.0" node-libs-browser "^2.0.0" - source-map "^0.5.3" - supports-color "^4.2.1" - tapable "^0.2.7" - uglifyjs-webpack-plugin "^0.4.6" - watchpack "^1.4.0" + schema-utils "^0.4.4" + tapable "^1.0.0" + uglifyjs-webpack-plugin "^1.2.4" + watchpack "^1.5.0" webpack-sources "^1.0.1" - yargs "^8.0.2" websocket-driver@>=0.5.1: version "0.6.5" @@ -8538,10 +9245,6 @@ whet.extend@~0.9.9: version "0.9.9" resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -8580,12 +9283,19 @@ wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" -worker-loader@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-1.1.0.tgz#8cf21869a07add84d66f821d948d23c1eb98e809" +worker-farm@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae" + dependencies: + errno "^0.1.4" + xtend "^4.0.1" + +worker-loader@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-1.1.1.tgz#920d74ddac6816fc635392653ed8b4af1929fd92" dependencies: loader-utils "^1.0.0" - schema-utils "^0.3.0" + schema-utils "^0.4.0" wrap-ansi@^2.0.0: version "2.1.0" @@ -8598,6 +9308,14 @@ wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" +write-file-atomic@^1.2.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + slide "^1.1.5" + write-file-atomic@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" @@ -8640,7 +9358,7 @@ xregexp@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" -xtend@^4.0.0, xtend@~4.0.1: +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -8656,53 +9374,51 @@ yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" -yargs-parser@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" - dependencies: - camelcase "^3.0.0" - -yargs-parser@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" +yargs-parser@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" dependencies: camelcase "^4.1.0" -yargs@6.6.0: - version "6.6.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" +yargs@11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b" dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" + cliui "^4.0.0" decamelize "^1.1.1" + find-up "^2.1.0" get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" + os-locale "^2.0.0" require-directory "^2.1.1" require-main-filename "^1.0.1" set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" + string-width "^2.0.0" + which-module "^2.0.0" y18n "^3.2.1" - yargs-parser "^4.2.0" + yargs-parser "^9.0.2" -yargs@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" +yargs@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77" dependencies: - camelcase "^4.1.0" - cliui "^3.2.0" + cliui "^4.0.0" decamelize "^1.1.1" + find-up "^2.1.0" get-caller-file "^1.0.1" os-locale "^2.0.0" - read-pkg-up "^2.0.0" require-directory "^2.1.1" require-main-filename "^1.0.1" set-blocking "^2.0.0" string-width "^2.0.0" which-module "^2.0.0" y18n "^3.2.1" - yargs-parser "^7.0.0" + yargs-parser "^9.0.2" + +yargs@~1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.2.6.tgz#9c7b4a82fd5d595b2bf17ab6dcc43135432fe34b" + dependencies: + minimist "^0.1.0" yargs@~3.10.0: version "3.10.0" @@ -8716,3 +9432,69 @@ yargs@~3.10.0: yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + +yeoman-environment@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.0.5.tgz#84f22bafa84088971fe99ea85f654a3a3dd2b693" + dependencies: + chalk "^2.1.0" + debug "^3.1.0" + diff "^3.3.1" + escape-string-regexp "^1.0.2" + globby "^6.1.0" + grouped-queue "^0.3.3" + inquirer "^3.3.0" + is-scoped "^1.0.0" + lodash "^4.17.4" + log-symbols "^2.1.0" + mem-fs "^1.1.0" + text-table "^0.2.0" + untildify "^3.0.2" + +yeoman-environment@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.0.6.tgz#ae1b21d826b363f3d637f88a7fc9ea7414cb5377" + dependencies: + chalk "^2.1.0" + debug "^3.1.0" + diff "^3.3.1" + escape-string-regexp "^1.0.2" + globby "^6.1.0" + grouped-queue "^0.3.3" + inquirer "^3.3.0" + is-scoped "^1.0.0" + lodash "^4.17.4" + log-symbols "^2.1.0" + mem-fs "^1.1.0" + text-table "^0.2.0" + untildify "^3.0.2" + +yeoman-generator@^2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/yeoman-generator/-/yeoman-generator-2.0.5.tgz#57b0b3474701293cc9ec965288f3400b00887c81" + dependencies: + async "^2.6.0" + chalk "^2.3.0" + cli-table "^0.3.1" + cross-spawn "^6.0.5" + dargs "^5.1.0" + dateformat "^3.0.3" + debug "^3.1.0" + detect-conflict "^1.0.0" + error "^7.0.2" + find-up "^2.1.0" + github-username "^4.0.0" + istextorbinary "^2.2.1" + lodash "^4.17.10" + make-dir "^1.1.0" + mem-fs-editor "^4.0.0" + minimist "^1.2.0" + pretty-bytes "^4.0.2" + read-chunk "^2.1.0" + read-pkg-up "^3.0.0" + rimraf "^2.6.2" + run-async "^2.0.0" + shelljs "^0.8.0" + text-table "^0.2.0" + through2 "^2.0.0" + yeoman-environment "^2.0.5" |
