diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-18 12:07:48 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-18 12:07:48 +0000 |
commit | 79d62647bcfad69d7272020acb7d8be5ee5df003 (patch) | |
tree | 008d96a4c5fdfdecda79dae5e942c7df07511c77 | |
parent | 1a9d9cc14ec54036548824e3ce17da03960f5f81 (diff) | |
download | gitlab-ce-79d62647bcfad69d7272020acb7d8be5ee5df003.tar.gz |
Add latest changes from gitlab-org/gitlab@master
60 files changed, 1570 insertions, 246 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml index a8cbd9731a3..3c304e957b4 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -50,7 +50,6 @@ rules: # all offenses of no-jquery/no-animate-toggle are false positives ( $toast.show() ) no-jquery/no-animate-toggle: off no-jquery/no-event-shorthand: off - no-jquery/no-fade: off no-jquery/no-serialize: error no-jquery/no-sizzle: off promise/always-return: off @@ -458,7 +458,7 @@ gem 'grpc', '~> 1.24.0' gem 'google-protobuf', '~> 3.8.0' -gem 'toml-rb', '~> 1.0.0', require: false +gem 'toml-rb', '~> 1.0.0' # Feature toggles gem 'flipper', '~> 0.17.1' diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index a28e17f7a56..fb8b1c17407 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -40,7 +40,10 @@ export default class ImageFile { .removeClass('active') .filter(`.${viewMode}`) .addClass('active'); + + // eslint-disable-next-line no-jquery/no-fade return $(`.view:visible:not(.${viewMode})`, this.file).fadeOut(200, () => { + // eslint-disable-next-line no-jquery/no-fade $(`.view.${viewMode}`, this.file).fadeIn(200); return this.initView(viewMode); }); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 3c650397a19..b973316b3b9 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -116,11 +116,13 @@ class DueDateSelect { } updateIssueBoardIssue() { + // eslint-disable-next-line no-jquery/no-fade this.$loading.fadeIn(); this.$dropdown.trigger('loading.gl.dropdown'); this.$selectbox.hide(); this.$value.css('display', ''); const fadeOutLoader = () => { + // eslint-disable-next-line no-jquery/no-fade this.$loading.fadeOut(); }; @@ -135,6 +137,7 @@ class DueDateSelect { const hasDueDate = this.displayedDate !== __('None'); const displayedDateStyle = hasDueDate ? 'bold' : 'no-value'; + // eslint-disable-next-line no-jquery/no-fade this.$loading.removeClass('hidden').fadeIn(); if (isDropdown) { @@ -158,6 +161,7 @@ class DueDateSelect { } this.$sidebarCollapsedValue.attr('data-original-title', tooltipText); + // eslint-disable-next-line no-jquery/no-fade return this.$loading.fadeOut(); }); } diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index 8e2128ac713..1042029b6db 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -25,10 +25,33 @@ export default { PREV_PAGE: 1, NEXT_PAGE: 2, fields: [ - { key: 'error', label: __('Open errors'), thClass: 'w-70p' }, - { key: 'events', label: __('Events') }, - { key: 'users', label: __('Users') }, - { key: 'lastSeen', label: __('Last seen'), thClass: 'w-15p' }, + { + key: 'error', + label: __('Error'), + thClass: 'w-70p', + tdClass: 'table-col d-flex align-items-center d-sm-table-cell', + }, + { + key: 'events', + label: __('Events'), + tdClass: 'table-col d-flex align-items-center d-sm-table-cell', + }, + { + key: 'users', + label: __('Users'), + tdClass: 'table-col d-flex align-items-center d-sm-table-cell', + }, + { + key: 'lastSeen', + label: __('Last seen'), + thClass: 'w-15p', + tdClass: 'table-col d-flex align-items-center d-sm-table-cell', + }, + { + key: 'details', + tdClass: 'table-col d-sm-none d-flex align-items-center', + thClass: 'invisible w-0', + }, ], sortFields: { last_seen: __('Last Seen'), @@ -149,61 +172,63 @@ export default { <div class="error-list"> <div v-if="errorTrackingEnabled"> <div - class="d-flex flex-row justify-content-around align-items-center bg-secondary border mt-2" + class="row flex-column flex-sm-row align-items-sm-center row-top m-0 mt-sm-2 mx-sm-1 p-0 p-sm-3" > - <div class="filtered-search-box flex-grow-1 my-3 ml-3 mr-2"> - <gl-dropdown - :text="__('Recent searches')" - class="filtered-search-history-dropdown-wrapper d-none d-md-block" - toggle-class="filtered-search-history-dropdown-toggle-button" - :disabled="loading" - > - <div v-if="!$options.hasLocalStorage" class="px-3"> - {{ __('This feature requires local storage to be enabled') }} - </div> - <template v-else-if="recentSearches.length > 0"> - <gl-dropdown-item - v-for="searchQuery in recentSearches" - :key="searchQuery" - @click="setSearchText(searchQuery)" - >{{ searchQuery }}</gl-dropdown-item - > - <gl-dropdown-divider /> - <gl-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches">{{ - __('Clear recent searches') - }}</gl-dropdown-item> - </template> - <div v-else class="px-3">{{ __("You don't have any recent searches") }}</div> - </gl-dropdown> - <div class="filtered-search-input-container flex-fill"> - <gl-form-input - v-model="errorSearchQuery" - class="pl-2 filtered-search" + <div class="search-box flex-fill mr-sm-2 my-3 m-sm-0 p-3 p-sm-0"> + <div class="filtered-search-box mb-0"> + <gl-dropdown + :text="__('Recent searches')" + class="filtered-search-history-dropdown-wrapper" + toggle-class="filtered-search-history-dropdown-toggle-button" :disabled="loading" - :placeholder="__('Search or filter results…')" - autofocus - @keyup.enter.native="searchByQuery(errorSearchQuery)" - /> - </div> - <div class="gl-search-box-by-type-right-icons"> - <gl-button - v-if="errorSearchQuery.length > 0" - v-gl-tooltip.hover - :title="__('Clear')" - class="clear-search text-secondary" - name="clear" - @click="errorSearchQuery = ''" > - <gl-icon name="close" :size="12" /> - </gl-button> + <div v-if="!$options.hasLocalStorage" class="px-3"> + {{ __('This feature requires local storage to be enabled') }} + </div> + <template v-else-if="recentSearches.length > 0"> + <gl-dropdown-item + v-for="searchQuery in recentSearches" + :key="searchQuery" + @click="setSearchText(searchQuery)" + >{{ searchQuery }} + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches" + >{{ __('Clear recent searches') }} + </gl-dropdown-item> + </template> + <div v-else class="px-3">{{ __("You don't have any recent searches") }}</div> + </gl-dropdown> + <div class="filtered-search-input-container flex-fill"> + <gl-form-input + v-model="errorSearchQuery" + class="pl-2 filtered-search" + :disabled="loading" + :placeholder="__('Search or filter results…')" + autofocus + @keyup.enter.native="searchByQuery(errorSearchQuery)" + /> + </div> + <div class="gl-search-box-by-type-right-icons"> + <gl-button + v-if="errorSearchQuery.length > 0" + v-gl-tooltip.hover + :title="__('Clear')" + class="clear-search text-secondary" + name="clear" + @click="errorSearchQuery = ''" + > + <gl-icon name="close" :size="12" /> + </gl-button> + </div> </div> </div> <gl-dropdown + class="sort-control" :text="$options.sortFields[sortField]" left :disabled="loading" - class="mr-3" menu-class="sort-dropdown" > <gl-dropdown-item @@ -227,62 +252,77 @@ export default { <gl-loading-icon size="md" /> </div> - <gl-table - v-else - class="mt-3" - :items="errors" - :fields="$options.fields" - :show-empty="true" - fixed - stacked="sm" - > - <template slot="HEAD_events" slot-scope="data"> - <div class="text-md-right">{{ data.label }}</div> - </template> - <template slot="HEAD_users" slot-scope="data"> - <div class="text-md-right">{{ data.label }}</div> - </template> - <template slot="error" slot-scope="errors"> - <div class="d-flex flex-column"> - <gl-link class="d-flex text-dark" :href="getDetailsLink(errors.item.id)"> - <strong class="text-truncate">{{ errors.item.title.trim() }}</strong> - </gl-link> - <span class="text-secondary text-truncate"> - {{ errors.item.culprit }} - </span> - </div> - </template> - <template slot="events" slot-scope="errors"> - <div class="text-md-right">{{ errors.item.count }}</div> - </template> + <template v-else> + <h4 class="d-block d-sm-none my-3">{{ __('Open errors') }}</h4> - <template slot="users" slot-scope="errors"> - <div class="text-md-right">{{ errors.item.userCount }}</div> - </template> + <gl-table + class="mt-3" + :items="errors" + :fields="$options.fields" + :show-empty="true" + fixed + stacked="sm" + tbody-tr-class="table-row mb-4" + > + <template v-slot:head(error)> + <div class="d-none d-sm-block">{{ __('Open errors') }}</div> + </template> + <template v-slot:head(events)="data"> + <div class="text-sm-right">{{ data.label }}</div> + </template> + <template v-slot:head(users)="data"> + <div class="text-sm-right">{{ data.label }}</div> + </template> - <template slot="lastSeen" slot-scope="errors"> - <div class="d-flex align-items-center"> - <time-ago :time="errors.item.lastSeen" class="text-secondary" /> - </div> - </template> - <template slot="empty"> - <div ref="empty"> + <template v-slot:error="errors"> + <div class="d-flex flex-column"> + <gl-link class="d-flex mw-100 text-dark" :href="getDetailsLink(errors.item.id)"> + <strong class="text-truncate">{{ errors.item.title.trim() }}</strong> + </gl-link> + <span class="text-secondary text-truncate mw-100"> + {{ errors.item.culprit }} + </span> + </div> + </template> + <template v-slot:events="errors"> + <div class="text-right">{{ errors.item.count }}</div> + </template> + + <template v-slot:users="errors"> + <div class="text-right">{{ errors.item.userCount }}</div> + </template> + + <template v-slot:lastSeen="errors"> + <div class="text-md-left text-right"> + <time-ago :time="errors.item.lastSeen" class="text-secondary" /> + </div> + </template> + <template v-slot:details="errors"> + <gl-button + :href="getDetailsLink(errors.item.id)" + variant="outline-info" + class="d-block" + > + {{ __('More details') }} + </gl-button> + </template> + <template v-slot:empty> {{ __('No errors to display.') }} <gl-link class="js-try-again" @click="restartPolling"> {{ __('Check again') }} </gl-link> - </div> - </template> - </gl-table> - <gl-pagination - v-show="!loading" - v-if="paginationRequired" - :prev-page="$options.PREV_PAGE" - :next-page="$options.NEXT_PAGE" - :value="pageValue" - align="center" - @input="goToPage" - /> + </template> + </gl-table> + <gl-pagination + v-show="!loading" + v-if="paginationRequired" + :prev-page="$options.PREV_PAGE" + :next-page="$options.NEXT_PAGE" + :value="pageValue" + align="center" + @input="goToPage" + /> + </template> </div> <div v-else-if="userCanEnableErrorTracking"> <gl-empty-state diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index c21fba06d42..be2eee828ff 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -64,6 +64,7 @@ export default class FilterableList { return false; } + // eslint-disable-next-line no-jquery/no-fade $(this.listHolderElement).fadeTo(250, 0.5); this.isBusy = true; @@ -98,6 +99,7 @@ export default class FilterableList { onFilterComplete() { this.isBusy = false; + // eslint-disable-next-line no-jquery/no-fade $(this.listHolderElement).fadeTo(250, 1); } } diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 6abf723be9a..f57febbda37 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -45,6 +45,7 @@ export default class LabelsSelect { const $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); const $value = $block.find('.value'); const $dropdownMenu = $dropdown.parent().find('.dropdown-menu'); + // eslint-disable-next-line no-jquery/no-fade const $loading = $block.find('.block-loading').fadeOut(); const fieldName = $dropdown.data('fieldName'); let initialSelected = $selectbox @@ -84,6 +85,7 @@ export default class LabelsSelect { if (!selected.length) { data[abilityName].label_ids = ['']; } + // eslint-disable-next-line no-jquery/no-fade $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); axios @@ -91,6 +93,7 @@ export default class LabelsSelect { .then(({ data }) => { let labelTooltipTitle; let template; + // eslint-disable-next-line no-jquery/no-fade $loading.fadeOut(); $dropdown.trigger('loaded.gl.dropdown'); $selectbox.hide(); @@ -361,6 +364,7 @@ export default class LabelsSelect { const label = clickEvent.selectedObj; const fadeOutLoader = () => { + // eslint-disable-next-line no-jquery/no-fade $loading.fadeOut(); }; @@ -422,6 +426,7 @@ export default class LabelsSelect { boardsStore.detail.issue.labels = labels; } + // eslint-disable-next-line no-jquery/no-fade $loading.fadeIn(); const oldLabels = boardsStore.detail.issue.labels; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 674415c9d01..8373ddd7a43 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -113,6 +113,7 @@ function deferredInitialisation() { }); $('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() { + // eslint-disable-next-line no-jquery/no-fade $(this) .closest('tr') .fadeOut(); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 1738dbe439c..d15e4ecb537 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -52,6 +52,7 @@ export default class MilestoneSelect { const $block = $selectBox.closest('.block'); const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); const $value = $block.find('.value'); + // eslint-disable-next-line no-jquery/no-fade const $loading = $block.find('.block-loading').fadeOut(); selectedMilestoneDefault = showAny ? '' : null; selectedMilestoneDefault = @@ -202,15 +203,18 @@ export default class MilestoneSelect { } $dropdown.trigger('loading.gl.dropdown'); + // eslint-disable-next-line no-jquery/no-fade $loading.removeClass('hidden').fadeIn(); boardsStore.detail.issue .update($dropdown.attr('data-issue-update')) .then(() => { $dropdown.trigger('loaded.gl.dropdown'); + // eslint-disable-next-line no-jquery/no-fade $loading.fadeOut(); }) .catch(() => { + // eslint-disable-next-line no-jquery/no-fade $loading.fadeOut(); }); } else { @@ -218,12 +222,14 @@ export default class MilestoneSelect { data = {}; data[abilityName] = {}; data[abilityName].milestone_id = selected != null ? selected : null; + // eslint-disable-next-line no-jquery/no-fade $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); return axios .put(issueUpdateURL, data) .then(({ data }) => { $dropdown.trigger('loaded.gl.dropdown'); + // eslint-disable-next-line no-jquery/no-fade $loading.fadeOut(); $selectBox.hide(); $value.css('display', ''); @@ -247,6 +253,7 @@ export default class MilestoneSelect { } }) .catch(() => { + // eslint-disable-next-line no-jquery/no-fade $loading.fadeOut(); }); } diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 492d8de3802..4ca32b9b005 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -336,6 +336,7 @@ export default { <markdown-field ref="markdownField" + :is-submitting="isSubmitting" :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index bf2880aca9c..6d7d863f273 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -53,6 +53,7 @@ function UsersSelect(currentUser, els, options = {}) { const abilityName = $dropdown.data('abilityName'); let $value = $block.find('.value'); const $collapsedSidebar = $block.find('.sidebar-collapsed-user'); + // eslint-disable-next-line no-jquery/no-fade const $loading = $block.find('.block-loading').fadeOut(); const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null; let selectedId = $dropdown.data('selected'); @@ -188,6 +189,7 @@ function UsersSelect(currentUser, els, options = {}) { const data = {}; data[abilityName] = {}; data[abilityName].assignee_id = selected != null ? selected : null; + // eslint-disable-next-line no-jquery/no-fade $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); @@ -195,6 +197,7 @@ function UsersSelect(currentUser, els, options = {}) { let user = {}; let tooltipTitle = user.name; $dropdown.trigger('loaded.gl.dropdown'); + // eslint-disable-next-line no-jquery/no-fade $loading.fadeOut(); if (data.assignee) { user = { diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 326440f5013..4f5f3ee5cf9 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -20,6 +20,11 @@ export default { Suggestions, }, props: { + isSubmitting: { + type: Boolean, + required: false, + default: false, + }, markdownPreviewPath: { type: String, required: false, @@ -133,6 +138,20 @@ export default { ); }, }, + watch: { + isSubmitting(isSubmitting) { + if (!isSubmitting || !this.$refs['markdown-preview'].querySelectorAll) { + return; + } + const mediaInPreview = this.$refs['markdown-preview'].querySelectorAll('video, audio'); + + if (mediaInPreview) { + mediaInPreview.forEach(media => { + media.pause(); + }); + } + }, + }, mounted() { /* GLForm class handles all the toolbar buttons @@ -177,7 +196,6 @@ export default { this.renderMarkdown(); } }, - showWriteTab() { this.markdownPreview = ''; this.previewMarkdown = false; diff --git a/app/assets/stylesheets/pages/error_list.scss b/app/assets/stylesheets/pages/error_list.scss new file mode 100644 index 00000000000..f97953ce824 --- /dev/null +++ b/app/assets/stylesheets/pages/error_list.scss @@ -0,0 +1,69 @@ +$gray-border: 1px solid $border-color; + +.error-list { + .sort-control { + .btn { + padding-right: 2rem; + } + + .gl-dropdown-caret { + position: absolute; + right: 0.5rem; + top: 0.5rem; + } + } + + @include media-breakpoint-up(sm) { + .row-top { + border: $gray-border; + background-color: $gray-50; + } + } + + @include media-breakpoint-down(xs) { + .table-row { + border: $gray-border; + border-radius: 4px; + } + + .search-box { + border-top: $gray-border; + border-bottom: $gray-border; + background-color: $gray-50; + } + + .table-col { + min-height: 68px; + + &::before { + text-align: left !important; + } + + &:first-child { + div { + padding: 0 !important; + align-items: flex-end; + } + } + + &:last-child { + height: 64px; + background-color: $gray-normal; + + &::before { + content: none !important; + } + + div { + width: 100% !important; + padding: 0 !important; + + a { + color: $blue-500; + border-color: $blue-500; + } + } + } + } + } +} diff --git a/app/models/blob.rb b/app/models/blob.rb index 0a425f2b961..42ee00bc196 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -51,6 +51,7 @@ class Blob < SimpleDelegator BlobViewer::Contributing, BlobViewer::Changelog, + BlobViewer::CargoToml, BlobViewer::Cartfile, BlobViewer::ComposerJson, BlobViewer::Gemfile, diff --git a/app/models/blob_viewer/cargo_toml.rb b/app/models/blob_viewer/cargo_toml.rb new file mode 100644 index 00000000000..2f1ebd25b4f --- /dev/null +++ b/app/models/blob_viewer/cargo_toml.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module BlobViewer + class CargoToml < DependencyManager + include Static + + self.file_types = %i(cargo_toml) + + def manager_name + 'Cargo' + end + + def manager_url + 'https://crates.io/' + end + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 7e7c580a48e..e3f0e07bb8f 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -717,8 +717,8 @@ module Ci end end - def has_expiring_artifacts? - artifacts_expire_at.present? && artifacts_expire_at > Time.now + def has_expiring_archive_artifacts? + has_expiring_artifacts? && artifacts_file&.exists? end def keep_artifacts! @@ -933,6 +933,10 @@ module Ci value.with_indifferent_access end end + + def has_expiring_artifacts? + artifacts_expire_at.present? && artifacts_expire_at > Time.now + end end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 686d06d3ee0..939d8bc4bef 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -23,6 +23,12 @@ class DiffNote < Note before_validation :set_line_code, if: :on_text?, unless: :importing? after_save :keep_around_commits, unless: :importing? + + NoteDiffFileCreationError = Class.new(StandardError) + + DIFF_LINE_NOT_FOUND_MESSAGE = "Failed to find diff line for: %{file_path}, old_line: %{old_line}, new_line: %{new_line}" + DIFF_FILE_NOT_FOUND_MESSAGE = "Failed to find diff file" + after_commit :create_diff_file, on: :create def discussion_class(*) @@ -33,7 +39,16 @@ class DiffNote < Note return unless should_create_diff_file? diff_file = fetch_diff_file + raise NoteDiffFileCreationError, DIFF_FILE_NOT_FOUND_MESSAGE unless diff_file + diff_line = diff_file.line_for_position(self.original_position) + unless diff_line + raise NoteDiffFileCreationError, DIFF_LINE_NOT_FOUND_MESSAGE % { + file_path: diff_file.file_path, + old_line: original_position.old_line, + new_line: original_position.new_line + } + end creation_params = diff_file.diff.to_hash .except(:too_large) @@ -110,19 +125,20 @@ class DiffNote < Note def fetch_diff_file return note_diff_file.raw_diff_file if note_diff_file - file = - if created_at_diff?(noteable.diff_refs) - # We're able to use the already persisted diffs (Postgres) if we're - # presenting a "current version" of the MR discussion diff. - # So no need to make an extra Gitaly diff request for it. - # As an extra benefit, the returned `diff_file` already - # has `highlighted_diff_lines` data set from Redis on - # `Diff::FileCollection::MergeRequestDiff`. - noteable.diffs(original_position.diff_options).diff_files.first - else - original_position.diff_file(repository) - end + if created_at_diff?(noteable.diff_refs) + # We're able to use the already persisted diffs (Postgres) if we're + # presenting a "current version" of the MR discussion diff. + # So no need to make an extra Gitaly diff request for it. + # As an extra benefit, the returned `diff_file` already + # has `highlighted_diff_lines` data set from Redis on + # `Diff::FileCollection::MergeRequestDiff`. + file = noteable.diffs(original_position.diff_options).diff_files.first + # if line is not found in persisted diffs, fallback and retrieve file from repository using gitaly + # This is required because of https://gitlab.com/gitlab-org/gitlab/issues/42676 + file = nil if file&.line_for_position(original_position).nil? && importing? + end + file ||= original_position.diff_file(repository) file&.unfold_diff_lines(position) file diff --git a/app/serializers/build_artifact_entity.rb b/app/serializers/build_artifact_entity.rb index 414f436e76e..c173c155edf 100644 --- a/app/serializers/build_artifact_entity.rb +++ b/app/serializers/build_artifact_entity.rb @@ -14,7 +14,7 @@ class BuildArtifactEntity < Grape::Entity download_project_job_artifacts_path(project, job) end - expose :keep_path, if: -> (*) { job.has_expiring_artifacts? } do |job| + expose :keep_path, if: -> (*) { job.has_expiring_archive_artifacts? } do |job| keep_project_job_artifacts_path(project, job) end diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 480a8cab6ff..0ef71ff8af5 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -31,7 +31,7 @@ class BuildDetailsEntity < JobEntity browse_project_job_artifacts_path(project, build) end - expose :keep_path, if: -> (*) { build.has_expiring_artifacts? && can?(current_user, :update_build, build) } do |build| + expose :keep_path, if: -> (*) { build.has_expiring_archive_artifacts? && can?(current_user, :update_build, build) } do |build| keep_project_job_artifacts_path(project, build) end diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml index 084d295f2c1..128508e954e 100644 --- a/app/views/shared/_auto_devops_callout.html.haml +++ b/app/views/shared/_auto_devops_callout.html.haml @@ -1,16 +1,15 @@ -.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20.prepend-top-10{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } - .banner-graphic - = custom_icon('icon_autodevops') +%section.js-autodevops-banner.gl-banner{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } + .gl-banner-illustration + = image_tag('illustrations/autodevops.svg') - .banner-body.prepend-left-10.append-bottom-10 - %h5.banner-title= s_('AutoDevOps|Auto DevOps') + .gl-banner-content + %h1.gl-banner-title= s_('AutoDevOps|Auto DevOps') %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') %p - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } - .banner-buttons - = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn js-close-callout' + = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn btn-md new-gl-button js-close-callout' - %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', + %button.gl-banner-close.close.js-close-callout{ type: 'button', 'aria-label' => 'Dismiss Auto DevOps box' } = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true') diff --git a/changelogs/unreleased/33892-add-conan-recipe-to-package-details.yml b/changelogs/unreleased/33892-add-conan-recipe-to-package-details.yml new file mode 100644 index 00000000000..a3079f1c902 --- /dev/null +++ b/changelogs/unreleased/33892-add-conan-recipe-to-package-details.yml @@ -0,0 +1,5 @@ +--- +title: Added Conan recipe in place of the package name on the package details page. +merge_request: 21247 +author: +type: changed diff --git a/changelogs/unreleased/35527-add-created-at-to-package.yml b/changelogs/unreleased/35527-add-created-at-to-package.yml new file mode 100644 index 00000000000..fa127f8b4b2 --- /dev/null +++ b/changelogs/unreleased/35527-add-created-at-to-package.yml @@ -0,0 +1,5 @@ +--- +title: Adds created_at object to package api response +merge_request: 20816 +author: +type: added diff --git a/changelogs/unreleased/36410-error-list-mobile.yml b/changelogs/unreleased/36410-error-list-mobile.yml new file mode 100644 index 00000000000..759fe4ed76d --- /dev/null +++ b/changelogs/unreleased/36410-error-list-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Improve error list UI on mobile viewports +merge_request: 21192 +author: +type: added diff --git a/changelogs/unreleased/37682-fix-diff-file-creation.yml b/changelogs/unreleased/37682-fix-diff-file-creation.yml new file mode 100644 index 00000000000..fb9761dec98 --- /dev/null +++ b/changelogs/unreleased/37682-fix-diff-file-creation.yml @@ -0,0 +1,5 @@ +--- +title: Add fallbacks and proper errors for diff file creation +merge_request: 21034 +author: +type: fixed diff --git a/changelogs/unreleased/dont_run_auto_devops.yml b/changelogs/unreleased/dont_run_auto_devops.yml new file mode 100644 index 00000000000..eb460e0538b --- /dev/null +++ b/changelogs/unreleased/dont_run_auto_devops.yml @@ -0,0 +1,5 @@ +--- +title: Don't run Auto DevOps when no dockerfile or matching buildpack exists +merge_request: 20267 +author: +type: changed diff --git a/changelogs/unreleased/eb-no-keep-report-artifacts.yml b/changelogs/unreleased/eb-no-keep-report-artifacts.yml new file mode 100644 index 00000000000..c80fbd2c221 --- /dev/null +++ b/changelogs/unreleased/eb-no-keep-report-artifacts.yml @@ -0,0 +1,5 @@ +--- +title: Remove keep button for non archive artifacts +merge_request: 21553 +author: +type: fixed diff --git a/changelogs/unreleased/feat-rust-cargo-toml-blob-view.yml b/changelogs/unreleased/feat-rust-cargo-toml-blob-view.yml new file mode 100644 index 00000000000..ccc5ea0fe7e --- /dev/null +++ b/changelogs/unreleased/feat-rust-cargo-toml-blob-view.yml @@ -0,0 +1,5 @@ +--- +title: Add support for Rust Cargo.toml dependency vizualisation and linking +merge_request: 21374 +author: Fabio Huser +type: added diff --git a/config/webpack.config.js b/config/webpack.config.js index d85fa84c32f..7da7e571d67 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -201,7 +201,7 @@ module.exports = { loader: 'raw-loader', }, { - test: /\.(gif|png)$/, + test: /\.(gif|png|mp4)$/, loader: 'url-loader', options: { limit: 2048 }, }, diff --git a/doc/api/packages.md b/doc/api/packages.md index 5b490b872da..afca7db97c8 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -31,13 +31,15 @@ Example response: "id": 1, "name": "com/mycompany/my-app", "version": "1.0-SNAPSHOT", - "package_type": "maven" + "package_type": "maven", + "created_at": "2019-11-27T03:37:38.711Z" }, { "id": 2, "name": "@foo/bar", "version": "1.0.3", - "package_type": "npm" + "package_type": "npm", + "created_at": "2019-11-27T03:37:38.711Z" } ] ``` @@ -76,7 +78,8 @@ Example response: "_links": { "web_path": "/namespace1/project1/-/packages/1", "delete_api_path": "/namespace1/project1/-/packages/1" - } + }, + "created_at": "2019-11-27T03:37:38.711Z" }, { "id": 2, @@ -86,7 +89,8 @@ Example response: "_links": { "web_path": "/namespace1/project1/-/packages/1", "delete_api_path": "/namespace1/project1/-/packages/1" - } + }, + "created_at": "2019-11-27T03:37:38.711Z" } ] ``` @@ -128,7 +132,8 @@ Example response: "_links": { "web_path": "/namespace1/project1/-/packages/1", "delete_api_path": "/namespace1/project1/-/packages/1" - } + }, + "created_at": "2019-11-27T03:37:38.711Z" } ``` diff --git a/doc/ci/README.md b/doc/ci/README.md index d1cf7e63c63..8a33298ea63 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -127,6 +127,7 @@ Its feature set is listed on the table below according to DevOps stages. | [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. | | [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. | | [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. | +| [Cloud deployment](cloud_deployment/index.md) | Deploy your application to a main cloud provider. | |---+---| | **Secure** || | [Container Scanning](../user/application_security/container_scanning/index.md) **(ULTIMATE)** | Check your Docker containers for known vulnerabilities.| diff --git a/doc/ci/cloud_deployment/index.md b/doc/ci/cloud_deployment/index.md new file mode 100644 index 00000000000..f7dfe37da7a --- /dev/null +++ b/doc/ci/cloud_deployment/index.md @@ -0,0 +1,46 @@ +--- +type: howto +--- + +# Cloud deployment + +Interacting with a major cloud provider such as Amazon AWS may have become a much needed task that's +part of your delivery process. GitLab is making this process less painful by providing Docker images +that come with the needed libraries and tools pre-installed. +By referencing them in your CI/CD pipeline, you'll be able to interact with your chosen +cloud provider more easily. + +## AWS + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31167) in GitLab 12.6. + +GitLab's AWS Docker image provides the [AWS Command Line Interface](https://aws.amazon.com/cli/), +which enables you to run `aws` commands. As part of your deployment strategy, you can run `aws` commands directly from +`.gitlab-ci.yml` by specifying GitLab's AWS Docker image. + +Some credentials are required to be able to run `aws` commands: + +1. Sign up for [an AWS account](https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-set-up.html) if you don't have one yet. +1. Log in onto the console and create [a new IAM user](https://console.aws.amazon.com/iam/home#/home). +1. Select your newly created user to access its details. Navigate to **Security credentials > Create a new access key**. + + NOTE: **Note:** + A new **Access key ID** and **Secret access key** pair will be generated. Please take a note of them right away. + +1. In your GitLab project, go to **Settings > CI / CD**. Set the Access key ID and Secret access key as [environment variables](../variables/README.md#gitlab-cicd-environment-variables), using the following variable names: + + | Env. variable name | Value | + |:------------------------|:-------------------------| + | `AWS_ACCESS_KEY_ID` | Your "Access key ID" | + | `AWS_SECRET_ACCESS_KEY` | Your "Secret access key" | + +1. You can now use `aws` commands in the `.gitlab-ci.yml` file of this project: + + ```yml + deploy: + stage: deploy + image: registry.gitlab.com/gitlab-org/cloud-deploy:latest + script: + - aws s3 ... + - aws create-deployment ... + ``` diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md index 35cb2b42c56..ccf20d797aa 100644 --- a/doc/user/admin_area/index.md +++ b/doc/user/admin_area/index.md @@ -18,22 +18,23 @@ Only admin users can access the Admin Area. The Admin Area is made up of the following sections: -| Section | Description | -|:------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [Overview](#overview-section) | View your GitLab [Dashboard](#admin-dashboard), and administer [projects](#administering-projects), [users](#administering-users), [groups](#administering-groups), [jobs](#administering-jobs), [Runners](#administering-runners), and [Gitaly servers](#administering-gitaly-servers). | -| Monitoring | View GitLab [system information](#system-info), and information on [background jobs](#background-jobs), [logs](#logs), [health checks](monitoring/health_check.md), [requests profiles](#requests-profiles), and [audit logs](#audit-log-premium-only). | -| Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. | -| System Hooks | Configure [system hooks](../../system_hooks/system_hooks.md) for many events. | -| Applications | Create system [OAuth applications](../../integration/oauth_provider.md) for integrations with other services. | -| Abuse Reports | Manage [abuse reports](abuse_reports.md) submitted by your users. | -| License **(STARTER ONLY)** | Upload, display, and remove [licenses](license.md). | -| Push Rules **(STARTER)** | Configure pre-defined Git [push rules](../../push_rules/push_rules.md) for projects. | -| Geo **(PREMIUM ONLY)** | Configure and maintain [Geo nodes](geo_nodes.md). | -| Deploy Keys | Create instance-wide [SSH deploy keys](../../ssh/README.md#deploy-keys). | -| Service Templates | Create [service templates](../project/integrations/services_templates.md) for projects. | -| Labels | Create and maintain [labels](labels.md) for your GitLab instance. | -| Appearance | Customize [GitLab's appearance](appearance.md). | -| Settings | Modify the [settings](settings/index.md) for your GitLab instance. | +| Section | Description | +|:--------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Overview](#overview-section) | View your GitLab [Dashboard](#admin-dashboard), and administer [projects](#administering-projects), [users](#administering-users), [groups](#administering-groups), [jobs](#administering-jobs), [Runners](#administering-runners), and [Gitaly servers](#administering-gitaly-servers). | +| Monitoring | View GitLab [system information](#system-info), and information on [background jobs](#background-jobs), [logs](#logs), [health checks](monitoring/health_check.md), [requests profiles](#requests-profiles), and [audit logs](#audit-log-premium-only). | +| Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. | +| System Hooks | Configure [system hooks](../../system_hooks/system_hooks.md) for many events. | +| Applications | Create system [OAuth applications](../../integration/oauth_provider.md) for integrations with other services. | +| Abuse Reports | Manage [abuse reports](abuse_reports.md) submitted by your users. | +| License **(STARTER ONLY)** | Upload, display, and remove [licenses](license.md). | +| Push Rules **(STARTER)** | Configure pre-defined Git [push rules](../../push_rules/push_rules.md) for projects. | +| Geo **(PREMIUM ONLY)** | Configure and maintain [Geo nodes](geo_nodes.md). | +| Deploy Keys | Create instance-wide [SSH deploy keys](../../ssh/README.md#deploy-keys). | +| Credentials **(ULTIMATE ONLY)** | View [credentials](credentials_inventory.md) that can be used to access your instance. | +| Service Templates | Create [service templates](../project/integrations/services_templates.md) for projects. | +| Labels | Create and maintain [labels](labels.md) for your GitLab instance. | +| Appearance | Customize [GitLab's appearance](appearance.md). | +| Settings | Modify the [settings](settings/index.md) for your GitLab instance. | ## Admin Dashboard diff --git a/doc/user/project/clusters/add_remove_clusters.md b/doc/user/project/clusters/add_remove_clusters.md index 6a0377f118d..270950ea44c 100644 --- a/doc/user/project/clusters/add_remove_clusters.md +++ b/doc/user/project/clusters/add_remove_clusters.md @@ -728,7 +728,11 @@ When removing the cluster integration, note: - You need Maintainer [permissions](../../permissions.md) and above to remove a Kubernetes cluster integration. - When you remove a cluster, you only remove its relationship to GitLab, not the cluster itself. To - remove the cluster, you can do so by visiting the GKE dashboard or using `kubectl`. + remove the cluster, you can do so by visiting the GKE or EKS dashboard, or using `kubectl`. + +[From GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/issues/26815), you can also remove all +related GitLab cluster resources (for example, namespaces, roles, and bindings) when removing the +integration. ## Learn more diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index c069882e38f..218bab26142 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -27,7 +27,7 @@ From the image above, we can deduce the following things: - Administrator is the Owner and member of **all** groups and for that reason, there is an indication of an ancestor group and inherited Owner permissions. -[From](https://gitlab.com/gitlab-org/gitlab/issues/21727), you can filter this list +[From GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/issues/21727), you can filter this list using dropdown on the right side: ![Project members filter](img/project_members_filter_v12_6.png) diff --git a/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb index e9bcc67de9c..54be789988c 100644 --- a/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb +++ b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb @@ -11,7 +11,7 @@ module Gitlab strong_memoize(:content) do next unless project&.auto_devops_enabled? - template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') + template = Gitlab::Template::GitlabCiYmlTemplate.find(template_name) YAML.dump('include' => [{ 'template' => template.full_name }]) end end @@ -19,6 +19,22 @@ module Gitlab def source :auto_devops_source end + + private + + def template_name + if beta_enabled? + 'Beta/Auto-DevOps' + else + 'Auto-DevOps' + end + end + + def beta_enabled? + Feature.enabled?(:auto_devops_beta, project, default_enabled: true) && + # workflow:rules are required by `Beta/Auto-DevOps.gitlab-ci.yml` + Feature.enabled?(:workflow_rules, project, default_enabled: true) + end end end end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb index c4cef356628..b282886a56f 100644 --- a/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb +++ b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb @@ -11,7 +11,7 @@ module Gitlab strong_memoize(:content) do next unless project&.auto_devops_enabled? - template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') + template = Gitlab::Template::GitlabCiYmlTemplate.find(template_name) template.content end end @@ -19,6 +19,22 @@ module Gitlab def source :auto_devops_source end + + private + + def template_name + if beta_enabled? + 'Beta/Auto-DevOps' + else + 'Auto-DevOps' + end + end + + def beta_enabled? + Feature.enabled?(:auto_devops_beta, project, default_enabled: true) && + # workflow:rules are required by `Beta/Auto-DevOps.gitlab-ci.yml` + Feature.enabled?(:workflow_rules, project, default_enabled: true) + end end end end diff --git a/lib/gitlab/ci/templates/Beta/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Beta/Auto-DevOps.gitlab-ci.yml new file mode 100644 index 00000000000..2c5035705ac --- /dev/null +++ b/lib/gitlab/ci/templates/Beta/Auto-DevOps.gitlab-ci.yml @@ -0,0 +1,163 @@ +# Auto DevOps - BETA do not use +# This CI/CD configuration provides a standard pipeline for +# * building a Docker image (using a buildpack if necessary), +# * storing the image in the container registry, +# * running tests from a buildpack, +# * running code quality analysis, +# * creating a review app for each topic branch, +# * and continuous deployment to production +# +# Test jobs may be disabled by setting environment variables: +# * test: TEST_DISABLED +# * code_quality: CODE_QUALITY_DISABLED +# * license_management: LICENSE_MANAGEMENT_DISABLED +# * performance: PERFORMANCE_DISABLED +# * sast: SAST_DISABLED +# * dependency_scanning: DEPENDENCY_SCANNING_DISABLED +# * container_scanning: CONTAINER_SCANNING_DISABLED +# * dast: DAST_DISABLED +# * review: REVIEW_DISABLED +# * stop_review: REVIEW_DISABLED +# +# In order to deploy, you must have a Kubernetes cluster configured either +# via a project integration, or via group/project variables. +# KUBE_INGRESS_BASE_DOMAIN must also be set on the cluster settings, +# as a variable at the group or project level, or manually added below. +# +# Continuous deployment to production is enabled by default. +# If you want to deploy to staging first, set STAGING_ENABLED environment variable. +# If you want to enable incremental rollout, either manual or time based, +# set INCREMENTAL_ROLLOUT_MODE environment variable to "manual" or "timed". +# If you want to use canary deployments, set CANARY_ENABLED environment variable. +# +# If Auto DevOps fails to detect the proper buildpack, or if you want to +# specify a custom buildpack, set a project variable `BUILDPACK_URL` to the +# repository URL of the buildpack. +# e.g. BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-ruby.git#v142 +# If you need multiple buildpacks, add a file to your project called +# `.buildpacks` that contains the URLs, one on each line, in order. +# Note: Auto CI does not work with multiple buildpacks yet + +image: alpine:latest + +variables: + # KUBE_INGRESS_BASE_DOMAIN is the application deployment domain and should be set as a variable at the group or project level. + # KUBE_INGRESS_BASE_DOMAIN: domain.example.com + + POSTGRES_USER: user + POSTGRES_PASSWORD: testing-password + POSTGRES_ENABLED: "true" + POSTGRES_DB: $CI_ENVIRONMENT_SLUG + POSTGRES_VERSION: 9.6.2 + + DOCKER_DRIVER: overlay2 + + ROLLOUT_RESOURCE_TYPE: deployment + + DOCKER_TLS_CERTDIR: "" # https://gitlab.com/gitlab-org/gitlab-runner/issues/4501 + +stages: + - build + - test + - deploy # dummy stage to follow the template guidelines + - review + - dast + - staging + - canary + - production + - incremental rollout 10% + - incremental rollout 25% + - incremental rollout 50% + - incremental rollout 100% + - performance + - cleanup + +workflow: + rules: + - if: '$BUILDPACK_URL || $AUTO_DEVOPS_EXPLICITLY_ENABLED == "1"' + + - exists: + - Dockerfile + + # https://github.com/heroku/heroku-buildpack-clojure + - exists: + - project.clj + + # https://github.com/heroku/heroku-buildpack-go + - exists: + - go.mod + - Gopkg.mod + - Godeps/Godeps.json + - vendor/vendor.json + - glide.yaml + - src/**/*.go + + # https://github.com/heroku/heroku-buildpack-gradle + - exists: + - gradlew + - build.gradle + - settings.gradle + + # https://github.com/heroku/heroku-buildpack-java + - exists: + - pom.xml + - pom.atom + - pom.clj + - pom.groovy + - pom.rb + - pom.scala + - pom.yaml + - pom.yml + + # https://github.com/heroku/heroku-buildpack-multi + - exists: + - .buildpacks + + # https://github.com/heroku/heroku-buildpack-nodejs + - exists: + - package.json + + # https://github.com/heroku/heroku-buildpack-php + - exists: + - composer.json + - index.php + + # https://github.com/heroku/heroku-buildpack-play + # TODO: detect script excludes some scala files + - exists: + - '**/conf/application.conf' + + # https://github.com/heroku/heroku-buildpack-python + # TODO: detect script checks that all of these exist, not any + - exists: + - requirements.txt + - setup.py + - Pipfile + + # https://github.com/heroku/heroku-buildpack-ruby + - exists: + - Gemfile + + # https://github.com/heroku/heroku-buildpack-scala + - exists: + - '*.sbt' + - project/*.scala + - .sbt/*.scala + - project/build.properties + + # https://github.com/dokku/buildpack-nginx + - exists: + - .static + +include: + - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml + - template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml + - template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml + - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml + - template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml + - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml + - template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml + - template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml + - template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml + - template: Security/License-Management.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml + - template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb index c63d9e5bb71..7af380689d5 100644 --- a/lib/gitlab/dependency_linker.rb +++ b/lib/gitlab/dependency_linker.rb @@ -12,7 +12,8 @@ module Gitlab PodspecJsonLinker, CartfileLinker, GodepsJsonLinker, - RequirementsTxtLinker + RequirementsTxtLinker, + CargoTomlLinker ].freeze def self.linker(blob_name) diff --git a/lib/gitlab/dependency_linker/cargo_toml_linker.rb b/lib/gitlab/dependency_linker/cargo_toml_linker.rb new file mode 100644 index 00000000000..57e0a5f4699 --- /dev/null +++ b/lib/gitlab/dependency_linker/cargo_toml_linker.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module DependencyLinker + class CargoTomlLinker < BaseLinker + self.file_type = :cargo_toml + + def link + return highlighted_text unless toml + + super + end + + private + + def link_dependencies + link_dependencies_at("dependencies") + link_dependencies_at("dev-dependencies") + link_dependencies_at("build-dependencies") + end + + def link_dependencies_at(type) + dependencies = toml[type] + return unless dependencies + + dependencies.each do |name, value| + link_toml(name, value, type) do |name| + "https://crates.io/crates/#{name}" + end + end + end + + def link_toml(key, value, type, &url_proc) + if value.is_a? String + link_regex(/^(?<name>#{key})\s*=\s*"#{value}"/, &url_proc) + else + link_regex(/^\[#{type}\.(?<name>#{key})]/, &url_proc) + end + end + + def toml + @toml ||= TomlRB.parse(plain_text) rescue nil + end + end + end +end diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb index a386c21983d..234c834a83a 100644 --- a/lib/gitlab/file_detector.rb +++ b/lib/gitlab/file_detector.rb @@ -25,6 +25,7 @@ module Gitlab route_map: '.gitlab/route-map.yml', # Dependency files + cargo_toml: 'Cargo.toml', cartfile: %r{\ACartfile[^/]*\z}, composer_json: 'composer.json', gemfile: /\A(Gemfile|gems\.rb)\z/, diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb index 3df688a1fda..dd589b6194d 100644 --- a/lib/sentry/client.rb +++ b/lib/sentry/client.rb @@ -3,6 +3,7 @@ module Sentry class Client include Sentry::Client::Projects + include Sentry::Client::Issue Error = Class.new(StandardError) MissingKeysError = Class.new(StandardError) @@ -23,12 +24,6 @@ module Sentry @token = token end - def issue_details(issue_id:) - issue = get_issue(issue_id: issue_id) - - map_to_detailed_error(issue) - end - def issue_latest_event(issue_id:) latest_event = get_issue_latest_event(issue_id: issue_id) @@ -107,10 +102,6 @@ module Sentry }.compact end - def get_issue(issue_id:) - http_get(issue_api_url(issue_id))[:body] - end - def get_issue_latest_event(issue_id:) http_get(issue_latest_event_api_url(issue_id))[:body] end @@ -145,13 +136,6 @@ module Sentry raise Client::Error, message end - def issue_api_url(issue_id) - issue_url = URI(@url) - issue_url.path = "/api/0/issues/#{issue_id}/" - - issue_url - end - def issue_latest_event_api_url(issue_id) latest_event_url = URI(@url) latest_event_url.path = "/api/0/issues/#{issue_id}/events/latest/" @@ -212,42 +196,6 @@ module Sentry stack_trace_entry.dig('stacktrace', 'frames') end - def parse_gitlab_issue(plugin_issues) - return unless plugin_issues - - gitlab_plugin = plugin_issues.detect { |item| item['id'] == 'gitlab' } - return unless gitlab_plugin - - gitlab_plugin.dig('issue', 'url') - end - - def map_to_detailed_error(issue) - Gitlab::ErrorTracking::DetailedError.new( - id: issue.fetch('id'), - first_seen: issue.fetch('firstSeen', nil), - last_seen: issue.fetch('lastSeen', nil), - title: issue.fetch('title', nil), - type: issue.fetch('type', nil), - user_count: issue.fetch('userCount', nil), - count: issue.fetch('count', nil), - message: issue.dig('metadata', 'value'), - culprit: issue.fetch('culprit', nil), - external_url: issue_url(issue.fetch('id')), - external_base_url: project_url, - short_id: issue.fetch('shortId', nil), - status: issue.fetch('status', nil), - frequency: issue.dig('stats', '24h'), - project_id: issue.dig('project', 'id'), - project_name: issue.dig('project', 'name'), - project_slug: issue.dig('project', 'slug'), - gitlab_issue: parse_gitlab_issue(issue.fetch('pluginIssues', nil)), - first_release_last_commit: issue.dig('firstRelease', 'lastCommit'), - last_release_last_commit: issue.dig('lastRelease', 'lastCommit'), - first_release_short_version: issue.dig('firstRelease', 'shortVersion'), - last_release_short_version: issue.dig('lastRelease', 'shortVersion') - ) - end - def map_to_error(issue) Gitlab::ErrorTracking::Error.new( id: issue.fetch('id'), diff --git a/lib/sentry/client/issue.rb b/lib/sentry/client/issue.rb new file mode 100644 index 00000000000..08ed5392a11 --- /dev/null +++ b/lib/sentry/client/issue.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Sentry + class Client + module Issue + def issue_details(issue_id:) + issue = get_issue(issue_id: issue_id) + + map_to_detailed_error(issue) + end + + private + + def get_issue(issue_id:) + http_get(issue_api_url(issue_id))[:body] + end + + def issue_api_url(issue_id) + issue_url = URI(url) + issue_url.path = "/api/0/issues/#{CGI.escape(issue_id.to_s)}/" + + issue_url + end + + def parse_gitlab_issue(plugin_issues) + return unless plugin_issues + + gitlab_plugin = plugin_issues.detect { |item| item['id'] == 'gitlab' } + return unless gitlab_plugin + + gitlab_plugin.dig('issue', 'url') + end + + def map_to_detailed_error(issue) + Gitlab::ErrorTracking::DetailedError.new( + id: issue.fetch('id'), + first_seen: issue.fetch('firstSeen', nil), + last_seen: issue.fetch('lastSeen', nil), + title: issue.fetch('title', nil), + type: issue.fetch('type', nil), + user_count: issue.fetch('userCount', nil), + count: issue.fetch('count', nil), + message: issue.dig('metadata', 'value'), + culprit: issue.fetch('culprit', nil), + external_url: issue_url(issue.fetch('id')), + external_base_url: project_url, + short_id: issue.fetch('shortId', nil), + status: issue.fetch('status', nil), + frequency: issue.dig('stats', '24h'), + project_id: issue.dig('project', 'id'), + project_name: issue.dig('project', 'name'), + project_slug: issue.dig('project', 'slug'), + gitlab_issue: parse_gitlab_issue(issue.fetch('pluginIssues', nil)), + first_release_last_commit: issue.dig('firstRelease', 'lastCommit'), + last_release_last_commit: issue.dig('lastRelease', 'lastCommit'), + first_release_short_version: issue.dig('firstRelease', 'shortVersion'), + last_release_short_version: issue.dig('lastRelease', 'shortVersion') + ) + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index dee48529f60..3dbf9478be9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7795,6 +7795,9 @@ msgstr "" msgid "Filter by milestone name" msgstr "" +msgid "Filter by name..." +msgstr "" + msgid "Filter by two-factor authentication" msgstr "" @@ -11548,6 +11551,9 @@ msgstr "" msgid "More actions" msgstr "" +msgid "More details" +msgstr "" + msgid "More info" msgstr "" @@ -14726,6 +14732,9 @@ msgstr "" msgid "Recent searches" msgstr "" +msgid "Recipe" +msgstr "" + msgid "Recovery Codes" msgstr "" diff --git a/spec/fixtures/sentry/issue_sample_response.json b/spec/fixtures/sentry/issue_sample_response.json new file mode 100644 index 00000000000..a320a21de34 --- /dev/null +++ b/spec/fixtures/sentry/issue_sample_response.json @@ -0,0 +1,311 @@ +{ + "activity": [ + { + "data": {}, + "dateCreated": "2018-11-06T21:19:55Z", + "id": "0", + "type": "first_seen", + "user": null + } + ], + "annotations": [], + "assignedTo": null, + "count": "1", + "culprit": "raven.scripts.runner in main", + "firstRelease": { + "authors": [], + "commitCount": 0, + "data": {}, + "dateCreated": "2018-11-06T21:19:55.146Z", + "dateReleased": null, + "deployCount": 0, + "firstEvent": "2018-11-06T21:19:55.271Z", + "lastCommit": null, + "lastDeploy": null, + "lastEvent": "2018-11-06T21:19:55.271Z", + "newGroups": 0, + "owner": null, + "projects": [ + { + "name": "Pump Station", + "slug": "pump-station" + } + ], + "ref": null, + "shortVersion": "1764232", + "url": null, + "version": "17642328ead24b51867165985996d04b29310337" + }, + "firstSeen": "2018-11-06T21:19:55Z", + "hasSeen": false, + "id": "503504", + "isBookmarked": false, + "isPublic": false, + "isSubscribed": true, + "lastRelease": null, + "lastSeen": "2018-11-06T21:19:55Z", + "level": "error", + "logger": null, + "metadata": { + "title": "This is an example Python exception" + }, + "numComments": 0, + "participants": [], + "permalink": "https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/503504/", + "pluginActions": [], + "pluginContexts": [], + "pluginIssues": [ + { + "id": "gitlab", + "issue": { + "url": "https://gitlab.com/gitlab-org/gitlab/issues/1" + } + } + ], + "project": { + "id": "2", + "name": "Pump Station", + "slug": "pump-station" + }, + "seenBy": [], + "shareId": null, + "shortId": "PUMP-STATION-1", + "stats": { + "24h": [ + [ + 1541451600.0, + 557 + ], + [ + 1541455200.0, + 473 + ], + [ + 1541458800.0, + 914 + ], + [ + 1541462400.0, + 991 + ], + [ + 1541466000.0, + 925 + ], + [ + 1541469600.0, + 881 + ], + [ + 1541473200.0, + 182 + ], + [ + 1541476800.0, + 490 + ], + [ + 1541480400.0, + 820 + ], + [ + 1541484000.0, + 322 + ], + [ + 1541487600.0, + 836 + ], + [ + 1541491200.0, + 565 + ], + [ + 1541494800.0, + 758 + ], + [ + 1541498400.0, + 880 + ], + [ + 1541502000.0, + 677 + ], + [ + 1541505600.0, + 381 + ], + [ + 1541509200.0, + 814 + ], + [ + 1541512800.0, + 329 + ], + [ + 1541516400.0, + 446 + ], + [ + 1541520000.0, + 731 + ], + [ + 1541523600.0, + 111 + ], + [ + 1541527200.0, + 926 + ], + [ + 1541530800.0, + 772 + ], + [ + 1541534400.0, + 400 + ], + [ + 1541538000.0, + 943 + ] + ], + "30d": [ + [ + 1538870400.0, + 565 + ], + [ + 1538956800.0, + 12862 + ], + [ + 1539043200.0, + 15617 + ], + [ + 1539129600.0, + 10809 + ], + [ + 1539216000.0, + 15065 + ], + [ + 1539302400.0, + 12927 + ], + [ + 1539388800.0, + 12994 + ], + [ + 1539475200.0, + 13139 + ], + [ + 1539561600.0, + 11838 + ], + [ + 1539648000.0, + 12088 + ], + [ + 1539734400.0, + 12338 + ], + [ + 1539820800.0, + 12768 + ], + [ + 1539907200.0, + 12816 + ], + [ + 1539993600.0, + 15356 + ], + [ + 1540080000.0, + 10910 + ], + [ + 1540166400.0, + 12306 + ], + [ + 1540252800.0, + 12912 + ], + [ + 1540339200.0, + 14700 + ], + [ + 1540425600.0, + 11890 + ], + [ + 1540512000.0, + 11684 + ], + [ + 1540598400.0, + 13510 + ], + [ + 1540684800.0, + 12625 + ], + [ + 1540771200.0, + 12811 + ], + [ + 1540857600.0, + 13180 + ], + [ + 1540944000.0, + 14651 + ], + [ + 1541030400.0, + 14161 + ], + [ + 1541116800.0, + 12612 + ], + [ + 1541203200.0, + 14316 + ], + [ + 1541289600.0, + 14742 + ], + [ + 1541376000.0, + 12505 + ], + [ + 1541462400.0, + 14180 + ] + ] + }, + "status": "unresolved", + "statusDetails": {}, + "subscriptionDetails": null, + "tags": [], + "title": "This is an example Python exception", + "type": "default", + "userCount": 0, + "userReportCount": 0 +} diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index 581581405b6..d0893c0de01 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -272,6 +272,7 @@ describe('ErrorTrackingList', () => { describe('When pagination is not required', () => { beforeEach(() => { + store.state.list.loading = false; store.state.list.pagination = {}; mountComponent(); }); @@ -284,6 +285,7 @@ describe('ErrorTrackingList', () => { describe('When pagination is required', () => { describe('and the user is on the first page', () => { beforeEach(() => { + store.state.list.loading = false; mountComponent({ sync: false }); }); @@ -295,6 +297,7 @@ describe('ErrorTrackingList', () => { describe('and the user is not on the first page', () => { describe('and the previous button is clicked', () => { beforeEach(() => { + store.state.list.loading = false; mountComponent({ sync: false }); wrapper.setData({ pageValue: 2 }); }); @@ -313,6 +316,7 @@ describe('ErrorTrackingList', () => { describe('and the next page button is clicked', () => { beforeEach(() => { + store.state.list.loading = false; mountComponent({ sync: false }); }); diff --git a/spec/frontend/fixtures/static/mock-video.mp4 b/spec/frontend/fixtures/static/mock-video.mp4 Binary files differnew file mode 100644 index 00000000000..1fc478842f5 --- /dev/null +++ b/spec/frontend/fixtures/static/mock-video.mp4 diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index 4cd0f62da0f..2569f2f02a7 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -1,9 +1,9 @@ import { mount, createLocalVue } from '@vue/test-utils'; -import { TEST_HOST } from 'spec/test_constants'; +import fieldComponent from '~/vue_shared/components/markdown/field.vue'; +import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants'; import AxiosMockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; -import fieldComponent from '~/vue_shared/components/markdown/field.vue'; const markdownPreviewPath = `${TEST_HOST}/preview`; const markdownDocsPath = `${TEST_HOST}/docs`; @@ -19,6 +19,7 @@ function createComponent() { propsData: { markdownDocsPath, markdownPreviewPath, + isSubmitting: false, }, slots: { textarea: '<textarea>testing\n123</textarea>', @@ -27,6 +28,7 @@ function createComponent() { <field-component markdown-preview-path="${markdownPreviewPath}" markdown-docs-path="${markdownDocsPath}" + :isSubmitting="false" > <textarea slot="textarea" @@ -44,6 +46,7 @@ const getPreviewLink = wrapper => wrapper.find('.nav-links .js-preview-link'); const getWriteLink = wrapper => wrapper.find('.nav-links .js-write-link'); const getMarkdownButton = wrapper => wrapper.find('.js-md'); const getAllMarkdownButtons = wrapper => wrapper.findAll('.js-md'); +const getVideo = wrapper => wrapper.find('video'); describe('Markdown field component', () => { let axiosMock; @@ -59,7 +62,10 @@ describe('Markdown field component', () => { describe('mounted', () => { let wrapper; - const previewHTML = '<p>markdown preview</p>'; + const previewHTML = ` + <p>markdown preview</p> + <video src="${FIXTURES_PATH}/static/mock-video.mp4" muted="muted"></video> + `; let previewLink; let writeLink; @@ -112,9 +118,35 @@ describe('Markdown field component', () => { previewLink.trigger('click'); - setTimeout(() => { - expect($.fn.renderGFM).toHaveBeenCalled(); - }, 0); + return axios.waitFor(markdownPreviewPath).then(() => { + expect(wrapper.find('.md-preview-holder').element.innerHTML).toContain(previewHTML); + }); + }); + + it('calls video.pause() on comment input when isSubmitting is changed to true', () => { + wrapper = createComponent(); + previewLink = getPreviewLink(wrapper); + previewLink.trigger('click'); + + let callPause; + + return axios + .waitFor(markdownPreviewPath) + .then(() => { + const video = getVideo(wrapper); + callPause = jest.spyOn(video.element, 'pause').mockImplementation(() => true); + + wrapper.setProps({ + isSubmitting: true, + markdownPreviewPath, + markdownDocsPath, + }); + + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(callPause).toHaveBeenCalled(); + }); }); it('clicking already active write or preview link does nothing', () => { diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb index 7ebe5842fd0..f84f10bdc46 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb @@ -40,7 +40,7 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do subject.perform! expect(pipeline.config_source).to eq 'auto_devops_source' - template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') + template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps') expect(command.config_content).to eq(template.content) end end @@ -52,7 +52,7 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do subject.perform! expect(pipeline.config_source).to eq 'auto_devops_source' - template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') + template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps') expect(command.config_content).to eq(template.content) end end @@ -82,12 +82,32 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(project).to receive(:auto_devops_enabled?).and_return(true) end - it 'returns the content of AutoDevops template' do - subject.perform! + context 'when beta is enabled' do + before do + stub_feature_flags(auto_devops_beta: true) + end - expect(pipeline.config_source).to eq 'auto_devops_source' - template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') - expect(command.config_content).to eq(template.content) + it 'returns the content of AutoDevops template' do + subject.perform! + + expect(pipeline.config_source).to eq 'auto_devops_source' + template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps') + expect(command.config_content).to eq(template.content) + end + end + + context 'when beta is disabled' do + before do + stub_feature_flags(auto_devops_beta: false) + end + + it 'returns the content of AutoDevops template' do + subject.perform! + + expect(pipeline.config_source).to eq 'auto_devops_source' + template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') + expect(command.config_content).to eq(template.content) + end end end @@ -190,15 +210,38 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(project).to receive(:auto_devops_enabled?).and_return(true) end - it 'builds root config including the auto-devops template' do - subject.perform! + context 'when beta is enabled' do + before do + stub_feature_flags(auto_devops_beta: true) + end - expect(pipeline.config_source).to eq 'auto_devops_source' - expect(command.config_content).to eq(<<~EOY) - --- - include: - - template: Auto-DevOps.gitlab-ci.yml - EOY + it 'builds root config including the auto-devops template' do + subject.perform! + + expect(pipeline.config_source).to eq 'auto_devops_source' + expect(command.config_content).to eq(<<~EOY) + --- + include: + - template: Beta/Auto-DevOps.gitlab-ci.yml + EOY + end + end + + context 'when beta is disabled' do + before do + stub_feature_flags(auto_devops_beta: false) + end + + it 'builds root config including the auto-devops template' do + subject.perform! + + expect(pipeline.config_source).to eq 'auto_devops_source' + expect(command.config_content).to eq(<<~EOY) + --- + include: + - template: Auto-DevOps.gitlab-ci.yml + EOY + end end end diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb index c2f9930056a..12600d97b2f 100644 --- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb @@ -9,7 +9,7 @@ describe 'Auto-DevOps.gitlab-ci.yml' do let(:user) { create(:admin) } let(:default_branch) { 'master' } let(:pipeline_branch) { default_branch } - let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } let(:pipeline) { service.execute!(:push) } let(:build_names) { pipeline.builds.pluck(:name) } @@ -107,4 +107,52 @@ describe 'Auto-DevOps.gitlab-ci.yml' do end end end + + describe 'build-pack detection' do + using RSpec::Parameterized::TableSyntax + + where(:case_name, :files, :variables, :include_build_names, :not_include_build_names) do + 'No match' | { 'README.md' => '' } | {} | %w() | %w(build test) + 'Buildpack' | { 'README.md' => '' } | { 'BUILDPACK_URL' => 'http://example.com' } | %w(build test) | %w() + 'Explicit set' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '1' } | %w(build test) | %w() + 'Explicit unset' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '0' } | %w() | %w(build test) + 'Dockerfile' | { 'Dockerfile' => '' } | {} | %w(build test) | %w() + 'Clojure' | { 'project.clj' => '' } | {} | %w(build test) | %w() + 'Go modules' | { 'go.mod' => '' } | {} | %w(build test) | %w() + 'Go gb' | { 'src/gitlab.com/gopackage.go' => '' } | {} | %w(build test) | %w() + 'Gradle' | { 'gradlew' => '' } | {} | %w(build test) | %w() + 'Java' | { 'pom.xml' => '' } | {} | %w(build test) | %w() + 'Multi-buildpack' | { '.buildpacks' => '' } | {} | %w(build test) | %w() + 'NodeJS' | { 'package.json' => '' } | {} | %w(build test) | %w() + 'PHP' | { 'composer.json' => '' } | {} | %w(build test) | %w() + 'Play' | { 'conf/application.conf' => '' } | {} | %w(build test) | %w() + 'Python' | { 'Pipfile' => '' } | {} | %w(build test) | %w() + 'Ruby' | { 'Gemfile' => '' } | {} | %w(build test) | %w() + 'Scala' | { 'build.sbt' => '' } | {} | %w(build test) | %w() + 'Static' | { '.static' => '' } | {} | %w(build test) | %w() + end + + with_them do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps') } + + let(:user) { create(:admin) } + let(:project) { create(:project, :custom_repo, files: files) } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: 'master' ) } + let(:pipeline) { service.execute(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + variables.each do |(key, value)| + create(:ci_variable, project: project, key: key, value: value) + end + end + + it 'creates a pipeline with the expected jobs' do + expect(build_names).to include(*include_build_names) + expect(build_names).not_to include(*not_include_build_names) + end + end + end end diff --git a/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb b/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb new file mode 100644 index 00000000000..86d5bc93bf7 --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/cargo_toml_linker_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::DependencyLinker::CargoTomlLinker do + describe '.support?' do + it 'supports Cargo.toml' do + expect(described_class.support?('Cargo.toml')).to be_truthy + end + + it 'does not support other files' do + expect(described_class.support?('cargo.yaml')).to be_falsey + end + end + + describe '#link' do + let(:file_name) { "Cargo.toml" } + + let(:file_content) do + <<-CONTENT.strip_heredoc + # See https://doc.rust-lang.org/cargo/reference/manifest.html + [package] + # Package shouldn't be matched + name = "gitlab-test" + version = "0.0.1" + authors = ["Some User <some.user@example.org>"] + description = "A GitLab test Cargo.toml." + keywords = ["gitlab", "test", "rust", "crago"] + readme = "README.md" + + [dependencies] + # Default dependencies format with fixed version and version range + chrono = "0.4.7" + xml-rs = ">=0.8.0" + + [dependencies.memchr] + # Specific dependency with optional info + version = "2.2.1" + optional = true + + [dev-dependencies] + # Dev dependency with version modifier + commandspec = "~0.12.2" + + [build-dependencies] + # Build dependency with version wildcard + thread_local = "0.3.*" + CONTENT + end + + subject { Gitlab::Highlight.highlight(file_name, file_content) } + + def link(name, url) + %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} + end + + it 'links dependencies' do + expect(subject).to include(link('chrono', 'https://crates.io/crates/chrono')) + expect(subject).to include(link('xml-rs', 'https://crates.io/crates/xml-rs')) + expect(subject).to include(link('memchr', 'https://crates.io/crates/memchr')) + expect(subject).to include(link('commandspec', 'https://crates.io/crates/commandspec')) + expect(subject).to include(link('thread_local', 'https://crates.io/crates/thread_local')) + end + + it 'does not contain metadata identified as package' do + expect(subject).not_to include(link('version', 'https://crates.io/crates/version')) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb index 3ea3334caf0..570a994f520 100644 --- a/spec/lib/gitlab/dependency_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker_spec.rb @@ -83,5 +83,13 @@ describe Gitlab::DependencyLinker do described_class.link(blob_name, nil, nil) end + + it 'links using CargoTomlLinker' do + blob_name = 'Cargo.toml' + + expect(described_class::CargoTomlLinker).to receive(:link) + + described_class.link(blob_name, nil, nil) + end end end diff --git a/spec/lib/sentry/client/issue_spec.rb b/spec/lib/sentry/client/issue_spec.rb new file mode 100644 index 00000000000..17548e2081d --- /dev/null +++ b/spec/lib/sentry/client/issue_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Sentry::Client::Issue do + include SentryClientHelpers + + let(:token) { 'test-token' } + let(:client) { Sentry::Client.new(sentry_url, token) } + + describe '#issue_details' do + let(:issue_sample_response) do + Gitlab::Utils.deep_indifferent_access( + JSON.parse(fixture_file('sentry/issue_sample_response.json')) + ) + end + + let(:issue_id) { 503504 } + let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0' } + let(:sentry_request_url) { "#{sentry_url}/issues/#{issue_id}/" } + let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: issue_sample_response) } + + subject { client.issue_details(issue_id: issue_id) } + + it_behaves_like 'calls sentry api' + + it 'escapes issue ID' do + allow(CGI).to receive(:escape).and_call_original + + subject + + expect(CGI).to have_received(:escape).with(issue_id.to_s) + end + + context 'error object created from sentry response' do + using RSpec::Parameterized::TableSyntax + + where(:error_object, :sentry_response) do + :id | :id + :first_seen | :firstSeen + :last_seen | :lastSeen + :title | :title + :type | :type + :user_count | :userCount + :count | :count + :message | [:metadata, :value] + :culprit | :culprit + :short_id | :shortId + :status | :status + :frequency | [:stats, '24h'] + :project_id | [:project, :id] + :project_name | [:project, :name] + :project_slug | [:project, :slug] + :first_release_last_commit | [:firstRelease, :lastCommit] + :last_release_last_commit | [:lastRelease, :lastCommit] + :first_release_short_version | [:firstRelease, :shortVersion] + :last_release_short_version | [:lastRelease, :shortVersion] + end + + with_them do + it do + expect(subject.public_send(error_object)).to eq(issue_sample_response.dig(*sentry_response)) + end + end + + it 'has a correct external URL' do + expect(subject.external_url).to eq('https://sentrytest.gitlab.com/api/0/issues/503504') + end + + it 'issue has a correct external base url' do + expect(subject.external_base_url).to eq('https://sentrytest.gitlab.com/api/0') + end + + it 'has a correct GitLab issue url' do + expect(subject.gitlab_issue).to eq('https://gitlab.com/gitlab-org/gitlab/issues/1') + end + end + end +end diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb index cff06bf4a5f..8500f67b8e9 100644 --- a/spec/lib/sentry/client_spec.rb +++ b/spec/lib/sentry/client_spec.rb @@ -14,12 +14,6 @@ describe Sentry::Client do } end - let(:issues_sample_response) do - Gitlab::Utils.deep_indifferent_access( - JSON.parse(fixture_file('sentry/issues_sample_response.json')) - ) - end - subject(:client) { described_class.new(sentry_url, token) } shared_examples 'issues has correct return type' do |klass| @@ -33,6 +27,12 @@ describe Sentry::Client do end describe '#list_issues' do + let(:issues_sample_response) do + Gitlab::Utils.deep_indifferent_access( + JSON.parse(fixture_file('sentry/issues_sample_response.json')) + ) + end + let(:issue_status) { 'unresolved' } let(:limit) { 20 } let(:search_term) { '' } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 900e0feaccc..20915e6d3b1 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2245,14 +2245,24 @@ describe Ci::Build do end end - describe '#has_expiring_artifacts?' do + describe '#has_expiring_archive_artifacts?' do context 'when artifacts have expiration date set' do before do build.update(artifacts_expire_at: 1.day.from_now) end - it 'has expiring artifacts' do - expect(build).to have_expiring_artifacts + context 'and job artifacts file exists' do + let!(:archive) { create(:ci_job_artifact, :archive, job: build) } + + it 'has expiring artifacts' do + expect(build).to have_expiring_archive_artifacts + end + end + + context 'and job artifacts file does not exist' do + it 'does not have expiring artifacts' do + expect(build).not_to have_expiring_archive_artifacts + end end end @@ -2262,7 +2272,7 @@ describe Ci::Build do end it 'does not have expiring artifacts' do - expect(build).not_to have_expiring_artifacts + expect(build).not_to have_expiring_archive_artifacts end end end diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index 601dac21e6a..ae493d5c081 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -91,18 +91,124 @@ describe DiffNote do end describe '#create_diff_file callback' do - let(:noteable) { create(:merge_request) } - let(:project) { noteable.project } - context 'merge request' do - let!(:diff_note) { create(:diff_note_on_merge_request, project: project, noteable: noteable) } + let(:position) do + Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 9, + diff_refs: merge_request.diff_refs) + end - it 'creates a diff note file' do - expect(diff_note.reload.note_diff_file).to be_present + subject { build(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } + + let(:diff_file_from_repository) do + position.diff_file(project.repository) + end + + let(:diff_file) do + diffs = merge_request.diffs + raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['files/ruby/popen.rb'])).first + Gitlab::Diff::File.new(raw_diff, + repository: diffs.project.repository, + diff_refs: diffs.diff_refs, + fallback_diff_refs: diffs.fallback_diff_refs) + end + + let(:diff_line) { diff_file.diff_lines.first } + + let(:line_code) { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14' } + + before do + allow(subject.position).to receive(:line_code).and_return('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14') + end + + context 'when diffs are already created' do + before do + allow(subject).to receive(:created_at_diff?).and_return(true) + end + + context 'when diff_file is found in persisted diffs' do + before do + allow(merge_request).to receive_message_chain(:diffs, :diff_files, :first).and_return(diff_file) + end + + context 'when importing' do + before do + subject.importing = true + subject.line_code = line_code + end + + context 'when diff_line is found in persisted diff_file' do + before do + allow(diff_file).to receive(:line_for_position).with(position).and_return(diff_line) + end + + it 'creates a diff note file' do + subject.save + expect(subject.note_diff_file).to be_present + end + end + + context 'when diff_line is not found in persisted diff_file' do + before do + allow(diff_file).to receive(:line_for_position).and_return(nil) + end + + it_behaves_like 'a valid diff note with after commit callback' + end + end + + context 'when not importing' do + context 'when diff_line is not found' do + before do + allow(diff_file).to receive(:line_for_position).with(position).and_return(nil) + end + + it 'raises an error' do + expect { subject.save }.to raise_error(::DiffNote::NoteDiffFileCreationError, + "Failed to find diff line for: #{diff_file.file_path}, "\ + "old_line: #{position.old_line}"\ + ", new_line: #{position.new_line}") + end + end + + context 'when diff_line is found' do + before do + allow(diff_file).to receive(:line_for_position).with(position).and_return(diff_line) + end + + it 'creates a diff note file' do + subject.save + expect(subject.reload.note_diff_file).to be_present + end + end + end + end + + context 'when diff file is not found in persisted diffs' do + before do + allow_next_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff) do |merge_request_diff| + allow(merge_request_diff).to receive(:diff_files).and_return([]) + end + end + + it_behaves_like 'a valid diff note with after commit callback' + end + end + + context 'when diffs are not already created' do + before do + allow(subject).to receive(:created_at_diff?).and_return(false) + end + + it_behaves_like 'a valid diff note with after commit callback' end it 'does not create diff note file if it is a reply' do - expect { create(:diff_note_on_merge_request, noteable: noteable, in_reply_to: diff_note) } + diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request) + + expect { create(:diff_note_on_merge_request, noteable: merge_request, in_reply_to: diff_note) } .not_to change(NoteDiffFile, :count) end end diff --git a/spec/serializers/build_artifact_entity_spec.rb b/spec/serializers/build_artifact_entity_spec.rb index 09fe094fff1..5f1d5093e0a 100644 --- a/spec/serializers/build_artifact_entity_spec.rb +++ b/spec/serializers/build_artifact_entity_spec.rb @@ -4,6 +4,8 @@ require 'spec_helper' describe BuildArtifactEntity do let(:job) { create(:ci_build, name: 'test:job', artifacts_expire_at: 1.hour.from_now) } + let!(:archive) { create(:ci_job_artifact, :archive, job: job) } + let!(:metadata) { create(:ci_job_artifact, :metadata, job: job) } let(:entity) do described_class.new(job, request: double) diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index 91c5fd6bf2c..6fe9eaedfeb 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -176,5 +176,27 @@ describe BuildDetailsEntity do expect(subject[:reports].first[:file_type]).to eq('codequality') end end + + context 'when the build has no archive type artifacts' do + let!(:report) { create(:ci_job_artifact, :codequality, job: build) } + + it 'does not expose any artifact actions path' do + expect(subject[:artifact].keys).not_to include(:download_path, :browse_path, :keep_path) + end + end + + context 'when the build has archive type artifacts' do + let!(:report) { create(:ci_job_artifact, :codequality, job: build) } + let!(:archive) { create(:ci_job_artifact, :archive, job: build) } + let!(:metadata) { create(:ci_job_artifact, :metadata, job: build) } + + before do + build.update(artifacts_expire_at: 7.days.from_now) + end + + it 'exposes artifact details' do + expect(subject[:artifact].keys).to include(:download_path, :browse_path, :keep_path, :expire_at, :expired) + end + end end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 04e57b1a2d4..5f760844046 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -493,6 +493,7 @@ describe Ci::CreatePipelineService do before do stub_ci_pipeline_yaml_file(nil) allow_any_instance_of(Project).to receive(:auto_devops_enabled?).and_return(true) + create(:project_auto_devops, project: project) end it 'pull it from Auto-DevOps' do diff --git a/spec/services/external_pull_requests/create_pipeline_service_spec.rb b/spec/services/external_pull_requests/create_pipeline_service_spec.rb index a4da5b38b97..03481baea87 100644 --- a/spec/services/external_pull_requests/create_pipeline_service_spec.rb +++ b/spec/services/external_pull_requests/create_pipeline_service_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' describe ExternalPullRequests::CreatePipelineService do describe '#execute' do - set(:project) { create(:project, :repository) } + set(:project) { create(:project, :auto_devops, :repository) } set(:user) { create(:user) } let(:pull_request) { create(:external_pull_request, project: project) } diff --git a/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb b/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb new file mode 100644 index 00000000000..835d2dfe757 --- /dev/null +++ b/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +shared_examples 'a valid diff note with after commit callback' do + context 'when diff file is fetched from repository' do + before do + allow_any_instance_of(::Gitlab::Diff::Position).to receive(:diff_file).with(project.repository).and_return(diff_file_from_repository) + end + + context 'when diff_line is not found' do + it 'raises an error' do + allow(diff_file_from_repository).to receive(:line_for_position).with(position).and_return(nil) + + expect { subject.save }.to raise_error(::DiffNote::NoteDiffFileCreationError, + "Failed to find diff line for: #{diff_file_from_repository.file_path}, "\ + "old_line: #{position.old_line}"\ + ", new_line: #{position.new_line}") + end + end + + context 'when diff_line is found' do + before do + allow(diff_file_from_repository).to receive(:line_for_position).with(position).and_return(diff_line) + end + + it 'fallback to fetch file from repository' do + expect_any_instance_of(::Gitlab::Diff::Position).to receive(:diff_file).with(project.repository) + + subject.save + end + + it 'creates a diff note file' do + subject.save + + expect(subject.reload.note_diff_file).to be_present + end + end + end + + context 'when diff file is not found in repository' do + it 'raises an error' do + allow_any_instance_of(::Gitlab::Diff::Position).to receive(:diff_file).with(project.repository).and_return(nil) + + expect { subject.save }.to raise_error(::DiffNote::NoteDiffFileCreationError, 'Failed to find diff file') + end + end +end |