diff options
123 files changed, 1452 insertions, 780 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5ea1d89e659..50f1f65e4e8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,6 +11,7 @@ variables: NODE_ENV: "test" SIMPLECOV: "true" GIT_DEPTH: "20" + GIT_SUBMODULE_STRATEGY: "none" PHANTOMJS_VERSION: "2.1.1" GET_SOURCES_ATTEMPTS: "3" KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index 6bb21e6a3af..241d90a204b 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -26,6 +26,7 @@ logs, and code as it's very hard to read otherwise.) #### Results of GitLab environment info <details> +<pre> (For installations with omnibus-gitlab package run and paste the output of: `sudo gitlab-rake gitlab:env:info`) @@ -33,11 +34,13 @@ logs, and code as it's very hard to read otherwise.) (For installations from source run and paste the output of: `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`) +</pre> </details> #### Results of GitLab application Check <details> +<pre> (For installations with omnibus-gitlab package run and paste the output of: `sudo gitlab-rake gitlab:check SANITIZE=true`) @@ -47,6 +50,7 @@ logs, and code as it's very hard to read otherwise.) (we will only investigate if the tests are passing) +</pre> </details> ### Possible fixes @@ -144,7 +144,7 @@ gem 'after_commit_queue', '~> 1.3.0' gem 'acts-as-taggable-on', '~> 4.0' # Background jobs -gem 'sidekiq', '~> 4.2.7' +gem 'sidekiq', '~> 5.0' gem 'sidekiq-cron', '~> 0.4.4' gem 'redis-namespace', '~> 1.5.2' gem 'sidekiq-limit_fetch', '~> 3.4' diff --git a/Gemfile.lock b/Gemfile.lock index fb11f590110..52707628748 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -429,7 +429,7 @@ GEM multi_json (~> 1.10) loofah (2.0.3) nokogiri (>= 1.5.9) - mail (2.6.4) + mail (2.6.5) mime-types (>= 1.16, < 4) mail_room (0.9.1) memoist (0.15.0) @@ -603,7 +603,7 @@ GEM json recursive-open-struct (1.0.0) redcarpet (3.4.0) - redis (3.2.2) + redis (3.3.3) redis-actionpack (5.0.1) actionpack (>= 4.0, < 6) redis-rack (>= 1, < 3) @@ -717,11 +717,11 @@ GEM rack shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (4.2.10) + sidekiq (5.0.0) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) - redis (~> 3.2, >= 3.2.1) + redis (~> 3.3, >= 3.3.3) sidekiq-cron (0.4.4) redis-namespace (>= 1.5.2) rufus-scheduler (>= 2.0.24) @@ -1007,7 +1007,7 @@ DEPENDENCIES settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) shoulda-matchers (~> 2.8.0) - sidekiq (~> 4.2.7) + sidekiq (~> 5.0) sidekiq-cron (~> 0.4.4) sidekiq-limit_fetch (~> 3.4) simplecov (~> 0.14.0) diff --git a/README.md b/README.md index 10d69efdc6b..59de828e1ac 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) +[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) ## Test coverage diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index 68a1c1de1df..e704be8b53e 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -106,15 +106,6 @@ export default Vue.component('pipelines-table', { eventHub.$on('refreshPipelines', this.fetchPipelines); }, - beforeUpdate() { - if (this.state.pipelines.length && - this.$children && - !this.isMakingRequest && - !this.isLoading) { - this.store.startTimeAgoLoops.call(this, Vue); - } - }, - beforeDestroyed() { eventHub.$off('refreshPipelines'); }, diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.vue index f7175e412da..f319d6ca0c8 100644 --- a/app/assets/javascripts/environments/components/environment.js +++ b/app/assets/javascripts/environments/components/environment.vue @@ -1,6 +1,7 @@ +<script> + /* eslint-disable no-new */ /* global Flash */ -import Vue from 'vue'; import EnvironmentsService from '../services/environments_service'; import EnvironmentTable from './environments_table.vue'; import EnvironmentsStore from '../stores/environments_store'; @@ -8,7 +9,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati import '../../lib/utils/common_utils'; import eventHub from '../event_hub'; -export default Vue.component('environment-component', { +export default { components: { 'environment-table': EnvironmentTable, @@ -140,76 +141,90 @@ export default Vue.component('environment-component', { }); }, }, - - template: ` - <div :class="cssContainerClass"> - <div class="top-area"> - <ul v-if="!isLoading" class="nav-links"> - <li v-bind:class="{ 'active': scope === null || scope === 'available' }"> - <a :href="projectEnvironmentsPath"> - Available - <span class="badge js-available-environments-count"> - {{state.availableCounter}} - </span> - </a> - </li> - <li v-bind:class="{ 'active' : scope === 'stopped' }"> - <a :href="projectStoppedEnvironmentsPath"> - Stopped - <span class="badge js-stopped-environments-count"> - {{state.stoppedCounter}} - </span> - </a> - </li> - </ul> - <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls"> - <a :href="newEnvironmentPath" class="btn btn-create"> - New environment +}; +</script> +<template> + <div :class="cssContainerClass"> + <div class="top-area"> + <ul + v-if="!isLoading" + class="nav-links"> + <li :class="{ active: scope === null || scope === 'available' }"> + <a :href="projectEnvironmentsPath"> + Available + <span class="badge js-available-environments-count"> + {{state.availableCounter}} + </span> + </a> + </li> + <li :class="{ active : scope === 'stopped' }"> + <a :href="projectStoppedEnvironmentsPath"> + Stopped + <span class="badge js-stopped-environments-count"> + {{state.stoppedCounter}} + </span> </a> - </div> + </li> + </ul> + <div + v-if="canCreateEnvironmentParsed && !isLoading" + class="nav-controls"> + <a + :href="newEnvironmentPath" + class="btn btn-create"> + New environment + </a> </div> + </div> + + <div class="content-list environments-container"> + <div + class="environments-list-loading text-center" + v-if="isLoading"> - <div class="content-list environments-container"> - <div class="environments-list-loading text-center" v-if="isLoading"> - <i class="fa fa-spinner fa-spin" aria-hidden="true"></i> - </div> - - <div class="blank-state blank-state-no-icon" - v-if="!isLoading && state.environments.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - You don't have any environments right now. - </h2> - <p class="blank-state-text"> - Environments are places where code gets deployed, such as staging or production. - <br /> - <a :href="helpPagePath"> - Read more about environments - </a> - </p> - - <a v-if="canCreateEnvironmentParsed" - :href="newEnvironmentPath" - class="btn btn-create js-new-environment-button"> - New Environment + <i + class="fa fa-spinner fa-spin" + aria-hidden="true" /> + </div> + + <div + class="blank-state blank-state-no-icon" + v-if="!isLoading && state.environments.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + You don't have any environments right now. + </h2> + <p class="blank-state-text"> + Environments are places where code gets deployed, such as staging or production. + <br /> + <a :href="helpPagePath"> + Read more about environments </a> - </div> - - <div class="table-holder" - v-if="!isLoading && state.environments.length > 0"> - - <environment-table - :environments="state.environments" - :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed" - :service="service" - :is-loading-folder-content="isLoadingFolderContent" /> - </div> - - <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" - :change="changePage" - :pageInfo="state.paginationInformation"> - </table-pagination> + </p> + + <a + v-if="canCreateEnvironmentParsed" + :href="newEnvironmentPath" + class="btn btn-create js-new-environment-button"> + New Environment + </a> </div> + + <div + class="table-holder" + v-if="!isLoading && state.environments.length > 0"> + + <environment-table + :environments="state.environments" + :can-create-deployment="canCreateDeploymentParsed" + :can-read-environment="canReadEnvironmentParsed" + :service="service" + :is-loading-folder-content="isLoadingFolderContent" /> + </div> + + <table-pagination + v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" + :change="changePage" + :pageInfo="state.paginationInformation" /> </div> - `, -}); + </div> +</template> diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js index 8d963b335cf..c0662125f28 100644 --- a/app/assets/javascripts/environments/environments_bundle.js +++ b/app/assets/javascripts/environments/environments_bundle.js @@ -1,13 +1,10 @@ -import EnvironmentsComponent from './components/environment'; +import Vue from 'vue'; +import EnvironmentsComponent from './components/environment.vue'; -$(() => { - window.gl = window.gl || {}; - - if (gl.EnvironmentsListApp) { - gl.EnvironmentsListApp.$destroy(true); - } - - gl.EnvironmentsListApp = new EnvironmentsComponent({ - el: document.querySelector('#environments-list-view'), - }); -}); +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#environments-list-view', + components: { + 'environments-table-app': EnvironmentsComponent, + }, + render: createElement => createElement('environments-table-app'), +})); diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index f939eccf246..9add8c3d721 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -1,13 +1,10 @@ -import EnvironmentsFolderComponent from './environments_folder_view'; +import Vue from 'vue'; +import EnvironmentsFolderComponent from './environments_folder_view.vue'; -$(() => { - window.gl = window.gl || {}; - - if (gl.EnvironmentsListFolderApp) { - gl.EnvironmentsListFolderApp.$destroy(true); - } - - gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({ - el: document.querySelector('#environments-folder-list-view'), - }); -}); +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#environments-folder-list-view', + components: { + 'environments-folder-app': EnvironmentsFolderComponent, + }, + render: createElement => createElement('environments-folder-app'), +})); diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 05d44f77d1d..d27b2acfcdf 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.js +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -1,6 +1,6 @@ +<script> /* eslint-disable no-new */ /* global Flash */ -import Vue from 'vue'; import EnvironmentsService from '../services/environments_service'; import EnvironmentTable from '../components/environments_table.vue'; import EnvironmentsStore from '../stores/environments_store'; @@ -8,7 +8,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati import '../../lib/utils/common_utils'; import '../../vue_shared/vue_resource_interceptor'; -export default Vue.component('environment-folder-view', { +export default { components: { 'environment-table': EnvironmentTable, 'table-pagination': TablePaginationComponent, @@ -116,54 +116,66 @@ export default Vue.component('environment-folder-view', { return param; }, }, +}; +</script> +<template> + <div :class="cssContainerClass"> + <div + class="top-area" + v-if="!isLoading"> + + <h4 class="js-folder-name environments-folder-name"> + Environments / <b>{{folderName}}</b> + </h4> + + <ul class="nav-links"> + <li :class="{ active: scope === null || scope === 'available' }"> + <a + :href="availablePath" + class="js-available-environments-folder-tab"> + Available + <span class="badge js-available-environments-count"> + {{state.availableCounter}} + </span> + </a> + </li> + <li :class="{ active : scope === 'stopped' }"> + <a + :href="stoppedPath" + class="js-stopped-environments-folder-tab"> + Stopped + <span class="badge js-stopped-environments-count"> + {{state.stoppedCounter}} + </span> + </a> + </li> + </ul> + </div> - template: ` - <div :class="cssContainerClass"> - <div class="top-area" v-if="!isLoading"> - - <h4 class="js-folder-name environments-folder-name"> - Environments / <b>{{folderName}}</b> - </h4> - - <ul class="nav-links"> - <li v-bind:class="{ 'active': scope === null || scope === 'available' }"> - <a :href="availablePath" class="js-available-environments-folder-tab"> - Available - <span class="badge js-available-environments-count"> - {{state.availableCounter}} - </span> - </a> - </li> - <li v-bind:class="{ 'active' : scope === 'stopped' }"> - <a :href="stoppedPath" class="js-stopped-environments-folder-tab"> - Stopped - <span class="badge js-stopped-environments-count"> - {{state.stoppedCounter}} - </span> - </a> - </li> - </ul> + <div class="environments-container"> + <div + class="environments-list-loading text-center" + v-if="isLoading"> + <i + class="fa fa-spinner fa-spin" + aria-hidden="true"/> </div> - <div class="environments-container"> - <div class="environments-list-loading text-center" v-if="isLoading"> - <i class="fa fa-spinner fa-spin"></i> - </div> - - <div class="table-holder" - v-if="!isLoading && state.environments.length > 0"> + <div + class="table-holder" + v-if="!isLoading && state.environments.length > 0"> - <environment-table - :environments="state.environments" - :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed" - :service="service"/> + <environment-table + :environments="state.environments" + :can-create-deployment="canCreateDeploymentParsed" + :can-read-environment="canReadEnvironmentParsed" + :service="service"/> - <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" - :change="changePage" - :pageInfo="state.paginationInformation"/> - </div> + <table-pagination + v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" + :change="changePage" + :pageInfo="state.paginationInformation"/> </div> </div> - `, -}); + </div> +</template> diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 68a832102a0..36af0674ac6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -77,13 +77,14 @@ class FilteredSearchManager { this.checkForEnterWrapper = this.checkForEnter.bind(this); this.onClearSearchWrapper = this.onClearSearch.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); - this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this); + this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this); this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); this.editTokenWrapper = this.editToken.bind(this); this.tokenChange = this.tokenChange.bind(this); this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this); this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this); this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this); + this.removeTokenWrapper = this.removeToken.bind(this); this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); @@ -96,12 +97,13 @@ class FilteredSearchManager { this.filteredSearchInput.addEventListener('keyup', this.tokenChange); this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper); this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken); + this.tokensContainer.addEventListener('click', this.removeTokenWrapper); this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper); document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.addEventListener('click', this.unselectEditTokensWrapper); document.addEventListener('click', this.removeInputContainerFocusWrapper); - document.addEventListener('keydown', this.removeSelectedTokenWrapper); + document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper); eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); } @@ -117,12 +119,13 @@ class FilteredSearchManager { this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper); this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken); + this.tokensContainer.removeEventListener('click', this.removeTokenWrapper); this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper); document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.removeEventListener('click', this.unselectEditTokensWrapper); document.removeEventListener('click', this.removeInputContainerFocusWrapper); - document.removeEventListener('keydown', this.removeSelectedTokenWrapper); + document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper); eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); } @@ -195,14 +198,28 @@ class FilteredSearchManager { static selectToken(e) { const button = e.target.closest('.selectable'); + const removeButtonSelected = e.target.closest('.remove-token'); - if (button) { + if (!removeButtonSelected && button) { e.preventDefault(); e.stopPropagation(); gl.FilteredSearchVisualTokens.selectToken(button); } } + removeToken(e) { + const removeButtonSelected = e.target.closest('.remove-token'); + + if (removeButtonSelected) { + e.preventDefault(); + e.stopPropagation(); + + const button = e.target.closest('.selectable'); + gl.FilteredSearchVisualTokens.selectToken(button, true); + this.removeSelectedToken(); + } + } + unselectEditTokens(e) { const inputContainer = this.container.querySelector('.filtered-search-box'); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); @@ -248,16 +265,21 @@ class FilteredSearchManager { } } - removeSelectedToken(e) { + removeSelectedTokenKeydown(e) { // 8 = Backspace Key // 46 = Delete Key if (e.keyCode === 8 || e.keyCode === 46) { - gl.FilteredSearchVisualTokens.removeSelectedToken(); - this.handleInputPlaceholder(); - this.toggleClearSearchButton(); + this.removeSelectedToken(); } } + removeSelectedToken() { + gl.FilteredSearchVisualTokens.removeSelectedToken(); + this.handleInputPlaceholder(); + this.toggleClearSearchButton(); + this.dropdownManager.updateCurrentDropdownOffset(); + } + onClearSearch(e) { e.preventDefault(); this.clearSearch(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index a5657fc8720..453ecccc6fc 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -16,11 +16,11 @@ class FilteredSearchVisualTokens { [].forEach.call(otherTokens, t => t.classList.remove('selected')); } - static selectToken(tokenButton) { + static selectToken(tokenButton, forceSelection = false) { const selected = tokenButton.classList.contains('selected'); FilteredSearchVisualTokens.unselectTokens(); - if (!selected) { + if (!selected || forceSelection) { tokenButton.classList.add('selected'); } } @@ -38,7 +38,12 @@ class FilteredSearchVisualTokens { return ` <div class="selectable" role="button"> <div class="name"></div> - <div class="value"></div> + <div class="value-container"> + <div class="value"></div> + <div class="remove-token" role="button"> + <i class="fa fa-close"></i> + </div> + </div> </div> `; } @@ -122,7 +127,8 @@ class FilteredSearchVisualTokens { if (value) { const button = lastVisualToken.querySelector('.selectable'); - button.removeChild(value); + const valueContainer = lastVisualToken.querySelector('.value-container'); + button.removeChild(valueContainer); lastVisualToken.innerHTML = button.innerHTML; } else { lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index b62b2cec4d8..687a462a0d4 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -3,6 +3,7 @@ import emojiMap from 'emojis/digests.json'; import emojiAliases from 'emojis/aliases.json'; import { glEmojiTag } from '~/behaviors/gl_emoji'; +import glRegexp from '~/lib/utils/regexp'; // Creates the variables for setting up GFM auto-completion window.gl = window.gl || {}; @@ -127,7 +128,15 @@ window.gl.GfmAutoComplete = { callbacks: { sorter: this.DefaultOptions.sorter, beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter + filter: this.DefaultOptions.filter, + + matcher: (flag, subtext) => { + const relevantText = subtext.trim().split(/\s/).pop(); + const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi'); + const match = regexp.exec(relevantText); + + return match && match.length ? match[1] : null; + } } }); // Team Members diff --git a/app/assets/javascripts/lib/utils/regexp.js b/app/assets/javascripts/lib/utils/regexp.js new file mode 100644 index 00000000000..baa0b51d59b --- /dev/null +++ b/app/assets/javascripts/lib/utils/regexp.js @@ -0,0 +1,10 @@ +/** + * Regexp utility for the convenience of working with regular expressions. + * + */ + +// Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203 +// Unicode 6.1 +const unicodeLetters = '\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC'; + +export default { unicodeLetters }; diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index 9c58c465001..64c1447f427 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -28,7 +28,9 @@ export default class MiniPipelineGraph { * All dropdown events are fired at the .dropdown-menu's parent element. */ bindEvents() { - $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); + $(document) + .off('shown.bs.dropdown', this.container) + .on('shown.bs.dropdown', this.container, this.getBuildsList); } /** @@ -91,6 +93,9 @@ export default class MiniPipelineGraph { }, error: () => { this.toggleLoading(button); + if ($(button).parent().hasClass('open')) { + $(button).dropdown('toggle'); + } new Flash('An error occurred while fetching the builds.', 'alert'); }, }); diff --git a/app/assets/javascripts/pipelines/components/stage.js b/app/assets/javascripts/pipelines/components/stage.js index b8cc3630611..203485f2990 100644 --- a/app/assets/javascripts/pipelines/components/stage.js +++ b/app/assets/javascripts/pipelines/components/stage.js @@ -2,13 +2,6 @@ import StatusIconEntityMap from '../../ci_status_icons'; export default { - data() { - return { - builds: '', - spinner: '<span class="fa fa-spinner fa-spin"></span>', - }; - }, - props: { stage: { type: Object, @@ -16,6 +9,13 @@ export default { }, }, + data() { + return { + builds: '', + spinner: '<span class="fa fa-spinner fa-spin"></span>', + }; + }, + updated() { if (this.builds) { this.stopDropdownClickPropagation(); @@ -31,7 +31,13 @@ export default { return this.$http.get(this.stage.dropdown_path) .then((response) => { this.builds = JSON.parse(response.body).html; - }, () => { + }) + .catch(() => { + // If dropdown is opened we'll close it. + if (this.$el.classList.contains('open')) { + $(this.$refs.dropdown).dropdown('toggle'); + } + const flash = new Flash('Something went wrong on our end.'); return flash; }); @@ -46,9 +52,10 @@ export default { * target the click event of this component. */ stopDropdownClickPropagation() { - $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => { - e.stopPropagation(); - }); + $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')) + .on('click', (e) => { + e.stopPropagation(); + }); }, }, computed: { @@ -81,12 +88,22 @@ export default { data-placement="top" data-toggle="dropdown" type="button" - :aria-label="stage.title"> - <span v-html="svgHTML" aria-hidden="true"></span> - <i class="fa fa-caret-down" aria-hidden="true"></i> + :aria-label="stage.title" + ref="dropdown"> + <span + v-html="svgHTML" + aria-hidden="true"> + </span> + <i + class="fa fa-caret-down" + aria-hidden="true" /> </button> - <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> - <div class="arrow-up" aria-hidden="true"></div> + <ul + ref="dropdown-content" + class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> + <div + class="arrow-up" + aria-hidden="true"></div> <div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" diff --git a/app/assets/javascripts/pipelines/components/time_ago.js b/app/assets/javascripts/pipelines/components/time_ago.js index 498d0715f54..188f74cc705 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.js +++ b/app/assets/javascripts/pipelines/components/time_ago.js @@ -2,68 +2,95 @@ import iconTimerSvg from 'icons/_icon_timer.svg'; import '../../lib/utils/datetime_utility'; export default { + props: { + finishedTime: { + type: String, + required: true, + }, + + duration: { + type: Number, + required: true, + }, + }, + data() { return { - currentTime: new Date(), iconTimerSvg, }; }, - props: ['pipeline'], + + updated() { + $(this.$refs.tooltip).tooltip('fixTitle'); + }, + computed: { - timeAgo() { - return gl.utils.getTimeago(); + hasDuration() { + return this.duration > 0; }, - localTimeFinished() { - return gl.utils.formatDate(this.pipeline.details.finished_at); + + hasFinishedTime() { + return this.finishedTime !== ''; }, - timeStopped() { - const changeTime = this.currentTime; - const options = { - weekday: 'long', - year: 'numeric', - month: 'short', - day: 'numeric', - }; - options.timeZoneName = 'short'; - const finished = this.pipeline.details.finished_at; - if (!finished && changeTime) return false; - return ({ words: this.timeAgo.format(finished) }); + + localTimeFinished() { + return gl.utils.formatDate(this.finishedTime); }, - duration() { - const { duration } = this.pipeline.details; - const date = new Date(duration * 1000); + + durationFormated() { + const date = new Date(this.duration * 1000); let hh = date.getUTCHours(); let mm = date.getUTCMinutes(); let ss = date.getSeconds(); - if (hh < 10) hh = `0${hh}`; - if (mm < 10) mm = `0${mm}`; - if (ss < 10) ss = `0${ss}`; + // left pad + if (hh < 10) { + hh = `0${hh}`; + } + if (mm < 10) { + mm = `0${mm}`; + } + if (ss < 10) { + ss = `0${ss}`; + } - if (duration !== null) return `${hh}:${mm}:${ss}`; - return false; + return `${hh}:${mm}:${ss}`; }, - }, - methods: { - changeTime() { - this.currentTime = new Date(); + + finishedTimeFormated() { + const timeAgo = gl.utils.getTimeago(); + + return timeAgo.format(this.finishedTime); }, }, + template: ` <td class="pipelines-time-ago"> - <p class="duration" v-if='duration'> - <span v-html="iconTimerSvg"></span> - {{duration}} + <p + class="duration" + v-if="hasDuration"> + <span + v-html="iconTimerSvg"> + </span> + {{durationFormated}} </p> - <p class="finished-at" v-if='timeStopped'> - <i class="fa fa-calendar"></i> + + <p + class="finished-at" + v-if="hasFinishedTime"> + + <i + class="fa fa-calendar" + aria-hidden="true" /> + <time + ref="tooltip" data-toggle="tooltip" data-placement="top" data-container="body" - :data-original-title='localTimeFinished'> - {{timeStopped.words}} + :title="localTimeFinished"> + {{finishedTimeFormated}} </time> </p> </td> diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js index 6eea4812f33..93d4818231f 100644 --- a/app/assets/javascripts/pipelines/pipelines.js +++ b/app/assets/javascripts/pipelines/pipelines.js @@ -1,4 +1,3 @@ -import Vue from 'vue'; import Visibility from 'visibilityjs'; import PipelinesService from './services/pipelines_service'; import eventHub from './event_hub'; @@ -161,15 +160,6 @@ export default { eventHub.$on('refreshPipelines', this.fetchPipelines); }, - beforeUpdate() { - if (this.state.pipelines.length && - this.$children && - !this.isMakingRequest && - !this.isLoading) { - this.store.startTimeAgoLoops.call(this, Vue); - } - }, - beforeDestroyed() { eventHub.$off('refreshPipelines'); }, diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js index 377ec8ba2cc..ffefe0192f2 100644 --- a/app/assets/javascripts/pipelines/stores/pipelines_store.js +++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js @@ -1,6 +1,3 @@ -/* eslint-disable no-underscore-dangle*/ -import VueRealtimeListener from '../../vue_realtime_listener'; - export default class PipelinesStore { constructor() { this.state = {}; @@ -30,32 +27,4 @@ export default class PipelinesStore { this.state.pageInfo = paginationInfo; } - - /** - * FIXME: Move this inside the component. - * - * Once the data is received we will start the time ago loops. - * - * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we - * update the time to show how long as passed. - * - */ - startTimeAgoLoops() { - const startTimeLoops = () => { - this.timeLoopInterval = setInterval(() => { - this.$children[0].$children.reduce((acc, component) => { - const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; - acc.push(timeAgoComponent); - return acc; - }, []).forEach(e => e.changeTime()); - }, 10000); - }; - - startTimeLoops(); - - const removeIntervals = () => clearInterval(this.timeLoopInterval); - const startIntervals = () => startTimeLoops(); - - VueRealtimeListener(removeIntervals, startIntervals); - } } diff --git a/app/assets/javascripts/vue_realtime_listener/index.js b/app/assets/javascripts/vue_realtime_listener/index.js deleted file mode 100644 index 4ddb2f975b0..00000000000 --- a/app/assets/javascripts/vue_realtime_listener/index.js +++ /dev/null @@ -1,9 +0,0 @@ -export default (removeIntervals, startIntervals) => { - window.removeEventListener('focus', startIntervals); - window.removeEventListener('blur', removeIntervals); - window.removeEventListener('onbeforeload', removeIntervals); - - window.addEventListener('focus', startIntervals); - window.addEventListener('blur', removeIntervals); - window.addEventListener('onbeforeload', removeIntervals); -}; diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js index 62b7131de51..79806bc7204 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js @@ -1,5 +1,4 @@ /* eslint-disable no-param-reassign */ - import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; @@ -166,6 +165,32 @@ export default { } return undefined; }, + + /** + * Timeago components expects a number + * + * @return {type} description + */ + pipelineDuration() { + if (this.pipeline.details && this.pipeline.details.duration) { + return this.pipeline.details.duration; + } + + return 0; + }, + + /** + * Timeago component expects a String. + * + * @return {String} + */ + pipelineFinishedAt() { + if (this.pipeline.details && this.pipeline.details.finished_at) { + return this.pipeline.details.finished_at; + } + + return ''; + }, }, template: ` @@ -192,7 +217,9 @@ export default { </div> </td> - <time-ago :pipeline="pipeline"/> + <time-ago + :duration="pipelineDuration" + :finished-time="pipelineFinishedAt" /> <td class="pipeline-actions"> <div class="pull-right btn-group"> diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 30d785464ac..1313ea25c2a 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -195,7 +195,6 @@ border: 1px solid $dropdown-border-color; border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; - overflow: hidden; @include set-invisible; @media (max-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 11d44df4867..0692f65043b 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -104,6 +104,24 @@ padding: 2px 7px; } + .value { + padding-right: 0; + } + + .remove-token { + display: inline-block; + padding-left: 4px; + padding-right: 8px; + + .fa-close { + color: $gl-text-color-disabled; + } + + &:hover .fa-close { + color: $gl-text-color-secondary; + } + } + .name { background-color: $filter-name-resting-color; color: $filter-name-text-color; @@ -112,7 +130,7 @@ text-transform: capitalize; } - .value { + .value-container { background-color: $white-normal; color: $filter-value-text-color; border-radius: 0 2px 2px 0; @@ -124,7 +142,7 @@ background-color: $filter-name-selected-color; } - .value { + .value-container { background-color: $filter-value-selected-color; } } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e5b811f3300..fff57472a4f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -196,38 +196,6 @@ module ApplicationHelper end end - def render_markup(file_name, file_content) - if gitlab_markdown?(file_name) - Hamlit::RailsHelpers.preserve(markdown(file_content)) - elsif asciidoc?(file_name) - asciidoc(file_content) - elsif plain?(file_name) - content_tag :pre, class: 'plain-readme' do - file_content - end - else - other_markup(file_name, file_content) - end - rescue RuntimeError - simple_format(file_content) - end - - def plain?(filename) - Gitlab::MarkupHelper.plain?(filename) - end - - def markup?(filename) - Gitlab::MarkupHelper.markup?(filename) - end - - def gitlab_markdown?(filename) - Gitlab::MarkupHelper.gitlab_markdown?(filename) - end - - def asciidoc?(filename) - Gitlab::MarkupHelper.asciidoc?(filename) - end - def promo_host 'about.gitlab.com' end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/markup_helper.rb index 106feb87398..0781874d7fc 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/markup_helper.rb @@ -1,6 +1,22 @@ require 'nokogiri' -module GitlabMarkdownHelper +module MarkupHelper + def plain?(filename) + Gitlab::MarkupHelper.plain?(filename) + end + + def markup?(filename) + Gitlab::MarkupHelper.markup?(filename) + end + + def gitlab_markdown?(filename) + Gitlab::MarkupHelper.gitlab_markdown?(filename) + end + + def asciidoc?(filename) + Gitlab::MarkupHelper.asciidoc?(filename) + end + # Use this in places where you would normally use link_to(gfm(...), ...). # # It solves a problem occurring with nested links (i.e. @@ -11,7 +27,7 @@ module GitlabMarkdownHelper # explicitly produce the correct linking behavior (i.e. # "<a>outer text </a><a>gfm ref</a><a> more outer text</a>"). def link_to_gfm(body, url, html_options = {}) - return "" if body.blank? + return '' if body.blank? context = { project: @project, @@ -43,71 +59,73 @@ module GitlabMarkdownHelper fragment.to_html.html_safe end + # Return the first line of +text+, up to +max_chars+, after parsing the line + # as Markdown. HTML tags in the parsed output are not counted toward the + # +max_chars+ limit. If the length limit falls within a tag's contents, then + # the tag contents are truncated without removing the closing tag. + def first_line_in_markdown(text, max_chars = nil, options = {}) + md = markdown(text, options).strip + + truncate_visible(md, max_chars || md.length) if md.present? + end + def markdown(text, context = {}) - return "" unless text.present? + return '' unless text.present? context[:project] ||= @project - - html = Banzai.render(text, context) + html = markdown_unsafe(text, context) banzai_postprocess(html, context) end def markdown_field(object, field) object = object.for_display if object.respond_to?(:for_display) - return "" unless object.present? + return '' unless object.present? html = Banzai.render_field(object, field) banzai_postprocess(html, object.banzai_render_context(field)) end - def asciidoc(text) - Gitlab::Asciidoc.render( - text, - project: @project, - current_user: (current_user if defined?(current_user)), - - # RelativeLinkFilter - project_wiki: @project_wiki, - requested_path: @path, - ref: @ref, - commit: @commit - ) + def markup(file_name, text, context = {}) + context[:project] ||= @project + html = context.delete(:rendered) || markup_unsafe(file_name, text, context) + banzai_postprocess(html, context) end - def other_markup(file_name, text) - Gitlab::OtherMarkup.render( - file_name, - text, - project: @project, - current_user: (current_user if defined?(current_user)), + def render_wiki_content(wiki_page) + text = wiki_page.content + return '' unless text.present? + + context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug } + + html = + case wiki_page.format + when :markdown + markdown_unsafe(text, context) + when :asciidoc + asciidoc_unsafe(text) + else + wiki_page.formatted_content.html_safe + end - # RelativeLinkFilter - project_wiki: @project_wiki, - requested_path: @path, - ref: @ref, - commit: @commit - ) + banzai_postprocess(html, context) end - # Return the first line of +text+, up to +max_chars+, after parsing the line - # as Markdown. HTML tags in the parsed output are not counted toward the - # +max_chars+ limit. If the length limit falls within a tag's contents, then - # the tag contents are truncated without removing the closing tag. - def first_line_in_markdown(text, max_chars = nil, options = {}) - md = markdown(text, options).strip + def markup_unsafe(file_name, text, context = {}) + return '' unless text.present? - truncate_visible(md, max_chars || md.length) if md.present? - end - - def render_wiki_content(wiki_page) - case wiki_page.format - when :markdown - markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug) - when :asciidoc - asciidoc(wiki_page.content) + if gitlab_markdown?(file_name) + Hamlit::RailsHelpers.preserve(markdown_unsafe(text, context)) + elsif asciidoc?(file_name) + asciidoc_unsafe(text) + elsif plain?(file_name) + content_tag :pre, class: 'plain-readme' do + text + end else - wiki_page.formatted_content.html_safe + other_markup_unsafe(file_name, text) end + rescue RuntimeError + simple_format(text) end # Returns the text necessary to reference `entity` across projects @@ -183,10 +201,10 @@ module GitlabMarkdownHelper end def markdown_toolbar_button(options = {}) - data = options[:data].merge({ container: "body" }) + data = options[:data].merge({ container: 'body' }) content_tag :button, - type: "button", - class: "toolbar-btn js-md has-tooltip hidden-xs", + type: 'button', + class: 'toolbar-btn js-md has-tooltip hidden-xs', tabindex: -1, data: data, title: options[:title], @@ -195,17 +213,34 @@ module GitlabMarkdownHelper end end + def markdown_unsafe(text, context = {}) + Banzai.render(text, context) + end + + def asciidoc_unsafe(text) + Gitlab::Asciidoc.render(text) + end + + def other_markup_unsafe(file_name, text) + Gitlab::OtherMarkup.render(file_name, text) + end + # Calls Banzai.post_process with some common context options def banzai_postprocess(html, context = {}) + return '' unless html.present? + context.merge!( current_user: (current_user if defined?(current_user)), # RelativeLinkFilter - requested_path: @path, + commit: @commit, project_wiki: @project_wiki, - ref: @ref + ref: @ref, + requested_path: @path ) Banzai.post_process(html, context) end + + extend self end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 715e5893a2c..3707bb5ba36 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -13,8 +13,8 @@ module ServicesHelper "Event will be triggered when a confidential issue is created/updated/closed" when "merge_request", "merge_request_events" "Event will be triggered when a merge request is created/updated/merged" - when "build", "build_events" - "Event will be triggered when a build status changes" + when "pipeline", "pipeline_events" + "Event will be triggered when a pipeline status changes" when "wiki_page", "wiki_page_events" "Event will be triggered when a wiki page is created/updated" when "commit", "commit_events" diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index f1dab60524e..f7b5a5f4dfc 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -12,10 +12,6 @@ module TreeHelper tree.html_safe end - def render_readme(readme) - render_markup(readme.name, readme.data) - end - # Return an image icon depending on the file type and mode # # type - String type of the tree item; either 'folder' or 'file' diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index a9b6b33eb5c..d2980db218a 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -1,6 +1,6 @@ class BaseMailer < ActionMailer::Base helper ApplicationHelper - helper GitlabMarkdownHelper + helper MarkupHelper attr_accessor :current_user helper_method :current_user, :can? diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index dd1a6922968..cf042717c95 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -28,6 +28,8 @@ class ApplicationSetting < ActiveRecord::Base attr_accessor :domain_whitelist_raw, :domain_blacklist_raw + validates :uuid, presence: true + validates :session_expire_delay, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } @@ -159,6 +161,7 @@ class ApplicationSetting < ActiveRecord::Base end end + before_validation :ensure_uuid! before_save :ensure_runners_registration_token before_save :ensure_health_check_access_token @@ -344,6 +347,12 @@ class ApplicationSetting < ActiveRecord::Base private + def ensure_uuid! + return if uuid? + + self.uuid = SecureRandom.uuid + end + def check_repository_storages invalid = repository_storages - Gitlab.config.repositories.storages.keys errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless diff --git a/app/models/event.rb b/app/models/event.rb index 5c34844b5d3..b780c1faf81 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -16,7 +16,7 @@ class Event < ActiveRecord::Base RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour - delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true + delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :merge_request, prefix: true, allow_nil: true delegate :title, to: :note, prefix: true, allow_nil: true diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 0bbc9451ffd..59737bb6085 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -107,7 +107,8 @@ module Network def find_commits(skip = 0) opts = { max_count: self.class.max_count, - skip: skip + skip: skip, + order: :date } opts[:ref] = @commit.id if @filter_ref diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index f2dfb87dbda..fa782c6fbb7 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -22,7 +22,7 @@ class ChatNotificationService < Service end def can_test? - super && valid? + valid? end def self.supported_events diff --git a/app/models/repository.rb b/app/models/repository.rb index 7bb874d7744..e74edb8e6f7 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -17,9 +17,9 @@ class Repository # same name. The cache key used by those methods must also match method's # name. # - # For example, for entry `:readme` there's a method called `readme` which - # stores its data in the `readme` cache key. - CACHED_METHODS = %i(size commit_count readme contribution_guide + # For example, for entry `:commit_count` there's a method called `commit_count` which + # stores its data in the `commit_count` cache key. + CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide changelog license_blob license_key gitignore koding_yml gitlab_ci_yml branch_names tag_names branch_count tag_count avatar exists? empty? root_ref).freeze @@ -28,7 +28,7 @@ class Repository # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # the corresponding methods to call for refreshing caches. METHOD_CACHES_FOR_FILE_TYPES = { - readme: :readme, + readme: :rendered_readme, changelog: :changelog, license: %i(license_blob license_key), contributing: :contribution_guide, @@ -527,7 +527,11 @@ class Repository head.readme end end - cache_method :readme + + def rendered_readme + MarkupHelper.markup_unsafe(readme.name, readme.data, project: project) if readme + end + cache_method :rendered_readme def contribution_guide file_on_head(:contributing) diff --git a/app/models/service.rb b/app/models/service.rb index dc76bf925d3..c71a7d169ec 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -26,6 +26,7 @@ class Service < ActiveRecord::Base has_one :service_hook validates :project_id, presence: true, unless: proc { |service| service.template? } + validates :type, presence: true scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :issue_trackers, -> { where(category: 'issue_tracker') } @@ -131,7 +132,7 @@ class Service < ActiveRecord::Base end def can_test? - !project.empty_repo? + true end # reason why service cannot be tested diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index f8594e29547..5baac9ebe4b 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -2,20 +2,13 @@ class ProjectPolicy < BasePolicy def rules team_access!(user) - owner = project.owner == user || - (project.group && project.group.has_owner?(user)) - - owner_access! if user.admin? || owner - team_member_owner_access! if owner + owner_access! if user.admin? || owner? + team_member_owner_access! if owner? if project.public? || (project.internal? && !user.external?) guest_access! public_access! - - if project.request_access_enabled && - !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) - can! :request_access - end + can! :request_access if access_requestable? end archived_access! if project.archived? @@ -27,6 +20,13 @@ class ProjectPolicy < BasePolicy @subject end + def owner? + return @owner if defined?(@owner) + + @owner = project.owner == user || + (project.group && project.group.has_owner?(user)) + end + def guest_access! can! :read_project can! :read_board @@ -226,14 +226,6 @@ class ProjectPolicy < BasePolicy disabled_features! end - def project_group_member?(user) - project.group && - ( - project.group.members_with_parents.exists?(user_id: user.id) || - project.group.requesters.exists?(user_id: user.id) - ) - end - def block_issues_abilities unless project.feature_available?(:issues, user) cannot! :read_issue if project.default_issues_tracker? @@ -254,6 +246,22 @@ class ProjectPolicy < BasePolicy private + def project_group_member?(user) + project.group && + ( + project.group.members_with_parents.exists?(user_id: user.id) || + project.group.requesters.exists?(user_id: user.id) + ) + end + + def access_requestable? + project.request_access_enabled && + !owner? && + !user.admin? && + !project.team.member?(user) && + !project_group_member?(user) + end + # A base set of abilities for read-only users, which # is then augmented as necessary for anonymous and other # read-only users. diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 7828c5806b0..535d93385e6 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -97,7 +97,8 @@ module Projects system_hook_service.execute_hooks_for(@project, :create) unless @project.group || @project.gitlab_project_import? - @project.team << [current_user, :master, current_user] + owners = [current_user, @project.namespace.owner].compact.uniq + @project.add_master(owners, current_user: current_user) end @project.group&.refresh_members_authorized_projects diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder index 158061579f6..e2aec532a9d 100644 --- a/app/views/events/_event.atom.builder +++ b/app/views/events/_event.atom.builder @@ -8,6 +8,7 @@ xml.entry do xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) xml.author do + xml.username event.author_username xml.name event.author_name xml.email event.author_public_email end diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index b6fb08b68e9..c0d12cbc66e 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -4,8 +4,7 @@ - if can?(current_user, :push_code, @project) = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme' .file-content.wiki - = cache(readme_cache_key) do - = render_readme(readme) + = markup(readme.name, readme.data, rendered: @repository.rendered_readme) - else .row-content-block.second-block.center %h3.page-title diff --git a/app/views/projects/blob/_markup.html.haml b/app/views/projects/blob/_markup.html.haml index 4ee4b03ff04..0090f7a11df 100644 --- a/app/views/projects/blob/_markup.html.haml +++ b/app/views/projects/blob/_markup.html.haml @@ -1,4 +1,4 @@ - blob.load_all_data!(@repository) .file-content.wiki - = render_markup(blob.name, blob.data) + = markup(blob.name, blob.data) diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml index 5cafb644b40..e87b73c9a34 100644 --- a/app/views/projects/blob/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml @@ -1,12 +1,8 @@ .diff-file .diff-content - - if gitlab_markdown?(@blob.name) + - if markup?(@blob.name) .file-content.wiki - = preserve do - = markdown(@content) - - elsif markup?(@blob.name) - .file-content.wiki - = raw render_markup(@blob.name, @content) + = markup(@blob.name, @content) - else .file-content.code.js-syntax-highlight - unless @diff_lines.empty? diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml index 008d1186478..190e7290303 100644 --- a/app/views/projects/boards/components/sidebar/_milestone.html.haml +++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml @@ -22,7 +22,7 @@ Milestone = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-selectable - = dropdown_title("Assignee milestone") + = dropdown_title("Assign milestone") = dropdown_filter("Search milestones") = dropdown_content = dropdown_loading diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index bdcc160a067..01599060844 100644 --- a/app/views/projects/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -5,4 +5,4 @@ %strong = readme.name .file-content.wiki - = render_readme(readme) + = markup(readme.name, readme.data) diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index f84be600df8..f2fe5742c12 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -21,7 +21,7 @@ .file-content.wiki - snippet_chunks.each do |chunk| - unless chunk[:data].empty? - = render_markup(snippet.file_name, chunk[:data]) + = markup(snippet.file_name, chunk[:data]) - else .file-content.code .nothing-here-block Empty file diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg index 2daa55a8652..5468545da2e 100644 --- a/app/views/shared/icons/_mr_bold.svg +++ b/app/views/shared/icons/_mr_bold.svg @@ -1 +1,2 @@ -<svg width="15" height="20" viewBox="0 0 12 14" xmlns="http://www.w3.org/2000/svg"><path d="M1 4.967a2.15 2.15 0 1 1 2.3 0v5.066a2.15 2.15 0 1 1-2.3 0V4.967zm7.85 5.17V5.496c0-.745-.603-1.346-1.35-1.346V6l-3-3 3-3v1.85c2.016 0 3.65 1.63 3.65 3.646v4.45a2.15 2.15 0 1 1-2.3.191z" fill-rule="nonzero"/></svg> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg> + diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index ccc808ff43e..774d20fb5ba 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -89,7 +89,7 @@ .sidebar-collapsed-icon %strong = icon('exclamation', 'aria-hidden': 'true') - %span= milestone.issues_visible_to_user(current_user).count + %span= milestone.merge_requests.count .title.hide-collapsed Merge requests %span.badge= milestone.merge_requests.count diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index 74f71e6cbd1..895c3f1e99d 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -24,6 +24,6 @@ - if gitlab_markdown?(@snippet.file_name) = preserve(markdown_field(@snippet, :content)) - else - = render_markup(@snippet.file_name, @snippet.content) + = markup(@snippet.file_name, @snippet.content) - else = render 'shared/file_highlight', blob: @snippet diff --git a/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml b/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml new file mode 100644 index 00000000000..70d35f06af4 --- /dev/null +++ b/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml @@ -0,0 +1,4 @@ +--- +title: Lazily sets UUID in ApplicationSetting for new installations +merge_request: +author: diff --git a/changelogs/unreleased/26585-remove-readme-view-caching.yml b/changelogs/unreleased/26585-remove-readme-view-caching.yml new file mode 100644 index 00000000000..6aefae982bf --- /dev/null +++ b/changelogs/unreleased/26585-remove-readme-view-caching.yml @@ -0,0 +1,4 @@ +--- +title: 'Remove view fragment caching for project READMEs' +merge_request: 8838 +author: diff --git a/changelogs/unreleased/29505-allow-admins-sudo-to-blocked-users.yml b/changelogs/unreleased/29505-allow-admins-sudo-to-blocked-users.yml new file mode 100644 index 00000000000..42fd71ccd5f --- /dev/null +++ b/changelogs/unreleased/29505-allow-admins-sudo-to-blocked-users.yml @@ -0,0 +1,4 @@ +--- +title: Allow admins to sudo to blocked users via the API +merge_request: 10842 +author: diff --git a/changelogs/unreleased/30466-click-x-to-remove-filter.yml b/changelogs/unreleased/30466-click-x-to-remove-filter.yml new file mode 100644 index 00000000000..2cf08e84ed1 --- /dev/null +++ b/changelogs/unreleased/30466-click-x-to-remove-filter.yml @@ -0,0 +1,4 @@ +--- +title: Add button to delete filters from filtered search bar +merge_request: +author: diff --git a/changelogs/unreleased/30645-show-pipeline-events-description.yml b/changelogs/unreleased/30645-show-pipeline-events-description.yml new file mode 100644 index 00000000000..fb75dde1d86 --- /dev/null +++ b/changelogs/unreleased/30645-show-pipeline-events-description.yml @@ -0,0 +1,4 @@ +--- +title: Fix pipeline events description for Slack and Mattermost integration +merge_request: 10908 +author: diff --git a/changelogs/unreleased/30973-fix-network-graph-ordering.yml b/changelogs/unreleased/30973-fix-network-graph-ordering.yml new file mode 100644 index 00000000000..420ec107842 --- /dev/null +++ b/changelogs/unreleased/30973-fix-network-graph-ordering.yml @@ -0,0 +1,4 @@ +--- +title: Fix ordering of commits in the network graph +merge_request: 10936 +author: diff --git a/changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml b/changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml new file mode 100644 index 00000000000..cb1de425d66 --- /dev/null +++ b/changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml @@ -0,0 +1,4 @@ +--- +title: Improves test settings for chat notification services for empty projects +merge_request: 10886 +author: diff --git a/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml b/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml new file mode 100644 index 00000000000..dee831c668b --- /dev/null +++ b/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml @@ -0,0 +1,4 @@ +--- +title: Fixed milestone sidebar showing incorrect number of MRs when collapsed +merge_request: 10933 +author: diff --git a/changelogs/unreleased/add-username-to-activity-feed.yml b/changelogs/unreleased/add-username-to-activity-feed.yml new file mode 100644 index 00000000000..f4c216a3954 --- /dev/null +++ b/changelogs/unreleased/add-username-to-activity-feed.yml @@ -0,0 +1,4 @@ +--- +title: Add username to activity atom feed +merge_request: 10802 +author: winniehell diff --git a/changelogs/unreleased/dm-sidekiq-5.yml b/changelogs/unreleased/dm-sidekiq-5.yml new file mode 100644 index 00000000000..69c94b18929 --- /dev/null +++ b/changelogs/unreleased/dm-sidekiq-5.yml @@ -0,0 +1,4 @@ +--- +title: Bump Sidekiq to 5.0.0 +merge_request: +author: diff --git a/changelogs/unreleased/fix-notify-post-receive.yml b/changelogs/unreleased/fix-notify-post-receive.yml new file mode 100644 index 00000000000..6b68396d5c5 --- /dev/null +++ b/changelogs/unreleased/fix-notify-post-receive.yml @@ -0,0 +1,4 @@ +--- +title: Fixed wrong method call on notify_post_receive +merge_request: +author: Luigi Leoni diff --git a/changelogs/unreleased/fix_emoji_parser.yml b/changelogs/unreleased/fix_emoji_parser.yml new file mode 100644 index 00000000000..2b1fffe2457 --- /dev/null +++ b/changelogs/unreleased/fix_emoji_parser.yml @@ -0,0 +1,4 @@ +--- +title: Fix rendering emoji inside a string +merge_request: 10647 +author: blackst0ne diff --git a/changelogs/unreleased/gl-version-backup-file.yml b/changelogs/unreleased/gl-version-backup-file.yml new file mode 100644 index 00000000000..9b5abd58ae7 --- /dev/null +++ b/changelogs/unreleased/gl-version-backup-file.yml @@ -0,0 +1,4 @@ +--- +title: Refactor backup/restore docs +merge_request: +author: diff --git a/changelogs/unreleased/replace_header_mr_icon.yml b/changelogs/unreleased/replace_header_mr_icon.yml new file mode 100644 index 00000000000..2ef6500f88a --- /dev/null +++ b/changelogs/unreleased/replace_header_mr_icon.yml @@ -0,0 +1,4 @@ +--- +title: Replace header merge request icon +merge_request: 10932 +author: blackst0ne diff --git a/changelogs/unreleased/tc-make-user-master-project-by-admin.yml b/changelogs/unreleased/tc-make-user-master-project-by-admin.yml new file mode 100644 index 00000000000..459d6178bdd --- /dev/null +++ b/changelogs/unreleased/tc-make-user-master-project-by-admin.yml @@ -0,0 +1,4 @@ +--- +title: Ensure namespace owner is Master of project upon creation +merge_request: 10910 +author: diff --git a/changelogs/unreleased/zj-dockerfiles.yml b/changelogs/unreleased/zj-dockerfiles.yml new file mode 100644 index 00000000000..40cb7dcfb76 --- /dev/null +++ b/changelogs/unreleased/zj-dockerfiles.yml @@ -0,0 +1,4 @@ +--- +title: Dockerfiles templates are imported from gitlab.com/gitlab-org/Dockerfile +merge_request: 10663 +author: diff --git a/db/migrate/20170421102337_remove_nil_type_services.rb b/db/migrate/20170421102337_remove_nil_type_services.rb new file mode 100644 index 00000000000..b835b9c6ed9 --- /dev/null +++ b/db/migrate/20170421102337_remove_nil_type_services.rb @@ -0,0 +1,12 @@ +class RemoveNilTypeServices < ActiveRecord::Migration + DOWNTIME = false + + def up + execute <<-SQL + DELETE FROM services WHERE type IS NULL OR type = ''; + SQL + end + + def down + end +end diff --git a/db/migrate/20170426175636_fill_missing_uuid_on_application_settings.rb b/db/migrate/20170426175636_fill_missing_uuid_on_application_settings.rb new file mode 100644 index 00000000000..58ad2c64075 --- /dev/null +++ b/db/migrate/20170426175636_fill_missing_uuid_on_application_settings.rb @@ -0,0 +1,10 @@ +class FillMissingUuidOnApplicationSettings < ActiveRecord::Migration + DOWNTIME = false + + def up + execute("UPDATE application_settings SET uuid = #{quote(SecureRandom.uuid)} WHERE uuid is NULL") + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index ff00951d5f6..49d7c996661 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170424142900) do +ActiveRecord::Schema.define(version: 20170426175636) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index 3245988fc14..d9ca74ca1a3 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -18,7 +18,8 @@ you need to use with GitLab. ## GitLab Pages Ports -If you're using GitLab Pages you will need some additional port configurations. +If you're using GitLab Pages with custom domain support you will need some +additional port configurations. GitLab Pages requires a separate virtual IP address. Configure DNS to point the `pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the [GitLab Pages documentation][gitlab-pages] for more information. diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index bf1aa6b9ac5..c5125dc6d5a 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -7,21 +7,20 @@ supported natively in NFS version 4. NFSv3 also supports locking as long as Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not specifically test NFSv3. -**no_root_squash**: NFS normally changes the `root` user to `nobody`. This is -a good security measure when NFS shares will be accessed by many different -users. However, in this case only GitLab will use the NFS share so it -is safe. GitLab requires the `no_root_squash` setting because we need to -manage file permissions automatically. Without the setting you will receive -errors when the Omnibus package tries to alter permissions. Note that GitLab -and other bundled components do **not** run as `root` but as non-privileged -users. The requirement for `no_root_squash` is to allow the Omnibus package to -set ownership and permissions on files, as needed. - ### Recommended options When you define your NFS exports, we recommend you also add the following options: +- `no_root_squash` - NFS normally changes the `root` user to `nobody`. This is + a good security measure when NFS shares will be accessed by many different + users. However, in this case only GitLab will use the NFS share so it + is safe. GitLab recommends the `no_root_squash` setting because we need to + manage file permissions automatically. Without the setting you may receive + errors when the Omnibus package tries to alter permissions. Note that GitLab + and other bundled components do **not** run as `root` but as non-privileged + users. The recommendation for `no_root_squash` is to allow the Omnibus package + to set ownership and permissions on files, as needed. - `sync` - Force synchronous behavior. Default is asynchronous and under certain circumstances it could lead to data loss if a failure occurs before data has synced. diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index e680a560888..5be6053b76e 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -1,41 +1,52 @@ -# Backup restore +# Backing up and restoring GitLab ![backup banner](backup_hrz.png) An application data backup creates an archive file that contains the database, all repositories and all attachments. -This archive will be saved in `backup_path`, which is specified in the -`config/gitlab.yml` file. -The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP` -identifies the time at which each backup was created. - -> In GitLab 8.15 we changed the timestamp format from `EPOCH` (`1393513186`) -> to `EPOCH_YYYY_MM_DD` (`1393513186_2014_02_27`) -You can only restore a backup to exactly the same version of GitLab on which it -was created. The best way to migrate your repositories from one server to +You can only restore a backup to **exactly the same version** of GitLab on which +it was created. The best way to migrate your repositories from one server to another is through backup restore. -To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json` -(for omnibus packages) or `/home/git/gitlab/.secret` (for installations -from source). This file contains the database encryption key, -[CI secret variables](../ci/variables/README.md#secret-variables), and -secret variables used for [two-factor authentication](../security/two_factor_authentication.md). -If you fail to restore this encryption key file along with the application data -backup, users with two-factor authentication enabled and GitLab Runners will -lose access to your GitLab server. +## Backup + +GitLab provides a simple command line interface to backup your whole installation, +and is flexible enough to fit your needs. -## Create a backup of the GitLab system +### Backup timestamp + +>**Note:** +In GitLab 9.2 the timestamp format was changed from `EPOCH_YYYY_MM_DD` to +`EPOCH_YYYY_MM_DD_GitLab version`, for example `1493107454_2017_04_25` +would become `1493107454_2017_04_25_9.1.0`. + +The backup archive will be saved in `backup_path`, which is specified in the +`config/gitlab.yml` file. +The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP` +identifies the time at which each backup was created, plus the GitLab version. +The timestamp is needed if you need to restore GitLab and multiple backups are +available. + +For example, if the backup name is `1493107454_2017_04_25_9.1.0_gitlab_backup.tar`, +then the timestamp is `1493107454_2017_04_25_9.1.0`. + +### Creating a backup of the GitLab system Use this command if you've installed GitLab with the Omnibus package: + ``` sudo gitlab-rake gitlab:backup:create ``` + Use this if you've installed GitLab from source: + ``` sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` + If you are running GitLab within a Docker container, you can run the backup from the host: + ``` docker exec -t <container name> gitlab-rake gitlab:backup:create ``` @@ -69,9 +80,9 @@ Deleting tmp directories...[DONE] Deleting old backups... [SKIPPING] ``` -## Backup Strategy Option +### Backup strategy option -> **Note:** Introduced as an option in 8.17 +> **Note:** Introduced as an option in GitLab 8.17. The default backup strategy is to essentially stream data from the respective data locations to the backup using the Linux command `tar` and `gzip`. This works @@ -91,7 +102,7 @@ To use the `copy` strategy instead of the default streaming strategy, specify `STRATEGY=copy` in the Rake task command. For example, `sudo gitlab-rake gitlab:backup:create STRATEGY=copy`. -## Exclude specific directories from the backup +### Excluding specific directories from the backup You can choose what should be backed up by adding the environment variable `SKIP`. The available options are: @@ -115,7 +126,7 @@ sudo gitlab-rake gitlab:backup:create SKIP=db,uploads sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production ``` -## Upload backups to remote (cloud) storage +### Uploading backups to a remote (cloud) storage Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates. It uses the [Fog library](http://fog.io/) to perform the upload. @@ -259,7 +270,7 @@ For installations from source: remote_directory: 'gitlab_backups' ``` -## Backup archive permissions +### Backup archive permissions The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`) will have owner/group git:git and 0600 permissions by default. @@ -277,11 +288,11 @@ gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives wo archive_permissions: 0644 # Makes the backup archives world-readable ``` -## Storing configuration files +### Storing configuration files Please be informed that a backup does not store your configuration -files. One reason for this is that your database contains encrypted -information for two-factor authentication. Storing encrypted +files. One reason for this is that your database contains encrypted +information for two-factor authentication. Storing encrypted information along with its key in the same place defeats the purpose of using encryption in the first place! @@ -294,11 +305,74 @@ At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and `/home/git/gitlab/config/secrets.yml` (source) to preserve your database encryption key. -## Restore a previously created backup +### Configuring cron to make daily backups + +>**Note:** +The following cron jobs do not [backup your GitLab configuration files](#storing-configuration-files) +or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). + +**For Omnibus installations** + +To schedule a cron job that backs up your repositories and GitLab metadata, use the root user: + +``` +sudo su - +crontab -e +``` + +There, add the following line to schedule the backup for everyday at 2 AM: + +``` +0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1 +``` + +You may also want to set a limited lifetime for backups to prevent regular +backups using all your disk space. To do this add the following lines to +`/etc/gitlab/gitlab.rb` and reconfigure: -You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. +``` +# limit backup lifetime to 7 days - 604800 seconds +gitlab_rails['backup_keep_time'] = 604800 +``` -### Prerequisites +Note that the `backup_keep_time` configuration option only manages local +files. GitLab does not automatically prune old files stored in a third-party +object storage (e.g., AWS S3) because the user may not have permission to list +and delete files. We recommend that you configure the appropriate retention +policy for your object storage. For example, you can configure [the S3 backup +policy as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3). + +**For installation from source** + +``` +cd /home/git/gitlab +sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups +sudo -u git crontab -e # Edit the crontab for the git user +``` + +Add the following lines at the bottom: + +``` +# Create a full backup of the GitLab repositories and SQL database every day at 4am +0 4 * * * cd /home/git/gitlab && PATH=/usr/local/bin:/usr/bin:/bin bundle exec rake gitlab:backup:create RAILS_ENV=production CRON=1 +``` + +The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors. +This is recommended to reduce cron spam. + +## Restore + +GitLab provides a simple command line interface to backup your whole installation, +and is flexible enough to fit your needs. + +The [restore prerequisites section](#restore-prerequisites) includes crucial +information. Make sure to read and test the whole restore process at least once +before attempting to perform it in a production environment. + +You can only restore a backup to **exactly the same version** of GitLab that +you created it on, for example 9.1.0. + +### Restore prerequisites You need to have a working GitLab installation before you can perform a restore. This is mainly because the system user performing the @@ -307,13 +381,23 @@ the SQL database it needs to import data into ('gitlabhq_production'). All existing data will be either erased (SQL) or moved to a separate directory (repositories, uploads). -If some or all of your GitLab users are using two-factor authentication (2FA) -then you must also make sure to restore `/etc/gitlab/gitlab.rb` and -`/etc/gitlab/gitlab-secrets.json` (Omnibus), or -`/home/git/gitlab/config/secrets.yml` (installations from source). Note that you -need to run `gitlab-ctl reconfigure` after changing `gitlab-secrets.json`. +To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json` +(for Omnibus packages) or `/home/git/gitlab/.secret` (for installations +from source). This file contains the database encryption key, +[CI secret variables](../ci/variables/README.md#secret-variables), and +secret variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md). +If you fail to restore this encryption key file along with the application data +backup, users with two-factor authentication enabled and GitLab Runners will +lose access to your GitLab server. + +Depending on your case, you might want to run the restore command with one or +more of the following options: + +- `BACKUP=timestamp_of_backup` - Required if more than one backup exists. + Read what the [backup timestamp is about](#backup-timestamp). +- `force=yes` - Do not ask if the authorized_keys file should get regenerated. -### Installation from source +### Restore for installation from source ``` # Stop processes that are connected to the database @@ -322,13 +406,6 @@ sudo service gitlab stop bundle exec rake gitlab:backup:restore RAILS_ENV=production ``` -Options: - -``` -BACKUP=timestamp_of_backup (required if more than one backup exists) -force=yes (do not ask if the authorized_keys file should get regenerated) -``` - Example output: ``` @@ -360,13 +437,13 @@ Restoring repositories: Deleting tmp directories...[DONE] ``` -### Omnibus installations +### Restore for Omnibus installations This procedure assumes that: -- You have installed the exact same version of GitLab Omnibus with which the - backup was created -- You have run `sudo gitlab-ctl reconfigure` at least once +- You have installed the **exact same version** of GitLab Omnibus with which the + backup was created. +- You have run `sudo gitlab-ctl reconfigure` at least once. - GitLab is running. If not, start it using `sudo gitlab-ctl start`. First make sure your backup tar file is in the backup directory described in the @@ -374,7 +451,7 @@ First make sure your backup tar file is in the backup directory described in the `/var/opt/gitlab/backups`. ```shell -sudo cp 1393513186_2014_02_27_gitlab_backup.tar /var/opt/gitlab/backups/ +sudo cp 1493107454_2017_04_25_9.1.0_gitlab_backup.tar /var/opt/gitlab/backups/ ``` Stop the processes that are connected to the database. Leave the rest of GitLab @@ -392,7 +469,7 @@ restore: ```shell # This command will overwrite the contents of your GitLab database! -sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186_2014_02_27 +sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2017_04_25_9.1.0 ``` Restart and check GitLab: @@ -404,59 +481,7 @@ sudo gitlab-rake gitlab:check SANITIZE=true If there is a GitLab version mismatch between your backup tar file and the installed version of GitLab, the restore command will abort with an error. Install the -[correct GitLab version](https://about.gitlab.com/downloads/archives/) and try again. - -## Configure cron to make daily backups - -### For installation from source: -``` -cd /home/git/gitlab -sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups -sudo -u git crontab -e # Edit the crontab for the git user -``` - -Add the following lines at the bottom: - -``` -# Create a full backup of the GitLab repositories and SQL database every day at 4am -0 4 * * * cd /home/git/gitlab && PATH=/usr/local/bin:/usr/bin:/bin bundle exec rake gitlab:backup:create RAILS_ENV=production CRON=1 -``` - -The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors. -This is recommended to reduce cron spam. - -### For omnibus installations - -To schedule a cron job that backs up your repositories and GitLab metadata, use the root user: - -``` -sudo su - -crontab -e -``` - -There, add the following line to schedule the backup for everyday at 2 AM: - -``` -0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1 -``` - -You may also want to set a limited lifetime for backups to prevent regular -backups using all your disk space. To do this add the following lines to -`/etc/gitlab/gitlab.rb` and reconfigure: - -``` -# limit backup lifetime to 7 days - 604800 seconds -gitlab_rails['backup_keep_time'] = 604800 -``` - -Note that the `backup_keep_time` configuration option only manages local -files. GitLab does not automatically prune old files stored in a third-party -object storage (e.g. AWS S3) because the user may not have permission to list -and delete files. We recommend that you configure the appropriate retention -policy for your object storage. For example, you can configure [the S3 backup -policy here as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3). - -NOTE: This cron job does not [backup your omnibus-gitlab configuration](#backup-and-restore-omnibus-gitlab-configuration) or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). +[correct GitLab version](https://packages.gitlab.com/gitlab/) and try again. ## Alternative backup strategies @@ -481,6 +506,19 @@ Example: LVM snapshots + rsync If you are running GitLab on a virtualized server you can possibly also create VM snapshots of the entire GitLab server. It is not uncommon however for a VM snapshot to require you to power down the server, so this approach is probably of limited practical use. +## Additional notes + +This documentation is for GitLab Community and Enterprise Edition. We backup +GitLab.com and make sure your data is secure, but you can't use these methods +to export / backup your data yourself from GitLab.com. + +Issues are stored in the database. They can't be stored in Git itself. + +To migrate your repositories from one server to another with an up-to-date version of +GitLab, you can use the [import rake task](import.md) to do a mass import of the +repository. Note that if you do an import rake task, rather than a backup restore, you +will have all your repositories, but not any other data. + ## Troubleshooting ### Restoring database backup using omnibus packages outputs warnings @@ -490,7 +528,6 @@ If you are using backup restore procedures you might encounter the following war psql:/var/opt/gitlab/backups/db/database.sql:22: ERROR: must be owner of extension plpgsql psql:/var/opt/gitlab/backups/db/database.sql:2931: WARNING: no privileges could be revoked for "public" (two occurrences) psql:/var/opt/gitlab/backups/db/database.sql:2933: WARNING: no privileges were granted for "public" (two occurrences) - ``` Be advised that, backup is successfully restored in spite of these warnings. @@ -499,14 +536,3 @@ The rake task runs this as the `gitlab` user which does not have the superuser a Those objects have no influence on the database backup/restore but they give this annoying warning. For more information see similar questions on postgresql issue tracker[here](http://www.postgresql.org/message-id/201110220712.30886.adrian.klaver@gmail.com) and [here](http://www.postgresql.org/message-id/2039.1177339749@sss.pgh.pa.us) as well as [stack overflow](http://stackoverflow.com/questions/4368789/error-must-be-owner-of-language-plpgsql). - -## Note -This documentation is for GitLab CE. -We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com. - -Issues are stored in the database. They can't be stored in Git itself. - -To migrate your repositories from one server to another with an up-to-date version of -GitLab, you can use the [import rake task](import.md) to do a mass import of the -repository. Note that if you do an import rake task, rather than a backup restore, you -will have all your repositories, but not any other data. diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md index c3f3179d99e..733e70ca9bf 100644 --- a/doc/user/admin_area/settings/usage_statistics.md +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -3,7 +3,7 @@ GitLab Inc. will periodically collect information about your instance in order to perform various actions. -All statistics are opt-in and you can always disable them from the admin panel. +All statistics are opt-out, you can disable them from the admin panel. ## Version check diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md index cad4757f287..1e28646bc97 100644 --- a/doc/user/project/integrations/bamboo.md +++ b/doc/user/project/integrations/bamboo.md @@ -51,9 +51,9 @@ service in GitLab. ## Troubleshooting -If builds are not triggered, these are a couple of things to keep in mind. +If builds are not triggered, ensure you entered the right GitLab IP address in +Bamboo under 'Trigger IP addresses'. + +>**Note:** +- Starting with GitLab 8.14.0, builds are triggered on push events. -1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger - IP addresses'. -1. Remember that GitLab only triggers builds on push events. A commit via the - web interface will not trigger CI currently. diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index ddff3c8c1e8..86bf567fe69 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -102,7 +102,7 @@ module API end def authenticate! - unauthorized! unless current_user && can?(current_user, :access_api) + unauthorized! unless current_user && can?(initial_current_user, :access_api) end def authenticate_non_get! diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 5b48ee8665f..ebed26dd178 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -140,7 +140,7 @@ module API begin Gitlab::GitalyClient::Notifications.new(project.repository).post_receive rescue GRPC::Unavailable => e - render_api_error(e, 500) + render_api_error!(e, 500) end end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 8d64c82272a..330cd963626 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -15,7 +15,7 @@ module Backup s[:gitlab_version] = Gitlab::VERSION s[:tar_version] = tar_version s[:skipped] = ENV["SKIP"] - tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d')}#{FILE_NAME_SUFFIX}" + tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{s[:gitlab_version]}#{FILE_NAME_SUFFIX}" Dir.chdir(backup_path) do File.open("#{backup_path}/backup_information.yml", "w+") do |file| diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb index d6138816e70..6255a611dbe 100644 --- a/lib/banzai/filter/emoji_filter.rb +++ b/lib/banzai/filter/emoji_filter.rb @@ -53,7 +53,10 @@ module Banzai # Build a regexp that matches all valid :emoji: names. def self.emoji_pattern - @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/ + @emoji_pattern ||= + /(?<=[^[:alnum:]:]|\n|^) + :(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}): + (?=[^[:alnum:]:]|$)/x end # Build a regexp that matches all valid unicode emojis names. diff --git a/lib/github/import.rb b/lib/github/import.rb index 3e9162ffb9d..d49761fd6c6 100644 --- a/lib/github/import.rb +++ b/lib/github/import.rb @@ -171,24 +171,28 @@ module Github begin restore_branches(pull_request) - author_id = user_id(pull_request.author, project.creator_id) - merge_request.iid = pull_request.iid - merge_request.title = pull_request.title - merge_request.description = format_description(pull_request.description, pull_request.author) - merge_request.source_project = pull_request.source_project - merge_request.source_branch = pull_request.source_branch_name - merge_request.source_branch_sha = pull_request.source_branch_sha - merge_request.target_project = pull_request.target_project - merge_request.target_branch = pull_request.target_branch_name - merge_request.target_branch_sha = pull_request.target_branch_sha - merge_request.state = pull_request.state - merge_request.milestone_id = milestone_id(pull_request.milestone) - merge_request.author_id = author_id - merge_request.assignee_id = user_id(pull_request.assignee) - merge_request.created_at = pull_request.created_at - merge_request.updated_at = pull_request.updated_at - merge_request.save!(validate: false) + author_id = user_id(pull_request.author, project.creator_id) + description = format_description(pull_request.description, pull_request.author) + + merge_request.attributes = { + iid: pull_request.iid, + title: pull_request.title, + description: description, + source_project: pull_request.source_project, + source_branch: pull_request.source_branch_name, + source_branch_sha: pull_request.source_branch_sha, + target_project: pull_request.target_project, + target_branch: pull_request.target_branch_name, + target_branch_sha: pull_request.target_branch_sha, + state: pull_request.state, + milestone_id: milestone_id(pull_request.milestone), + author_id: author_id, + assignee_id: user_id(pull_request.assignee), + created_at: pull_request.created_at, + updated_at: pull_request.updated_at + } + merge_request.save!(validate: false) merge_request.merge_request_diffs.create # Fetch review comments diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index d575367d81a..fba80c7132e 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -14,28 +14,16 @@ module Gitlab # Public: Converts the provided Asciidoc markup into HTML. # # input - the source text in Asciidoc format - # context - a Hash with the template context: - # :commit - # :project - # :project_wiki - # :requested_path - # :ref - # asciidoc_opts - a Hash of options to pass to the Asciidoctor converter # - def self.render(input, context, asciidoc_opts = {}) - asciidoc_opts.reverse_merge!( - safe: :secure, - backend: :gitlab_html5, - attributes: [] - ) - asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS) + def self.render(input) + asciidoc_opts = { safe: :secure, + backend: :gitlab_html5, + attributes: DEFAULT_ADOC_ATTRS } plantuml_setup html = ::Asciidoctor.convert(input, asciidoc_opts) - html = Banzai.post_process(html, context) - filter = Banzai::Filter::SanitizationFilter.new(html) html = filter.call.to_s diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index d76aa38f741..1ff34553f0a 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -41,7 +41,7 @@ module Gitlab type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push' # Hash to be passed as post_receive_data - data = { + { object_kind: type, event_name: type, before: oldrev, @@ -61,16 +61,15 @@ module Gitlab repository: project.hook_attrs.slice(:name, :url, :description, :homepage, :git_http_url, :git_ssh_url, :visibility_level) } - - data end # This method provide a sample data generated with # existing project and commits to test webhooks def build_sample(project, user) - commits = project.repository.commits(project.default_branch, limit: 3) ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}" - build(project, user, commits.last.id, commits.first.id, ref, commits) + commits = project.repository.commits(project.default_branch.to_s, limit: 3) rescue [] + + build(project, user, commits.last&.id, commits.first&.id, ref, commits) end def checkout_sha(repository, newrev, ref) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index d7dac9f6149..452dba7971d 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -494,7 +494,9 @@ module Gitlab # :contains is the commit contained by the refs from which to begin (SHA1 or name) # :max_count is the maximum number of commits to fetch # :skip is the number of commits to skip - # :order is the commits order and allowed value is :date(default) or :topo + # :order is the commits order and allowed value is :none (default), :date, or :topo + # commit ordering types are documented here: + # http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant) # def find_commits(options = {}) actual_options = options.dup @@ -522,11 +524,8 @@ module Gitlab end end - if actual_options[:order] == :topo - walker.sorting(Rugged::SORT_TOPO) - else - walker.sorting(Rugged::SORT_NONE) - end + sort_type = rugged_sort_type(actual_options[:order]) + walker.sorting(sort_type) commits = [] offset = actual_options[:skip] @@ -1273,6 +1272,18 @@ module Gitlab def gitaly_ref_client @gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self) end + + # Returns the `Rugged` sorting type constant for a given + # sort type key. Valid keys are `:none`, `:topo`, and `:date` + def rugged_sort_type(key) + @rugged_sort_types ||= { + none: Rugged::SORT_NONE, + topo: Rugged::SORT_TOPO, + date: Rugged::SORT_DATE + } + + @rugged_sort_types.fetch(key, Rugged::SORT_NONE) + end end end end diff --git a/lib/gitlab/other_markup.rb b/lib/gitlab/other_markup.rb index e67acf28c94..c2adc9aa10b 100644 --- a/lib/gitlab/other_markup.rb +++ b/lib/gitlab/other_markup.rb @@ -4,19 +4,11 @@ module Gitlab # Public: Converts the provided markup into HTML. # # input - the source text in a markup format - # context - a Hash with the template context: - # :commit - # :project - # :project_wiki - # :requested_path - # :ref # - def self.render(file_name, input, context) + def self.render(file_name, input) html = GitHub::Markup.render(file_name, input). force_encoding(input.encoding) - html = Banzai.post_process(html, context) - filter = Banzai::Filter::SanitizationFilter.new(html) html = filter.call.to_s diff --git a/lib/gitlab/template/dockerfile_template.rb b/lib/gitlab/template/dockerfile_template.rb index d5d3e045a42..20b054b0bd8 100644 --- a/lib/gitlab/template/dockerfile_template.rb +++ b/lib/gitlab/template/dockerfile_template.rb @@ -8,7 +8,7 @@ module Gitlab class << self def extension - 'Dockerfile' + '.Dockerfile' end def categories @@ -18,7 +18,7 @@ module Gitlab end def base_dir - Rails.root.join('vendor/dockerfile') + Rails.root.join('vendor/Dockerfile') end def finder(project = nil) diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake index cb2adc81c9d..1b04e1350ed 100644 --- a/lib/tasks/gitlab/update_templates.rake +++ b/lib/tasks/gitlab/update_templates.rake @@ -5,7 +5,7 @@ namespace :gitlab do end def update(template) - sub_dir = template.repo_url.match(/([a-z-]+)\.git\z/)[1] + sub_dir = template.repo_url.match(/([A-Za-z-]+)\.git\z/)[1] dir = File.join(vendor_directory, sub_dir) unless clone_repository(template.repo_url, dir) @@ -45,7 +45,11 @@ namespace :gitlab do Template.new( "https://gitlab.com/gitlab-org/gitlab-ci-yml.git", /(\.{1,2}|LICENSE|CONTRIBUTING.md|Pages|autodeploy|\.gitlab-ci.yml)\z/ - ) + ), + Template.new( + "https://gitlab.com/gitlab-org/Dockerfile.git", + /(\.{1,2}|LICENSE|CONTRIBUTING.md|\.Dockerfile)\z/ + ), ].freeze def vendor_directory diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index 16365642a34..2d892f4a2b7 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -8,6 +8,7 @@ describe Projects::ServicesController do before do sign_in(user) project.team << [user, :master] + controller.instance_variable_set(:@project, project) controller.instance_variable_set(:@service, service) end @@ -18,20 +19,60 @@ describe Projects::ServicesController do end describe "#test" do + context 'when can_test? returns false' do + it 'renders 404' do + allow_any_instance_of(Service).to receive(:can_test?).and_return(false) + + get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + + expect(response).to have_http_status(404) + end + end + context 'success' do + context 'with empty project' do + let(:project) { create(:empty_project) } + + context 'with chat notification service' do + let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') } + + it 'redirects and show success message' do + allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true) + + get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + + expect(response).to redirect_to(root_path) + expect(flash[:notice]).to eq('We sent a request to the provided URL') + end + end + + it 'redirects and show success message' do + expect(service).to receive(:test).and_return(success: true, result: 'done') + + get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + + expect(response).to redirect_to(root_path) + expect(flash[:notice]).to eq('We sent a request to the provided URL') + end + end + it "redirects and show success message" do - expect(service).to receive(:test).and_return({ success: true, result: 'done' }) + expect(service).to receive(:test).and_return(success: true, result: 'done') + get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html - expect(response.status).to redirect_to('/') + + expect(response).to redirect_to(root_path) expect(flash[:notice]).to eq('We sent a request to the provided URL') end end context 'failure' do it "redirects and show failure message" do - expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' }) + expect(service).to receive(:test).and_return(success: false, result: 'Bad test') + get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html - expect(response.status).to redirect_to('/') + + expect(response).to redirect_to(root_path) expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test') end end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 88f6c265505..62aa71ae8d8 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -1,6 +1,19 @@ FactoryGirl.define do factory :service do project factory: :empty_project + type 'Service' + end + + factory :custom_issue_tracker_service, class: CustomIssueTrackerService do + project factory: :empty_project + type 'CustomIssueTrackerService' + category 'issue_tracker' + active true + properties( + project_url: 'https://project.url.com', + issues_url: 'https://issues.url.com', + new_issue_url: 'https://newissue.url.com' + ) end factory :kubernetes_service do diff --git a/spec/features/admin/admin_cohorts_spec.rb b/spec/features/admin/admin_cohorts_spec.rb new file mode 100644 index 00000000000..dd14ffdb2ce --- /dev/null +++ b/spec/features/admin/admin_cohorts_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +feature 'Admin cohorts page', feature: true do + before do + login_as :admin + end + + scenario 'See users count per month' do + 2.times { create(:user) } + + visit admin_cohorts_path + + expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0") + end +end diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index 64a6c70061b..344e31e5ef5 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'Copy as GFM', feature: true, js: true do - include GitlabMarkdownHelper + include MarkupHelper include RepoHelpers include ActionView::Helpers::JavaScriptHelper diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 81ee0e2e4f6..c824aa6a414 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -12,7 +12,7 @@ describe 'Filter issues', js: true, feature: true do let!(:wontfix) { create(:label, project: project, title: "Won't fix") } let!(:bug_label) { create(:label, project: project, title: 'bug') } - let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } + let!(:caps_sensitive_label) { create(:label, project: project, title: 'CaPs') } let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) } let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index b571f6bd861..ad29911248f 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -45,6 +45,33 @@ feature 'GFM autocomplete', feature: true, js: true do expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type') end + it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do + note = find('#note_note') + + # Number. + page.within '.timeline-content-form' do + note.native.send_keys('7:') + end + + expect(page).not_to have_selector('.atwho-view') + + # ASCII letter. + page.within '.timeline-content-form' do + note.set('') + note.native.send_keys('w:') + end + + expect(page).not_to have_selector('.atwho-view') + + # Non-ASCII letter. + page.within '.timeline-content-form' do + note.set('') + note.native.send_keys('Ё:') + end + + expect(page).not_to have_selector('.atwho-view') + end + it 'selects the first item for assignee dropdowns' do page.within '.timeline-content-form' do find('#note_note').native.send_keys('') diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 894df13a2dc..ba930de937d 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -26,7 +26,7 @@ require 'erb' describe 'GitLab Markdown', feature: true do include Capybara::Node::Matchers - include GitlabMarkdownHelper + include MarkupHelper include MarkdownMatchers # Sometimes it can be useful to see the parsed output of the Markdown document diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb index a7cc98a2059..548131c7cd4 100644 --- a/spec/features/projects/files/dockerfile_dropdown_spec.rb +++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb @@ -1,11 +1,14 @@ require 'spec_helper' +require 'fileutils' feature 'User wants to add a Dockerfile file', feature: true do before do user = create(:user) project = create(:project) project.team << [user, :master] + login_as user + visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'Dockerfile') end @@ -15,11 +18,14 @@ feature 'User wants to add a Dockerfile file', feature: true do scenario 'user can pick a Dockerfile file from the dropdown', js: true do find('.js-dockerfile-selector').click + wait_for_ajax + within '.dockerfile-selector' do find('.dropdown-input-field').set('HTTPd') find('.dropdown-content li', text: 'HTTPd').click end + wait_for_ajax expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd') diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb index dab78fd3571..5e19907eef9 100644 --- a/spec/features/projects/milestones/milestone_spec.rb +++ b/spec/features/projects/milestones/milestone_spec.rb @@ -63,4 +63,28 @@ feature 'Project milestone', :feature do expect(page).not_to have_content('Assign some issues to this milestone.') end end + + context 'when project has an issue' do + before do + create(:issue, project: project, milestone: milestone) + + visit namespace_project_milestone_path(project.namespace, project, milestone) + end + + describe 'the collapsed sidebar' do + before do + find('.milestone-sidebar .gutter-toggle').click + end + + it 'shows the total MR and issue counts' do + find('.milestone-sidebar .block', match: :first) + blocks = all('.milestone-sidebar .block') + + aggregate_failures 'MR and issue blocks' do + expect(blocks[3]).to have_content 1 + expect(blocks[4]).to have_content 0 + end + end + end + end end diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 0cdbc32431d..51a3e91d201 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -116,7 +116,7 @@ Linking to a file relative to this project's repository should work. Because life would be :zzz: without Emoji, right? :rocket: -Get ready for the Emoji :bomb:: :+1::-1::ok_hand::wave::v::raised_hand::muscle: +Get ready for the Emoji :bomb: : :+1: :-1: :ok_hand: :wave: :v: :raised_hand: :muscle: ### TableOfContentsFilter diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 5c07ea8a872..01bdf01ad22 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -239,33 +239,6 @@ describe ApplicationHelper do end end - describe 'render_markup' do - let(:content) { 'Noël' } - let(:user) { create(:user) } - before do - allow(helper).to receive(:current_user).and_return(user) - end - - it 'preserves encoding' do - expect(content.encoding.name).to eq('UTF-8') - expect(helper.render_markup('foo.rst', content).encoding.name).to eq('UTF-8') - end - - it "delegates to #markdown when file name corresponds to Markdown" do - expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true) - expect(helper).to receive(:markdown).and_return('NOEL') - - expect(helper.render_markup('foo.md', content)).to eq('NOEL') - end - - it "delegates to #asciidoc when file name corresponds to AsciiDoc" do - expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true) - expect(helper).to receive(:asciidoc).and_return('NOEL') - - expect(helper.render_markup('foo.adoc', content)).to eq('NOEL') - end - end - describe '#active_when' do it { expect(helper.active_when(true)).to eq('active') } it { expect(helper.active_when(false)).to eq(nil) } diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index 6cf3f86680a..c10f4b09b5b 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' -describe GitlabMarkdownHelper do - include ApplicationHelper - +describe MarkupHelper do let!(:project) { create(:project, :repository) } let(:user) { create(:user, username: 'gfm') } @@ -111,9 +109,9 @@ describe GitlabMarkdownHelper do end it 'replaces commit message with emoji to link' do - actual = link_to_gfm(':book:Book', '/foo') + actual = link_to_gfm(':book: Book', '/foo') expect(actual). - to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo">Book</a>' + to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>' end end @@ -128,7 +126,7 @@ describe GitlabMarkdownHelper do it "uses Wiki pipeline for markdown files" do allow(@wiki).to receive(:format).and_return(:markdown) - expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki, page_slug: "nested/page") + expect(helper).to receive(:markdown_unsafe).with('wiki content', pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page") helper.render_wiki_content(@wiki) end @@ -136,7 +134,7 @@ describe GitlabMarkdownHelper do it "uses Asciidoctor for asciidoc files" do allow(@wiki).to receive(:format).and_return(:asciidoc) - expect(helper).to receive(:asciidoc).with('wiki content') + expect(helper).to receive(:asciidoc_unsafe).with('wiki content') helper.render_wiki_content(@wiki) end @@ -151,6 +149,29 @@ describe GitlabMarkdownHelper do end end + describe 'markup' do + let(:content) { 'Noël' } + + it 'preserves encoding' do + expect(content.encoding.name).to eq('UTF-8') + expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8') + end + + it "delegates to #markdown_unsafe when file name corresponds to Markdown" do + expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true) + expect(helper).to receive(:markdown_unsafe).and_return('NOEL') + + expect(helper.markup('foo.md', content)).to eq('NOEL') + end + + it "delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc" do + expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true) + expect(helper).to receive(:asciidoc_unsafe).and_return('NOEL') + + expect(helper.markup('foo.adoc', content)).to eq('NOEL') + end + end + describe '#first_line_in_markdown' do it 'truncates Markdown properly' do text = "@#{user.username}, can you look at this?\nHello world\n" diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js index 9762688af1a..1c54cc3054c 100644 --- a/spec/javascripts/environments/environment_spec.js +++ b/spec/javascripts/environments/environment_spec.js @@ -1,15 +1,18 @@ import Vue from 'vue'; import '~/flash'; -import EnvironmentsComponent from '~/environments/components/environment'; +import environmentsComponent from '~/environments/components/environment.vue'; import { environment, folder } from './mock_data'; describe('Environment', () => { preloadFixtures('static/environments/environments.html.raw'); + let EnvironmentsComponent; let component; beforeEach(() => { loadFixtures('static/environments/environments.html.raw'); + + EnvironmentsComponent = Vue.extend(environmentsComponent); }); describe('successfull request', () => { diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 72f3db29a66..350078ad5f5 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; import '~/flash'; -import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view'; +import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; import { environmentsList } from '../mock_data'; describe('Environments Folder View', () => { preloadFixtures('static/environments/environments_folder_view.html.raw'); + let EnvironmentsFolderViewComponent; beforeEach(() => { loadFixtures('static/environments/environments_folder_view.html.raw'); + EnvironmentsFolderViewComponent = Vue.extend(environmentsFolderViewComponent); window.history.pushState({}, null, 'environments/folders/build'); }); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 6683489f63c..e747aa497c2 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -26,6 +26,10 @@ describe('Filtered Search Manager', () => { element.dispatchEvent(event); } + function getVisualTokens() { + return tokensContainer.querySelectorAll('.js-visual-token'); + } + beforeEach(() => { setFixtures(` <div class="filtered-search-box"> @@ -170,11 +174,37 @@ describe('Filtered Search Manager', () => { }); }); - describe('removeSelectedToken', () => { - function getVisualTokens() { - return tokensContainer.querySelectorAll('.js-visual-token'); - } + describe('removeToken', () => { + it('removes token even when it is already selected', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), + ); + + tokensContainer.querySelector('.js-visual-token .remove-token').click(); + expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null); + }); + describe('unselected token', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough(); + + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'), + ); + tokensContainer.querySelector('.js-visual-token .remove-token').click(); + }); + + it('removes token when remove button is selected', () => { + expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null); + }); + + it('calls removeSelectedToken', () => { + expect(manager.removeSelectedToken).toHaveBeenCalled(); + }); + }); + }); + + describe('removeSelectedTokenKeydown', () => { beforeEach(() => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), @@ -224,6 +254,31 @@ describe('Filtered Search Manager', () => { }); }); + describe('removeSelectedToken', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough(); + spyOn(gl.FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough(); + spyOn(gl.FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough(); + manager.removeSelectedToken(); + }); + + it('calls FilteredSearchVisualTokens.removeSelectedToken', () => { + expect(gl.FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled(); + }); + + it('calls handleInputPlaceholder', () => { + expect(manager.handleInputPlaceholder).toHaveBeenCalled(); + }); + + it('calls toggleClearSearchButton', () => { + expect(manager.toggleClearSearchButton).toHaveBeenCalled(); + }); + + it('calls update dropdown offset', () => { + expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled(); + }); + }); + describe('unselects token', () => { beforeEach(() => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index bbda1476fed..d75b9061281 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -214,8 +214,12 @@ describe('Filtered Search Visual Tokens', () => { expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything()); }); + it('contains value container div', () => { + expect(tokenElement.querySelector('.value-container')).toEqual(jasmine.anything()); + }); + it('contains value div', () => { - expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything()); + expect(tokenElement.querySelector('.value-container .value')).toEqual(jasmine.anything()); }); it('contains selectable class', () => { @@ -225,6 +229,16 @@ describe('Filtered Search Visual Tokens', () => { it('contains button role', () => { expect(tokenElement.getAttribute('role')).toEqual('button'); }); + + describe('remove token', () => { + it('contains remove-token button', () => { + expect(tokenElement.querySelector('.value-container .remove-token')).toEqual(jasmine.anything()); + }); + + it('contains fa-close icon', () => { + expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(jasmine.anything()); + }); + }); }); describe('addVisualTokenElement', () => { diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js index ce83a256ddd..b8d4a93b1ab 100644 --- a/spec/javascripts/helpers/filtered_search_spec_helper.js +++ b/spec/javascripts/helpers/filtered_search_spec_helper.js @@ -10,7 +10,12 @@ class FilteredSearchSpecHelper { li.innerHTML = ` <div class="selectable ${isSelected ? 'selected' : ''}" role="button"> <div class="name">${name}</div> - <div class="value">${value}</div> + <div class="value-container"> + <div class="value">${value}</div> + <div class="remove-token" role="button"> + <i class="fa fa-close"></i> + </div> + </div> </div> `; diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js index e504d41d4d4..481b46c3ac6 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js @@ -3,70 +3,84 @@ import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; import '~/flash'; -(() => { - describe('Mini Pipeline Graph Dropdown', () => { - preloadFixtures('static/mini_dropdown_graph.html.raw'); +describe('Mini Pipeline Graph Dropdown', () => { + preloadFixtures('static/mini_dropdown_graph.html.raw'); - beforeEach(() => { - loadFixtures('static/mini_dropdown_graph.html.raw'); - }); + beforeEach(() => { + loadFixtures('static/mini_dropdown_graph.html.raw'); + }); - describe('When is initialized', () => { - it('should initialize without errors when no options are given', () => { - const miniPipelineGraph = new MiniPipelineGraph(); + describe('When is initialized', () => { + it('should initialize without errors when no options are given', () => { + const miniPipelineGraph = new MiniPipelineGraph(); - expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container'); - }); + expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container'); + }); - it('should set the container as the given prop', () => { - const container = '.foo'; + it('should set the container as the given prop', () => { + const container = '.foo'; - const miniPipelineGraph = new MiniPipelineGraph({ container }); + const miniPipelineGraph = new MiniPipelineGraph({ container }); - expect(miniPipelineGraph.container).toEqual(container); - }); + expect(miniPipelineGraph.container).toEqual(container); }); + }); - describe('When dropdown is clicked', () => { - it('should call getBuildsList', () => { - const getBuildsListSpy = spyOn(MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); + describe('When dropdown is clicked', () => { + it('should call getBuildsList', () => { + const getBuildsListSpy = spyOn( + MiniPipelineGraph.prototype, + 'getBuildsList', + ).and.callFake(function () {}); - new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); - document.querySelector('.js-builds-dropdown-button').click(); + document.querySelector('.js-builds-dropdown-button').click(); - expect(getBuildsListSpy).toHaveBeenCalled(); - }); + expect(getBuildsListSpy).toHaveBeenCalled(); + }); - it('should make a request to the endpoint provided in the html', () => { - const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); + it('should make a request to the endpoint provided in the html', () => { + const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); - new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); - document.querySelector('.js-builds-dropdown-button').click(); - expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); - }); + document.querySelector('.js-builds-dropdown-button').click(); + expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); + }); - it('should not close when user uses cmd/ctrl + click', () => { - spyOn($, 'ajax').and.callFake(function (params) { - params.success({ - html: `<li> - <a class="mini-pipeline-graph-dropdown-item" href="#"> - <span class="ci-status-icon ci-status-icon-failed"></span> - <span class="ci-build-text">build</span> - </a> - <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a> - </li>`, - }); + it('should not close when user uses cmd/ctrl + click', () => { + spyOn($, 'ajax').and.callFake(function (params) { + params.success({ + html: `<li> + <a class="mini-pipeline-graph-dropdown-item" href="#"> + <span class="ci-status-icon ci-status-icon-failed"></span> + <span class="ci-build-text">build</span> + </a> + <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a> + </li>`, }); - new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + }); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); - document.querySelector('.js-builds-dropdown-button').click(); + document.querySelector('.js-builds-dropdown-button').click(); - document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); + document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); - expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); - }); + expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); }); }); -})(); + + it('should close the dropdown when request returns an error', (done) => { + spyOn($, 'ajax').and.callFake(options => options.error()); + + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + + document.querySelector('.js-builds-dropdown-button').click(); + + setTimeout(() => { + expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false); + done(); + }, 0); + }); +}); diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index 66b57a82363..2f1154bd999 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -63,4 +63,19 @@ describe('Pipelines Stage', () => { expect(minifiedComponent).toContain(expectedSVG); }); }); + + describe('when request fails', () => { + it('closes dropdown', () => { + spyOn($, 'ajax').and.callFake(options => options.error()); + const StageComponent = Vue.extend(Stage); + + const component = new StageComponent({ + propsData: { stage: { status: { icon: 'foo' } } }, + }).$mount(); + + expect( + component.$el.classList.contains('open'), + ).toEqual(false); + }); + }); }); diff --git a/spec/javascripts/pipelines/time_ago_spec.js b/spec/javascripts/pipelines/time_ago_spec.js new file mode 100644 index 00000000000..24581e8c672 --- /dev/null +++ b/spec/javascripts/pipelines/time_ago_spec.js @@ -0,0 +1,64 @@ +import Vue from 'vue'; +import timeAgo from '~/pipelines/components/time_ago'; + +describe('Timeago component', () => { + let TimeAgo; + beforeEach(() => { + TimeAgo = Vue.extend(timeAgo); + }); + + describe('with duration', () => { + it('should render duration and timer svg', () => { + const component = new TimeAgo({ + propsData: { + duration: 10, + finishedTime: '', + }, + }).$mount(); + + expect(component.$el.querySelector('.duration')).toBeDefined(); + expect(component.$el.querySelector('.duration svg')).toBeDefined(); + }); + }); + + describe('without duration', () => { + it('should not render duration and timer svg', () => { + const component = new TimeAgo({ + propsData: { + duration: 0, + finishedTime: '', + }, + }).$mount(); + + expect(component.$el.querySelector('.duration')).toBe(null); + }); + }); + + describe('with finishedTime', () => { + it('should render time and calendar icon', () => { + const component = new TimeAgo({ + propsData: { + duration: 0, + finishedTime: '2017-04-26T12:40:23.277Z', + }, + }).$mount(); + + expect(component.$el.querySelector('.finished-at')).toBeDefined(); + expect(component.$el.querySelector('.finished-at i.fa-calendar')).toBeDefined(); + expect(component.$el.querySelector('.finished-at time')).toBeDefined(); + }); + }); + + describe('without finishedTime', () => { + it('should not render time and calendar icon', () => { + const component = new TimeAgo({ + propsData: { + duration: 0, + finishedTime: '', + }, + }).$mount(); + + expect(component.$el.querySelector('.finished-at')).toBe(null); + }); + }); +}); diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb index 707212e07fd..086a006c45f 100644 --- a/spec/lib/banzai/filter/emoji_filter_spec.rb +++ b/spec/lib/banzai/filter/emoji_filter_spec.rb @@ -68,9 +68,9 @@ describe Banzai::Filter::EmojiFilter, lib: true do expect(doc.css('gl-emoji').size).to eq 1 end - it 'matches multiple emoji in a row' do + it 'does not match multiple emoji in a row' do doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:') - expect(doc.css('gl-emoji').size).to eq 3 + expect(doc.css('gl-emoji').size).to eq 0 end it 'unicode matches multiple emoji in a row' do @@ -83,6 +83,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do expect(doc.css('gl-emoji').size).to eq 6 end + it 'does not match emoji in a string' do + doc = filter("'2a00:a4c0:100::1'") + + expect(doc.css('gl-emoji').size).to eq 0 + end + it 'has a data-name attribute' do doc = filter(':-1:') expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown' diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index bca57105d1d..0f47fb2fbd9 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -22,26 +22,9 @@ module Gitlab expect(Asciidoctor).to receive(:convert) .with(input, expected_asciidoc_opts).and_return(html) - expect( render(input, context) ).to eql html + expect(render(input)).to eq(html) end - context "with asciidoc_opts" do - let(:asciidoc_opts) { { safe: :safe, attributes: ['foo'] } } - - it "merges the options with default ones" do - expected_asciidoc_opts = { - safe: :safe, - backend: :gitlab_html5, - attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo'] - } - - expect(Asciidoctor).to receive(:convert) - .with(input, expected_asciidoc_opts).and_return(html) - - render(input, context, asciidoc_opts) - end - end - context "XSS" do links = { 'links' => { @@ -60,7 +43,7 @@ module Gitlab links.each do |name, data| it "does not convert dangerous #{name} into HTML" do - expect(render(data[:input], context)).to eql data[:output] + expect(render(data[:input])).to eq(data[:output]) end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 3d6d7292b42..f88653cb1fe 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1031,6 +1031,35 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#find_commits' do + it 'should return a return a collection of commits' do + commits = repository.find_commits + + expect(commits).not_to be_empty + expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) ) + end + + context 'while applying a sort order based on the `order` option' do + it "allows ordering topologically (no parents shown before their children)" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO) + + repository.find_commits(order: :topo) + end + + it "allows ordering by date" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE) + + repository.find_commits(order: :date) + end + + it "applies no sorting by default" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE) + + repository.find_commits + end + end + end + describe '#branches with deleted branch' do before(:each) do ref = double() diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 7a0b0b06d4b..bfecfa28ed1 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6981,28 +6981,6 @@ ], "services": [ { - "id": 164, - "title": null, - "project_id": 5, - "created_at": "2016-06-14T15:02:07.372Z", - "updated_at": "2016-06-14T15:02:07.372Z", - "active": false, - "properties": { - - }, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "build_events": true, - "category": "issue_tracker", - "type": "CustomIssueTrackerService", - "default": true, - "wiki_page_events": true - }, - { "id": 100, "title": "JetBrains TeamCity CI", "project_id": 5, @@ -7019,6 +6997,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "TeamcityService", "category": "ci", "default": false, "wiki_page_events": true @@ -7040,6 +7019,7 @@ "tag_push_events": true, "note_events": true, "pipeline_events": true, + "type": "SlackService", "category": "common", "default": false, "wiki_page_events": true @@ -7061,6 +7041,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "RedmineService", "category": "issue_tracker", "default": false, "wiki_page_events": true @@ -7082,6 +7063,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "PushoverService", "category": "common", "default": false, "wiki_page_events": true @@ -7103,6 +7085,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "PivotalTrackerService", "category": "common", "default": false, "wiki_page_events": true @@ -7125,6 +7108,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "JiraService", "category": "issue_tracker", "default": false, "wiki_page_events": true @@ -7146,6 +7130,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "IrkerService", "category": "common", "default": false, "wiki_page_events": true @@ -7167,6 +7152,7 @@ "tag_push_events": true, "note_events": true, "pipeline_events": true, + "type": "HipchatService", "category": "common", "default": false, "wiki_page_events": true @@ -7188,6 +7174,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "GemnasiumService", "category": "common", "default": false, "wiki_page_events": true @@ -7209,6 +7196,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "FlowdockService", "category": "common", "default": false, "wiki_page_events": true @@ -7230,6 +7218,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "ExternalWikiService", "category": "common", "default": false, "wiki_page_events": true @@ -7251,6 +7240,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "EmailsOnPushService", "category": "common", "default": false, "wiki_page_events": true @@ -7272,6 +7262,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "DroneCiService", "category": "ci", "default": false, "wiki_page_events": true @@ -7293,6 +7284,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "CustomIssueTrackerService", "category": "issue_tracker", "default": false, "wiki_page_events": true @@ -7314,6 +7306,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "CampfireService", "category": "common", "default": false, "wiki_page_events": true @@ -7335,6 +7328,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "BuildkiteService", "category": "ci", "default": false, "wiki_page_events": true @@ -7356,6 +7350,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "BambooService", "category": "ci", "default": false, "wiki_page_events": true @@ -7377,6 +7372,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "AssemblaService", "category": "common", "default": false, "wiki_page_events": true @@ -7398,6 +7394,7 @@ "tag_push_events": true, "note_events": true, "build_events": true, + "type": "AssemblaService", "category": "common", "default": false, "wiki_page_events": true diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb index fcc23a75ca1..06cd8ab87ed 100644 --- a/spec/lib/gitlab/import_export/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb @@ -60,7 +60,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do end context 'original service exists' do - let(:service_id) { Service.create(project: project).id } + let(:service_id) { create(:service, project: project).id } it 'does not have the original service_id' do expect(created_object.service_id).not_to eq(service_id) diff --git a/spec/lib/gitlab/other_markup_spec.rb b/spec/lib/gitlab/other_markup_spec.rb index 22e80ec46be..d6d53e8586c 100644 --- a/spec/lib/gitlab/other_markup_spec.rb +++ b/spec/lib/gitlab/other_markup_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::OtherMarkup, lib: true do } links.each do |name, data| it "does not convert dangerous #{name} into HTML" do - expect(render(data[:file], data[:input], context)).to eql data[:output] + expect(render(data[:file], data[:input])).to eq(data[:output]) end end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 01ca1584ed2..c2c19c62048 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -4,6 +4,7 @@ describe ApplicationSetting, models: true do let(:setting) { ApplicationSetting.create_from_defaults } it { expect(setting).to be_valid } + it { expect(setting.uuid).to be_present } describe 'validations' do let(:http) { 'http://example.com' } diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb index 492c4e01bd8..46b36e11c23 100644 --- a/spec/models/network/graph_spec.rb +++ b/spec/models/network/graph_spec.rb @@ -9,4 +9,25 @@ describe Network::Graph, models: true do expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } ) end + + describe "#commits" do + let(:graph) { described_class.new(project, 'refs/heads/master', project.repository.commit, nil) } + + it "returns a list of commits" do + commits = graph.commits + + expect(commits).not_to be_empty + expect(commits).to all( be_kind_of(Network::Commit) ) + end + + it "sorts the commits by commit date (descending)" do + # Remove duplicate timestamps because they make it harder to + # assert that the commits are sorted as expected. + commits = graph.commits.uniq(&:date) + sorted_commits = commits.sort_by(&:date).reverse + + expect(commits).not_to be_empty + expect(commits.map(&:id)).to eq(sorted_commits.map(&:id)) + end + end end diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb index 592c90cda36..8fbe42248ae 100644 --- a/spec/models/project_services/chat_notification_service_spec.rb +++ b/spec/models/project_services/chat_notification_service_spec.rb @@ -11,10 +11,10 @@ describe ChatNotificationService, models: true do describe '#can_test?' do context 'with empty repository' do - it 'returns false' do + it 'returns true' do subject.project = create(:empty_project, :empty_repo) - expect(subject.can_test?).to be false + expect(subject.can_test?).to be true end end diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb index fbe6f344a98..869b25b933b 100644 --- a/spec/models/project_services/issue_tracker_service_spec.rb +++ b/spec/models/project_services/issue_tracker_service_spec.rb @@ -8,7 +8,7 @@ describe IssueTrackerService, models: true do let(:service) { RedmineService.new(project: project, active: true) } before do - create(:service, project: project, active: true, category: 'issue_tracker') + create(:custom_issue_tracker_service, project: project) end context 'when service is changed manually by user' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 74d5ebc6db0..98d0641443e 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1803,9 +1803,9 @@ describe Repository, models: true do describe '#refresh_method_caches' do it 'refreshes the caches of the given types' do expect(repository).to receive(:expire_method_caches). - with(%i(readme license_blob license_key)) + with(%i(rendered_readme license_blob license_key)) - expect(repository).to receive(:readme) + expect(repository).to receive(:rendered_readme) expect(repository).to receive(:license_blob) expect(repository).to receive(:license_key) diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 0e2f07e945f..134882648b9 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -6,44 +6,53 @@ describe Service, models: true do it { is_expected.to have_one :service_hook } end + describe 'Validations' do + it { is_expected.to validate_presence_of(:type) } + end + describe "Test Button" do - before do - @service = Service.new - end + describe '#can_test?' do + let(:service) { create(:service, project: project) } - describe "Testable" do - let(:project) { create(:project, :repository) } + context 'when repository is not empty' do + let(:project) { create(:project, :repository) } - before do - allow(@service).to receive(:project).and_return(project) - @testable = @service.can_test? + it 'returns true' do + expect(service.can_test?).to be true + end end - describe '#can_test?' do - it { expect(@testable).to eq(true) } + context 'when repository is empty' do + let(:project) { create(:empty_project) } + + it 'returns true' do + expect(service.can_test?).to be true + end end + end + + describe '#test' do + let(:data) { 'test' } + let(:service) { create(:service, project: project) } - describe '#test' do - let(:data) { 'test' } + context 'when repository is not empty' do + let(:project) { create(:project, :repository) } it 'test runs execute' do - expect(@service).to receive(:execute).with(data) + expect(service).to receive(:execute).with(data) - @service.test(data) + service.test(data) end end - end - describe "With commits" do - let(:project) { create(:project, :repository) } + context 'when repository is empty' do + let(:project) { create(:empty_project) } - before do - allow(@service).to receive(:project).and_return(project) - @testable = @service.can_test? - end + it 'test runs execute' do + expect(service).to receive(:execute).with(data) - describe '#can_test?' do - it { expect(@testable).to eq(true) } + service.test(data) + end end end end diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 4845ab1ae1f..06c8eb1d0b7 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -427,6 +427,7 @@ describe API::Helpers do context 'current_user is nil' do before do expect_any_instance_of(self.class).to receive(:current_user).and_return(nil) + allow_any_instance_of(self.class).to receive(:initial_current_user).and_return(nil) end it 'returns a 401 response' do @@ -435,13 +436,38 @@ describe API::Helpers do end context 'current_user is present' do + let(:user) { build(:user) } + before do - expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(User.new) + expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user) + expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user) end it 'does not raise an error' do expect { authenticate! }.not_to raise_error end end + + context 'current_user is blocked' do + let(:user) { build(:user, :blocked) } + + before do + expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user) + end + + it 'raises an error' do + expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user) + + expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}' + end + + it "doesn't raise an error if an admin user is impersonating a blocked user (via sudo)" do + admin_user = build(:user, :admin) + + expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(admin_user) + + expect { authenticate! }.not_to raise_error + end + end end end diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb index 694786c3046..0389a264781 100644 --- a/spec/requests/api/v3/deployments_spec.rb +++ b/spec/requests/api/v3/deployments_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::Deployments do +describe API::V3::Deployments do let(:user) { create(:user) } let(:non_member) { create(:user) } let(:project) { deployment.environment.project } @@ -24,11 +24,11 @@ describe API::Deployments do describe 'GET /projects/:id/deployments' do context 'as member of the project' do it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/deployments", user) } + let(:request) { get v3_api("/projects/#{project.id}/deployments", user) } end it 'returns projects deployments' do - get api("/projects/#{project.id}/deployments", user) + get v3_api("/projects/#{project.id}/deployments", user) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -40,7 +40,7 @@ describe API::Deployments do context 'as non member' do it 'returns a 404 status code' do - get api("/projects/#{project.id}/deployments", non_member) + get v3_api("/projects/#{project.id}/deployments", non_member) expect(response).to have_http_status(404) end @@ -50,7 +50,7 @@ describe API::Deployments do describe 'GET /projects/:id/deployments/:deployment_id' do context 'as a member of the project' do it 'returns the projects deployment' do - get api("/projects/#{project.id}/deployments/#{deployment.id}", user) + get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", user) expect(response).to have_http_status(200) expect(json_response['sha']).to match /\A\h{40}\z/ @@ -60,7 +60,7 @@ describe API::Deployments do context 'as non member' do it 'returns a 404 status code' do - get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member) + get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", non_member) expect(response).to have_http_status(404) end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 7a07ea618c0..033e6ecd18c 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -27,6 +27,22 @@ describe Projects::CreateService, '#execute', services: true do end end + context "admin creates project with other user's namespace_id" do + it 'sets the correct permissions' do + admin = create(:admin) + opts = { + name: 'GitLab', + namespace_id: user.namespace.id + } + project = create_project(admin, opts) + + expect(project).to be_persisted + expect(project.owner).to eq(user) + expect(project.team.masters).to include(user, admin) + expect(project.namespace).to eq(user.namespace) + end + end + context 'group namespace' do let(:group) do create(:group).tap do |group| diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 42d63a9f9ba..75d7caf2508 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -595,7 +595,7 @@ describe SystemNoteService, services: true do end shared_examples 'cross project mentionable' do - include GitlabMarkdownHelper + include MarkupHelper it 'contains cross reference to new noteable' do expect(subject.note).to include cross_project_reference(new_project, new_noteable) diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index daea0c6bb37..0a4a6ed8145 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -350,7 +350,7 @@ describe 'gitlab:app namespace rake task' do end it 'name has human readable time' do - expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_gitlab_backup.tar$/) + expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+(-pre)?_gitlab_backup.tar$/) end end end # gitlab:app namespace diff --git a/vendor/Dockerfile/CONTRIBUTING.md b/vendor/Dockerfile/CONTRIBUTING.md new file mode 100644 index 00000000000..91b92eafa1b --- /dev/null +++ b/vendor/Dockerfile/CONTRIBUTING.md @@ -0,0 +1,5 @@ +The canonical repository for `Dockerfile` templates is +https://gitlab.com/gitlab-org/Dockerfile. + +GitLab only mirrors the templates. Please submit your merge requests to +https://gitlab.com/gitlab-org/Dockerfile. diff --git a/vendor/dockerfile/HTTPdDockerfile b/vendor/Dockerfile/HTTPd.Dockerfile index 2f05427323c..2f05427323c 100644 --- a/vendor/dockerfile/HTTPdDockerfile +++ b/vendor/Dockerfile/HTTPd.Dockerfile diff --git a/vendor/Dockerfile/LICENSE b/vendor/Dockerfile/LICENSE new file mode 100644 index 00000000000..d6c93c6fcf7 --- /dev/null +++ b/vendor/Dockerfile/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016-2017 GitLab.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/Dockerfile/PHP.Dockerfile b/vendor/Dockerfile/PHP.Dockerfile new file mode 100644 index 00000000000..6b098efcd85 --- /dev/null +++ b/vendor/Dockerfile/PHP.Dockerfile @@ -0,0 +1,14 @@ +FROM php:7.0-apache + +# Customize any core extensions here +#RUN apt-get update && apt-get install -y \ +# libfreetype6-dev \ +# libjpeg62-turbo-dev \ +# libmcrypt-dev \ +# libpng12-dev \ +# && docker-php-ext-install -j$(nproc) iconv mcrypt \ +# && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \ +# && docker-php-ext-install -j$(nproc) gd + +COPY config/php.ini /usr/local/etc/php/ +COPY src/ /var/www/html/ diff --git a/vendor/Dockerfile/Python2.Dockerfile b/vendor/Dockerfile/Python2.Dockerfile new file mode 100644 index 00000000000..c9a03584d40 --- /dev/null +++ b/vendor/Dockerfile/Python2.Dockerfile @@ -0,0 +1,11 @@ +FROM python:2.7 + +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +COPY requirements.txt /usr/src/app/ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . /usr/src/app + +CMD ["python", "app.py"] |