diff options
111 files changed, 884 insertions, 421 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index e23e3fd2982..5fea1768541 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.112.0 +0.113.0 @@ -418,7 +418,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.105.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.106.0', require: 'gitaly' gem 'grpc', '~> 1.11.0' # Locked until https://github.com/google/protobuf/issues/4210 is closed diff --git a/Gemfile.lock b/Gemfile.lock index 7f9207d9dfe..6415f9e6132 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -282,7 +282,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.105.0) + gitaly-proto (0.106.0) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -1037,7 +1037,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.105.0) + gitaly-proto (~> 0.106.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 766f2479ea5..50ca0d5a729 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -285,7 +285,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.105.0) + gitaly-proto (0.106.0) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -1047,7 +1047,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.105.0) + gitaly-proto (~> 0.106.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index fa00a3cf386..e8c59fab609 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -53,4 +53,8 @@ export default class Autosave { return window.localStorage.removeItem(this.key); } + + dispose() { + this.field.off('input'); + } } diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 8139aa69fc7..e565af800d0 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -162,8 +162,10 @@ export default class Clusters { if (type === 'password') { this.tokenField.setAttribute('type', 'text'); + this.showTokenButton.textContent = s__('ClusterIntegration|Hide'); } else { this.tokenField.setAttribute('type', 'password'); + this.showTokenButton.textContent = s__('ClusterIntegration|Show'); } } diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index ad838a32518..0fe0007057b 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -77,7 +77,8 @@ export default { diffViewType: state => state.diffs.diffViewType, diffFiles: state => state.diffs.diffFiles, }), - ...mapGetters(['isLoggedIn', 'discussionsByLineCode']), + ...mapGetters(['isLoggedIn']), + ...mapGetters('diffs', ['discussionsByLineCode']), lineHref() { return this.lineCode ? `#${this.lineCode}` : '#'; }, @@ -189,7 +190,6 @@ export default { </button> <a v-if="lineNumber" - v-once :data-linenumber="lineNumber" :href="lineHref" > diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index 32f9516d332..cbe4551d06b 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -1,17 +1,17 @@ <script> -import $ from 'jquery'; import { mapState, mapGetters, mapActions } from 'vuex'; import createFlash from '~/flash'; import { s__ } from '~/locale'; import noteForm from '../../notes/components/note_form.vue'; import { getNoteFormData } from '../store/utils'; -import Autosave from '../../autosave'; -import { DIFF_NOTE_TYPE, NOTE_TYPE } from '../constants'; +import autosave from '../../notes/mixins/autosave'; +import { DIFF_NOTE_TYPE } from '../constants'; export default { components: { noteForm, }, + mixins: [autosave], props: { diffFileHash: { type: String, @@ -41,28 +41,35 @@ export default { }, mounted() { if (this.isLoggedIn) { - const noteableData = this.getNoteableData; const keys = [ - NOTE_TYPE, - this.noteableType, - noteableData.id, - noteableData.diff_head_sha, + this.noteableData.diff_head_sha, DIFF_NOTE_TYPE, - noteableData.source_project_id, + this.noteableData.source_project_id, this.line.lineCode, ]; - this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys); + this.initAutoSave(this.noteableData, keys); } }, methods: { ...mapActions('diffs', ['cancelCommentForm']), ...mapActions(['saveNote', 'refetchDiscussionById']), - handleCancelCommentForm() { - this.autosave.reset(); + handleCancelCommentForm(shouldConfirm, isDirty) { + if (shouldConfirm && isDirty) { + const msg = s__('Notes|Are you sure you want to cancel creating this comment?'); + + // eslint-disable-next-line no-alert + if (!window.confirm(msg)) { + return; + } + } + this.cancelCommentForm({ lineCode: this.line.lineCode, }); + this.$nextTick(() => { + this.resetAutoSave(); + }); }, handleSaveNote(note) { const selectedDiffFile = this.getDiffFileByHash(this.diffFileHash); diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue index ca265dd892c..a6f011ff31e 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -26,13 +26,16 @@ export default { ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), - ...mapGetters(['discussionsByLineCode']), + ...mapGetters('diffs', ['discussionsByLineCode']), discussions() { return this.discussionsByLineCode[this.line.lineCode] || []; }, className() { return this.discussions.length ? '' : 'js-temp-notes-holder'; }, + hasCommentForm() { + return this.diffLineCommentForms[this.line.lineCode]; + }, }, }; </script> @@ -53,7 +56,7 @@ export default { :discussions="discussions" /> <diff-line-note-form - v-if="diffLineCommentForms[line.lineCode]" + v-if="hasCommentForm" :diff-file-hash="diffFileHash" :line="line" :note-target-line="line" diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue index 0197a510ef1..0e306f39a9f 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -101,7 +101,6 @@ export default { class="diff-line-num new_line" /> <td - v-once :class="line.type" class="line_content" v-html="line.richText" diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue index 9fd19b74cd7..8e491d293e5 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_view.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue @@ -20,8 +20,7 @@ export default { }, }, computed: { - ...mapGetters('diffs', ['commitId']), - ...mapGetters(['discussionsByLineCode']), + ...mapGetters('diffs', ['commitId', 'discussionsByLineCode']), ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue index cc5248c25d9..05e5cafc717 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -26,7 +26,7 @@ export default { ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), - ...mapGetters(['discussionsByLineCode']), + ...mapGetters('diffs', ['discussionsByLineCode']), leftLineCode() { return this.line.left.lineCode; }, diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue index ee5bb4d8d05..0031cedc68f 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue @@ -119,7 +119,6 @@ export default { class="diff-line-num old_line" /> <td - v-once :id="line.left.lineCode" :class="parallelViewLeftLineType" class="line_content parallel left-side" @@ -140,7 +139,6 @@ export default { class="diff-line-num new_line" /> <td - v-once :id="line.right.lineCode" :class="line.right.type" class="line_content parallel right-side" diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue index 32528c9e7ab..8f8d6bbc818 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue @@ -21,8 +21,7 @@ export default { }, }, computed: { - ...mapGetters('diffs', ['commitId']), - ...mapGetters(['discussionsByLineCode']), + ...mapGetters('diffs', ['commitId', 'discussionsByLineCode']), ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index 855de79adf8..d3881fa1a0a 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -1,5 +1,7 @@ import _ from 'underscore'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants'; +import { getDiffRefsByLineCode } from './utils'; export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE; @@ -56,6 +58,44 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) = discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash), ) || []; +/** + * Returns an Object with discussions by their diff line code + * To avoid rendering outdated discussions on the Changes tab we should do a bunch of SHA + * comparisions. `note.position.formatter` have the current version diff refs but + * `note.original_position.formatter` will have the first version's diff refs. + * If line diff refs matches with one of them, we should render it as a discussion on Changes tab. + * + * @param {Object} diff + * @returns {Array} + */ +export const discussionsByLineCode = (state, getters, rootState, rootGetters) => { + const diffRefsByLineCode = getDiffRefsByLineCode(state.diffFiles); + + return rootGetters.discussions.reduce((acc, note) => { + const isDiffDiscussion = note.diff_discussion; + const hasLineCode = note.line_code; + const isResolvable = note.resolvable; + const diffRefs = diffRefsByLineCode[note.line_code]; + + if (isDiffDiscussion && hasLineCode && isResolvable && diffRefs) { + const refs = convertObjectPropsToCamelCase(note.position.formatter); + const originalRefs = convertObjectPropsToCamelCase(note.original_position.formatter); + + if (_.isEqual(refs, diffRefs) || _.isEqual(originalRefs, diffRefs)) { + const lineCode = note.line_code; + + if (acc[lineCode]) { + acc[lineCode].push(note); + } else { + acc[lineCode] = [note]; + } + } + } + + return acc; + }, {}); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma∂ tests export const getDiffFileByHash = state => fileHash => state.diffFiles.find(file => file.fileHash === fileHash); diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index d9589baa76e..82082ac508a 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -173,3 +173,24 @@ export function trimFirstCharOfLineContent(line = {}) { return parsedLine; } + +export function getDiffRefsByLineCode(diffFiles) { + return diffFiles.reduce((acc, diffFile) => { + const { baseSha, headSha, startSha } = diffFile.diffRefs; + const { newPath, oldPath } = diffFile; + + // We can only use highlightedDiffLines to create the map of diff lines because + // highlightedDiffLines will also include every parallel diff line in it. + if (diffFile.highlightedDiffLines) { + diffFile.highlightedDiffLines.forEach(line => { + const { lineCode, oldLine, newLine } = line; + + if (lineCode) { + acc[lineCode] = { baseSha, headSha, startSha, newPath, oldPath, oldLine, newLine }; + } + }); + } + + return acc; + }, {}); +} diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 92fe98508ad..1e6803abf3a 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -125,6 +125,7 @@ export default { :class="flagOrientation" class="prometheus-graph-flag popover" > + <div class="arrow-shadow"></div> <div class="arrow"></div> <div class="popover-title"> <h5 v-if="deploymentFlagData"> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 26482a02e00..abcd4422d7c 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -7,7 +7,7 @@ import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; export default { - name: 'IssueNoteForm', + name: 'NoteForm', components: { issueWarning, markdownField, diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index bee635398b3..2f1a68731c7 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -6,6 +6,7 @@ import nextDiscussionsSvg from 'icons/_next_discussion.svg'; import { convertObjectPropsToCamelCase, scrollToElement } from '~/lib/utils/common_utils'; import { truncateSha } from '~/lib/utils/text_utility'; import systemNote from '~/vue_shared/components/notes/system_note.vue'; +import { s__ } from '~/locale'; import Flash from '../../flash'; import { SYSTEM_NOTE } from '../constants'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -144,19 +145,17 @@ export default { return this.isDiffDiscussion ? '' : 'card discussion-wrapper'; }, }, - mounted() { - if (this.isReplying) { - this.initAutoSave(this.transformedDiscussion); - } - }, - updated() { - if (this.isReplying) { - if (!this.autosave) { - this.initAutoSave(this.transformedDiscussion); + watch: { + isReplying() { + if (this.isReplying) { + this.$nextTick(() => { + // Pass an extra key to separate reply and note edit forms + this.initAutoSave(this.transformedDiscussion, ['Reply']); + }); } else { - this.setAutoSave(); + this.disposeAutoSave(); } - } + }, }, created() { this.resolveDiscussionsSvg = resolveDiscussionsSvg; @@ -194,16 +193,18 @@ export default { showReplyForm() { this.isReplying = true; }, - cancelReplyForm(shouldConfirm) { - if (shouldConfirm && this.$refs.noteForm.isDirty) { + cancelReplyForm(shouldConfirm, isDirty) { + if (shouldConfirm && isDirty) { + const msg = s__('Notes|Are you sure you want to cancel creating this comment?'); + // eslint-disable-next-line no-alert - if (!window.confirm('Are you sure you want to cancel creating this comment?')) { + if (!window.confirm(msg)) { return; } } - this.resetAutoSave(); this.isReplying = false; + this.resetAutoSave(); }, saveReply(noteText, form, callback) { const postData = { @@ -420,7 +421,8 @@ Please check your network connection and try again.`; :is-editing="false" save-button-title="Comment" @handleFormUpdate="saveReply" - @cancelForm="cancelReplyForm" /> + @cancelForm="cancelReplyForm" + /> <note-signed-out-widget v-if="!canReply" /> </div> </div> diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js index 36cc8d5d056..4f45f912479 100644 --- a/app/assets/javascripts/notes/mixins/autosave.js +++ b/app/assets/javascripts/notes/mixins/autosave.js @@ -4,12 +4,18 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility'; export default { methods: { - initAutoSave(noteable) { - this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [ + initAutoSave(noteable, extraKeys = []) { + let keys = [ 'Note', - capitalizeFirstCharacter(noteable.noteable_type), + capitalizeFirstCharacter(noteable.noteable_type || noteable.noteableType), noteable.id, - ]); + ]; + + if (extraKeys) { + keys = keys.concat(extraKeys); + } + + this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys); }, resetAutoSave() { this.autosave.reset(); @@ -17,5 +23,8 @@ export default { setAutoSave() { this.autosave.save(); }, + disposeAutoSave() { + this.autosave.dispose(); + }, }, }; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 5c65e1c3bb5..e9e95dd4219 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -28,18 +28,6 @@ export const notesById = state => return acc; }, {}); -export const discussionsByLineCode = state => - state.discussions.reduce((acc, note) => { - if (note.diff_discussion && note.line_code && note.resolvable) { - // For context about line notes: there might be multiple notes with the same line code - const items = acc[note.line_code] || []; - items.push(note); - - Object.assign(acc, { [note.line_code]: items }); - } - return acc; - }, {}); - export const noteableType = state => { const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants; diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index c42c4a1fbe7..e7ff76c8218 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -1,24 +1,40 @@ <script> -/* This is a re-usable vue component for rendering a svg sprite - icon - Sample configuration: - - <icon - name="retry" - :size="32" - css-classes="top" - /> - -*/ // only allow classes in images.scss e.g. s12 const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; +let iconValidator = () => true; + +/* + During development/tests we want to validate that we are just using icons that are actually defined +*/ +if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line global-require + const data = require('@gitlab-org/gitlab-svgs/dist/icons.json'); + const { icons } = data; + iconValidator = value => { + if (icons.includes(value)) { + return true; + } + // eslint-disable-next-line no-console + console.warn(`Icon '${value}' is not a known icon of @gitlab/gitlab-svg`); + return false; + }; +} +/** This is a re-usable vue component for rendering a svg sprite icon + * @example + * <icon + * name="retry" + * :size="32" + * css-classes="top" + * /> + */ export default { props: { name: { type: String, required: true, + validator: iconValidator, }, size: { @@ -83,6 +99,6 @@ export default { :x="x" :y="y" > - <use v-bind="{ 'xlink:href':spriteHref }" /> + <use v-bind="{ 'xlink:href':spriteHref }"/> </svg> </template> diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index a8e28104a94..5ca4d944d73 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -47,7 +47,6 @@ .card-body { padding: $gl-padding; - background-color: $white-light; .form-actions { margin: -$gl-padding; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3144dcc4dc0..8915b323b3c 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -293,6 +293,8 @@ .prometheus-graph-flag { display: block; min-width: 160px; + border: 0; + box-shadow: 0 1px 4px 0 $black-transparent; h5 { padding: 0; @@ -312,7 +314,6 @@ &.popover { padding: 0; - border: 1px solid $border-color; &.left { left: auto; @@ -320,12 +321,19 @@ margin-right: 10px; > .arrow { - right: -16px; + right: -14px; border-left-color: $border-color; } > .arrow::after { - border-left-color: $theme-gray-50; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-left: 4px solid $theme-gray-50; + } + + .arrow-shadow { + right: -3px; + box-shadow: 1px 0 9px 0 $black-transparent; } } @@ -335,19 +343,35 @@ margin-left: 10px; > .arrow { - left: -16px; + left: -7px; border-right-color: $border-color; } > .arrow::after { - border-right-color: $theme-gray-50; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-right: 4px solid $theme-gray-50; + } + + .arrow-shadow { + left: -3px; + box-shadow: 1px 0 8px 0 $black-transparent; } } > .arrow { - top: 16px; - margin-top: -8px; - border-width: 8px; + top: 10px; + margin: 0; + } + + .arrow-shadow { + content: ""; + position: absolute; + width: 7px; + height: 7px; + background-color: transparent; + transform: rotate(45deg); + top: 13px; } > .popover-title, @@ -355,10 +379,12 @@ padding: 8px; font-size: 12px; white-space: nowrap; + position: relative; } > .popover-title { background-color: $theme-gray-50; + border-radius: $border-radius-default $border-radius-default 0 0; } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f45fcd4d900..eeceb99c8d2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base include EnforcesTwoFactorAuthentication include WithPerformanceBar + before_action :limit_unauthenticated_session_times before_action :authenticate_sessionless_user! before_action :authenticate_user! before_action :enforce_terms!, if: :should_enforce_terms? @@ -85,6 +86,24 @@ class ApplicationController < ActionController::Base end end + # By default, all sessions are given the same expiration time configured in + # the session store (e.g. 1 week). However, unauthenticated users can + # generate a lot of sessions, primarily for CSRF verification. It makes + # sense to reduce the TTL for unauthenticated to something much lower than + # the default (e.g. 1 hour) to limit Redis memory. In addition, Rails + # creates a new session after login, so the short TTL doesn't even need to + # be extended. + def limit_unauthenticated_session_times + return if current_user + + # Rack sets this header, but not all tests may have it: https://github.com/rack/rack/blob/fdcd03a3c5a1c51d1f96fc97f9dfa1a9deac0c77/lib/rack/session/abstract/id.rb#L251-L259 + return unless request.env['rack.session.options'] + + # This works because Rack uses these options every time a request is handled: + # https://github.com/rack/rack/blob/fdcd03a3c5a1c51d1f96fc97f9dfa1a9deac0c77/lib/rack/session/abstract/id.rb#L342 + request.env['rack.session.options'][:expire_after] = Settings.gitlab['unauthenticated_session_expire_delay'] + end + protected def append_info_to_payload(payload) diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 2f304b040c7..41084ec686f 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -1,3 +1,5 @@ +require 'json' + module IconsHelper extend self include FontAwesome::Rails::IconHelper @@ -38,6 +40,13 @@ module IconsHelper end def sprite_icon(icon_name, size: nil, css_class: nil) + if Gitlab::Sentry.should_raise? + unless known_sprites.include?(icon_name) + exception = ArgumentError.new("#{icon_name} is not a known icon in @gitlab-org/gitlab-svg") + raise exception + end + end + css_classes = size ? "s#{size}" : "" css_classes << " #{css_class}" unless css_class.blank? content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) @@ -134,4 +143,10 @@ module IconsHelper icon_class end + + private + + def known_sprites + @known_sprites ||= JSON.parse(File.read(Rails.root.join('node_modules/@gitlab-org/gitlab-svgs/dist/icons.json')))['icons'] + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index a96c73e6ab7..e248f94cbd8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -154,12 +154,9 @@ class Repository # Returns a list of commits that are not present in any reference def new_commits(newrev) - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1233 - refs = Gitlab::GitalyClient::StorageSettings.allow_disk_access do - ::Gitlab::Git::RevList.new(raw, newrev: newrev).new_refs - end + commits = raw.new_commits(newrev) - refs.map { |sha| commit(sha.strip) } + ::Commit.decorate(commits, project) end # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/384 diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb index 8a39a4950f5..7505bbdeb3d 100644 --- a/app/serializers/discussion_entity.rb +++ b/app/serializers/discussion_entity.rb @@ -4,6 +4,7 @@ class DiscussionEntity < Grape::Entity expose :id, :reply_id expose :position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? } + expose :original_position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? } expose :line_code, if: -> (d, _) { d.diff_discussion? } expose :expanded?, as: :expanded expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? } diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 83f7b99d2a5..b1365659834 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -136,10 +136,6 @@ class FileUploader < GitlabUploader } end - def filename - self.file.filename - end - def upload=(value) super diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index b0562226f5f..f730fd05176 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -149,8 +149,8 @@ %br = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - else - .card.bg-warning - .card-header + .card.border-warning + .card-header.bg-warning.text-white Block this user .card-body %p Blocking user has the following effects: @@ -170,8 +170,8 @@ %br = link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - .card.bg-danger - .card-header + .card.border-danger + .card-header.bg-danger.text-white = s_('AdminUsers|Delete user') .card-body - if @user.can_be_removed? && can?(current_user, :destroy_user, @user) @@ -196,8 +196,8 @@ %p You don't have access to delete this user. - .card.bg-danger - .card-header + .card.border-danger + .card-header.bg-danger.text-white = s_('AdminUsers|Delete user and contributions') .card-body - if can?(current_user, :destroy_user, @user) diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index e35ebdea435..6950e2e332d 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -13,10 +13,16 @@ - if current_user.two_factor_otp_enabled? %p You've already enabled two-factor authentication using mobile authenticator applications. In order to register a different device, you must first disable two-factor authentication. + %p + If you lose your recovery codes you can generate new ones, invalidating all previous codes. + %div = link_to 'Disable two-factor authentication', profile_two_factor_auth_path, method: :delete, data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." }, - class: 'btn btn-danger' + class: 'btn btn-danger append-right-10' + = form_tag codes_profile_two_factor_auth_path, {style: 'display: inline-block', method: :post} do |f| + = submit_tag 'Regenerate recovery codes', class: 'btn' + - else %p Download the Google Authenticator application from App Store or Google Play Store and scan this code. diff --git a/app/views/projects/deploy_tokens/_form.html.haml b/app/views/projects/deploy_tokens/_form.html.haml index f8db30df7b4..329b9e7e562 100644 --- a/app/views/projects/deploy_tokens/_form.html.haml +++ b/app/views/projects/deploy_tokens/_form.html.haml @@ -14,16 +14,16 @@ .form-group = f.label :scopes, class: 'label-light' - %fieldset - = f.check_box :read_repository - = label_tag ("deploy_token_read_repository"), 'read_repository' - %span= s_('DeployTokens|Allows read-only access to the repository') + %fieldset.form-group.form-check + = f.check_box :read_repository, class: 'form-check-input' + = label_tag ("deploy_token_read_repository"), 'read_repository', class: 'label-light form-check-label' + .text-secondary= s_('DeployTokens|Allows read-only access to the repository') - if container_registry_enabled?(project) - %fieldset - = f.check_box :read_registry - = label_tag ("deploy_token_read_registry"), 'read_registry' - %span= s_('DeployTokens|Allows read-only access to the registry images') + %fieldset.form-group.form-check + = f.check_box :read_registry, class: 'form-check-input' + = label_tag ("deploy_token_read_registry"), 'read_registry', class: 'label-light form-check-label' + .text-secondary= s_('DeployTokens|Allows read-only access to the registry images') .prepend-top-default = f.submit s_('DeployTokens|Create deploy token'), class: 'btn btn-success' diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index ca82054d799..8ce822c43b7 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -5,8 +5,8 @@ %hr - if @project.import_failed? - .card.bg-danger - .card-header The repository could not be imported. + .card.border-danger + .card-header.bg-danger.text-white The repository could not be imported. .card-body %pre :preserve diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index 9b77c4e3494..ae8c801b705 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -1,7 +1,7 @@ - if @project.pages_deployed? - if can?(current_user, :remove_pages, @project) - .card.bg-danger - .card-header Remove pages + .card.border-danger + .card-header.bg-danger.text-white Remove pages .errors-holder .card-body %p diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 8d0cfc73ccd..17ad1d5ab88 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -51,7 +51,7 @@ class EmailsOnPushWorker end end - recipients.split.each do |recipient| + valid_recipients(recipients).each do |recipient| begin send_email( recipient, @@ -89,4 +89,10 @@ class EmailsOnPushWorker email.header[:skip_premailer] = true if skip_premailer email.deliver_now end + + def valid_recipients(recipients) + recipients.split.select do |recipient| + recipient.include?('@') + end + end end diff --git a/changelogs/unreleased/41784-monitoring-graph-popovers.yml b/changelogs/unreleased/41784-monitoring-graph-popovers.yml new file mode 100644 index 00000000000..757445d7e0c --- /dev/null +++ b/changelogs/unreleased/41784-monitoring-graph-popovers.yml @@ -0,0 +1,5 @@ +--- +title: Update design for system metrics popovers +merge_request: 20655 +author: +type: fixed diff --git a/changelogs/unreleased/4525-fix-project-indexes.yml b/changelogs/unreleased/4525-fix-project-indexes.yml new file mode 100644 index 00000000000..930e3b934c2 --- /dev/null +++ b/changelogs/unreleased/4525-fix-project-indexes.yml @@ -0,0 +1,5 @@ +--- +title: Rework some projects table indexes around repository_storage field +merge_request: 20377 +author: +type: fixed diff --git a/changelogs/unreleased/49324-add-support-for-tar-gz-autodevops-charts.yml b/changelogs/unreleased/49324-add-support-for-tar-gz-autodevops-charts.yml new file mode 100644 index 00000000000..a87eef3f7f3 --- /dev/null +++ b/changelogs/unreleased/49324-add-support-for-tar-gz-autodevops-charts.yml @@ -0,0 +1,5 @@ +--- +title: Add support for tar.gz AUTO_DEVOPS_CHART charts (#49324) +merge_request: 20691 +author: '@kondi1' +type: added diff --git a/changelogs/unreleased/_acet-fix-expanding-context-lines.yml b/changelogs/unreleased/_acet-fix-expanding-context-lines.yml new file mode 100644 index 00000000000..41b4dbca5d6 --- /dev/null +++ b/changelogs/unreleased/_acet-fix-expanding-context-lines.yml @@ -0,0 +1,5 @@ +--- +title: Fix rendering of the context lines in MR diffs page +merge_request: 20642 +author: +type: fixed diff --git a/changelogs/unreleased/_acet-fix-mr-autosave.yml b/changelogs/unreleased/_acet-fix-mr-autosave.yml new file mode 100644 index 00000000000..f87b32f68e2 --- /dev/null +++ b/changelogs/unreleased/_acet-fix-mr-autosave.yml @@ -0,0 +1,5 @@ +--- +title: Fix autosave and ESC confirmation issues for MR discussions +merge_request: 20569 +author: +type: fixed diff --git a/changelogs/unreleased/_acet-fix-outdated-discussions.yml b/changelogs/unreleased/_acet-fix-outdated-discussions.yml new file mode 100644 index 00000000000..d31483b4765 --- /dev/null +++ b/changelogs/unreleased/_acet-fix-outdated-discussions.yml @@ -0,0 +1,5 @@ +--- +title: Fix showing outdated discussions on Changes tab +merge_request: 20445 +author: +type: fixed diff --git a/changelogs/unreleased/accept-rf3-2822-compliant-addresses.yml b/changelogs/unreleased/accept-rf3-2822-compliant-addresses.yml new file mode 100644 index 00000000000..97d017613ba --- /dev/null +++ b/changelogs/unreleased/accept-rf3-2822-compliant-addresses.yml @@ -0,0 +1,5 @@ +--- +title: Emails on push recipients now accepts formats like John Doe <johndoe@example.com> +merge_request: +author: George Thomas +type: added diff --git a/changelogs/unreleased/fix-diff-note.yml b/changelogs/unreleased/fix-diff-note.yml new file mode 100644 index 00000000000..6f10f86b9bc --- /dev/null +++ b/changelogs/unreleased/fix-diff-note.yml @@ -0,0 +1,5 @@ +--- +title: Fix serialization of LegacyDiffNote +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-filename-for-direct-uploads.yml b/changelogs/unreleased/fix-filename-for-direct-uploads.yml new file mode 100644 index 00000000000..a1bbf3704c0 --- /dev/null +++ b/changelogs/unreleased/fix-filename-for-direct-uploads.yml @@ -0,0 +1,5 @@ +--- +title: Fix filename for accelerated uploads +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/ravlen-deploy-tokens-display-update.yml b/changelogs/unreleased/ravlen-deploy-tokens-display-update.yml new file mode 100644 index 00000000000..fd5a6521882 --- /dev/null +++ b/changelogs/unreleased/ravlen-deploy-tokens-display-update.yml @@ -0,0 +1,5 @@ +--- +title: "Cleans up display of Deploy Tokens to match Personal Access Tokens" +merge_request: 20578 +author: Marcel Amirault +type: added
\ No newline at end of file diff --git a/changelogs/unreleased/regen-2fa-codes.yml b/changelogs/unreleased/regen-2fa-codes.yml new file mode 100644 index 00000000000..596f759df0f --- /dev/null +++ b/changelogs/unreleased/regen-2fa-codes.yml @@ -0,0 +1,5 @@ +--- +title: Added button to regenerate 2FA codes +merge_request: +author: Luke Picciau +type: added diff --git a/changelogs/unreleased/sh-limit-unauthenticated-session-times.yml b/changelogs/unreleased/sh-limit-unauthenticated-session-times.yml new file mode 100644 index 00000000000..44a46b4115e --- /dev/null +++ b/changelogs/unreleased/sh-limit-unauthenticated-session-times.yml @@ -0,0 +1,5 @@ +--- +title: Limit the TTL for anonymous sessions to 1 hour +merge_request: 20700 +author: +type: performance diff --git a/changelogs/unreleased/toggle-password-cluster.yml b/changelogs/unreleased/toggle-password-cluster.yml new file mode 100644 index 00000000000..1a43c4baa25 --- /dev/null +++ b/changelogs/unreleased/toggle-password-cluster.yml @@ -0,0 +1,5 @@ +--- +title: Toggle Show / Hide Button for Kubernetes Password +merge_request: 20659 +author: gfyoung +type: fixed diff --git a/changelogs/unreleased/update-card-body-style.yml b/changelogs/unreleased/update-card-body-style.yml new file mode 100644 index 00000000000..d9197c18502 --- /dev/null +++ b/changelogs/unreleased/update-card-body-style.yml @@ -0,0 +1,5 @@ +--- +title: Remove background color from card-body style +merge_request: 20689 +author: George Tsiolis +type: fixed diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b4868596785..c3122827a6b 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -140,6 +140,7 @@ Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 Settings.gitlab['session_expire_delay'] ||= 10080 +Settings.gitlab['unauthenticated_session_expire_delay'] ||= 1.hour.to_i Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? diff --git a/db/post_migrate/20180704145007_update_project_indexes.rb b/db/post_migrate/20180704145007_update_project_indexes.rb new file mode 100644 index 00000000000..193563b36db --- /dev/null +++ b/db/post_migrate/20180704145007_update_project_indexes.rb @@ -0,0 +1,23 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class UpdateProjectIndexes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + NEW_INDEX_NAME = 'idx_project_repository_check_partial' + + disable_ddl_transaction! + + def up + add_concurrent_index(:projects, + [:repository_storage, :created_at], + name: NEW_INDEX_NAME, + where: 'last_repository_check_at IS NULL' + ) + end + + def down + remove_concurrent_index_by_name(:projects, NEW_INDEX_NAME) + end +end diff --git a/db/schema.rb b/db/schema.rb index d2aa31fae30..1a5555fb3a4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1674,6 +1674,7 @@ ActiveRecord::Schema.define(version: 20180704204006) do add_index "projects", ["path"], name: "index_projects_on_path", using: :btree add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree + add_index "projects", ["repository_storage", "created_at"], name: "idx_project_repository_check_partial", where: "(last_repository_check_at IS NULL)", using: :btree add_index "projects", ["repository_storage"], name: "index_projects_on_repository_storage", using: :btree add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md index 48f3df1925a..692f81dd7cd 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -16,16 +16,8 @@ The default deployment includes: ### Limitations -Some features and functions are not currently available in the beta release: -* [GitLab Pages](../../user/project/pages/) -* [Reply by email](../../administration/reply_by_email.html) -* [Project templates](../../gitlab-basics/create-project.html) -* [Project import/export](../../user/project/settings/import_export.html) -* [Geo](https://docs.gitlab.com/ee/administration/geo/replication/) - -Currently out of scope: -* [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/) -* [MySQL support](https://docs.gitlab.com/omnibus/settings/database.html#using-a-mysql-database-management-server-enterprise-edition-only) +Some features and functions are not currently available in the beta release. +For details, see [known issues and limitations](https://gitlab.com/charts/gitlab/blob/master/doc/architecture/beta.md#known-issues-and-limitations) in the charts repository. ## Prerequisites diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md index c01da883562..b050c83fb72 100644 --- a/doc/user/project/integrations/emails_on_push.md +++ b/doc/user/project/integrations/emails_on_push.md @@ -6,7 +6,7 @@ every change that is pushed to your project. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) and select the **Emails on push** service to configure it. -In the _Recipients_ area, provide a list of emails separated by commas. +In the _Recipients_ area, provide a list of emails separated by spaces or newlines. You can configure any of the following settings depending on your preference. diff --git a/lib/api/entities.rb b/lib/api/entities.rb index b256c33c631..3f3a95ea8e6 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -701,7 +701,7 @@ module API expose :system?, as: :system expose :noteable_id, :noteable_type - expose :position, if: ->(note, options) { note.diff_note? } do |note| + expose :position, if: ->(note, options) { note.is_a?(DiffNote) } do |note| note.position.to_h end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 19c79b6f7b7..39fbf6e2526 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -381,6 +381,21 @@ module Gitlab end end + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1233 + def new_commits(newrev) + gitaly_migrate(:new_commits) do |is_enabled| + if is_enabled + gitaly_ref_client.list_new_commits(newrev) + else + refs = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + rev_list(including: newrev, excluding: :all).split("\n").map(&:strip) + end + + Gitlab::Git::Commit.batch_by_oid(self, refs) + end + end + end + def count_commits(options) options = process_count_commits_options(options.dup) diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb index 9faa62be28e..ef86d4a55ca 100644 --- a/lib/gitlab/git/repository_mirroring.rb +++ b/lib/gitlab/git/repository_mirroring.rb @@ -17,33 +17,19 @@ module Gitlab rugged.config["remote.#{remote_name}.prune"] = true end - def remote_tags(remote) - # Each line has this format: "dc872e9fa6963f8f03da6c8f6f264d0845d6b092\trefs/tags/v1.10.0\n" - # We want to convert it to: [{ 'v1.10.0' => 'dc872e9fa6963f8f03da6c8f6f264d0845d6b092' }, ...] - list_remote_tags(remote).map do |line| - target, path = line.strip.split("\t") - - # When the remote repo does not have tags. - if target.nil? || path.nil? - Rails.logger.info "Empty or invalid list of tags for remote: #{remote}. Output: #{output}" - break [] + def remote_branches(remote_name) + gitaly_migrate(:ref_find_all_remote_branches) do |is_enabled| + if is_enabled + gitaly_ref_client.remote_branches(remote_name) + else + rugged_remote_branches(remote_name) end - - name = path.split('/', 3).last - # We're only interested in tag references - # See: http://stackoverflow.com/questions/15472107/when-listing-git-ls-remote-why-theres-after-the-tag-name - next if name =~ /\^\{\}\Z/ - - target_commit = Gitlab::Git::Commit.find(self, target) - Gitlab::Git::Tag.new(self, { - name: name, - target: target, - target_commit: target_commit - }) - end.compact + end end - def remote_branches(remote_name) + private + + def rugged_remote_branches(remote_name) branches = [] rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref| @@ -60,8 +46,6 @@ module Gitlab branches end - private - def set_remote_refmap(remote_name, refmap) Array(refmap).each_with_index do |refspec, i| refspec = REFMAPS[refspec] || refspec @@ -75,21 +59,6 @@ module Gitlab end end end - - def list_remote_tags(remote) - tag_list, exit_code, error = nil - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-remote --tags #{remote}) - - Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr| - tag_list = stdout.read - error = stderr.read - exit_code = wait_thr.value.exitstatus - end - - raise RemoteError, error unless exit_code.zero? - - tag_list.split("\n") - end end end end diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb deleted file mode 100644 index 2ba68343aa5..00000000000 --- a/lib/gitlab/git/rev_list.rb +++ /dev/null @@ -1,50 +0,0 @@ -module Gitlab - module Git - class RevList - include Gitlab::Git::Popen - - attr_reader :oldrev, :newrev, :repository - - def initialize(repository, newrev:, oldrev: nil) - @oldrev = oldrev - @newrev = newrev - @repository = repository - end - - # This method returns an array of new commit references - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1233 - # - def new_refs - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - repository.rev_list(including: newrev, excluding: :all).split("\n") - end - end - - private - - def execute(args) - repository.rev_list(args).split("\n") - end - - def get_objects(including: [], excluding: [], options: [], require_path: nil) - opts = { including: including, excluding: excluding, options: options, objects: true } - - repository.rev_list(opts) do |lazy_output| - objects = objects_from_output(lazy_output, require_path: require_path) - - yield(objects) - end - end - - def objects_from_output(object_output, require_path: nil) - object_output.map do |output_line| - sha, path = output_line.split(' ', 2) - - next if require_path && path.to_s.empty? - - sha - end.reject(&:nil?) - end - end - end -end diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index 7f4eed9222a..fbe7d4ba1ae 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -17,6 +17,13 @@ module Gitlab consume_find_all_branches_response(response) end + def remote_branches(remote_name) + request = Gitaly::FindAllRemoteBranchesRequest.new(repository: @gitaly_repo, remote_name: remote_name) + response = GitalyClient.call(@repository.storage, :ref_service, :find_all_remote_branches, request) + + consume_find_all_remote_branches_response(remote_name, response) + end + def merged_branches(branch_names = []) request = Gitaly::FindAllBranchesRequest.new( repository: @gitaly_repo, @@ -56,6 +63,25 @@ module Gitlab encode!(response.name.dup) end + def list_new_commits(newrev) + request = Gitaly::ListNewCommitsRequest.new( + repository: @gitaly_repo, + commit_id: newrev + ) + + response = GitalyClient + .call(@storage, :ref_service, :list_new_commits, request, timeout: GitalyClient.medium_timeout) + + commits = [] + response.each do |msg| + msg.commits.each do |c| + commits << Gitlab::Git::Commit.new(@repository, c) + end + end + + commits + end + def count_tag_names tag_names.count end @@ -224,6 +250,18 @@ module Gitlab end end + def consume_find_all_remote_branches_response(remote_name, response) + remote_name += '/' unless remote_name.ends_with?('/') + + response.flat_map do |message| + message.branches.map do |branch| + target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit) + branch_name = branch.name.sub(remote_name, '') + Gitlab::Git::Branch.new(@repository, branch_name, branch.target_commit.id, target_commit) + end + end + end + def consume_tags_response(response) response.flat_map do |message| message.tags.map { |gitaly_tag| Gitlab::Git::Tag.new(@repository, gitaly_tag) } diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb index 4b9cb59eab5..53e5ac02e42 100644 --- a/lib/uploaded_file.rb +++ b/lib/uploaded_file.rb @@ -21,7 +21,7 @@ class UploadedFile raise InvalidPathError, "#{path} file does not exist" unless ::File.exist?(path) @content_type = content_type - @original_filename = filename || ::File.basename(path) + @original_filename = sanitize_filename(filename || path) @content_type = content_type @sha256 = sha256 @remote_id = remote_id @@ -55,6 +55,16 @@ class UploadedFile end end + # copy-pasted from CarrierWave::SanitizedFile + def sanitize_filename(name) + name = name.tr("\\", "/") # work-around for IE + name = ::File.basename(name) + name = name.gsub(CarrierWave::SanitizedFile.sanitize_regexp, "_") + name = "_#{name}" if name =~ /\A\.+\z/ + name = "unnamed" if name.empty? + name.mb_chars.to_s + end + def path @tempfile.path end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 562760552e0..19973f4f321 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1286,6 +1286,9 @@ msgstr "" msgid "ClusterIntegration|Helm Tiller" msgstr "" +msgid "ClusterIntegration|Hide" +msgstr "" + msgid "ClusterIntegration|Ingress" msgstr "" @@ -3523,6 +3526,9 @@ msgstr "" msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token." msgstr "" +msgid "Notes|Are you sure you want to cancel creating this comment?" +msgstr "" + msgid "Notification events" msgstr "" diff --git a/package.json b/package.json index 9dd7d80945f..e1801d4d435 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, "dependencies": { - "@gitlab-org/gitlab-svgs": "^1.25.0", + "@gitlab-org/gitlab-svgs": "^1.26.0", "autosize": "^4.0.0", "axios": "^0.17.1", "babel-core": "^6.26.3", diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index fc753554fc4..3df6db05970 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -59,7 +59,7 @@ module QA end def add_file(name, contents) - File.write(name, contents) + ::File.write(name, contents) `git add #{name}` end diff --git a/qa/qa/page/component/dropzone.rb b/qa/qa/page/component/dropzone.rb index 15bdc742fda..fd44c57123a 100644 --- a/qa/qa/page/component/dropzone.rb +++ b/qa/qa/page/component/dropzone.rb @@ -15,7 +15,7 @@ module QA # instantiated on one page because there is no distinguishing # attribute per dropzone file field. def attach_file(attachment) - filename = File.basename(attachment) + filename = ::File.basename(attachment) field_style = { visibility: 'visible', height: '', width: '' } page.attach_file(attachment, class: 'dz-hidden-input', make_visible: field_style) diff --git a/qa/qa/runtime/api/request.rb b/qa/qa/runtime/api/request.rb index c33ada0de7a..ff9f0004524 100644 --- a/qa/qa/runtime/api/request.rb +++ b/qa/qa/runtime/api/request.rb @@ -28,7 +28,7 @@ module QA # # Returns the relative path to the requested API resource def request_path(path, version: API_VERSION, **query_string) - full_path = File.join('/api', version, path) + full_path = ::File.join('/api', version, path) if query_string.any? full_path << (path.include?('?') ? '&' : '?') diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 877864fb40c..0c8eca5229e 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -86,7 +86,7 @@ module QA end Capybara::Screenshot.register_filename_prefix_formatter(:rspec) do |example| - File.join(QA::Runtime::Namespace.name, example.file_path.sub('./qa/specs/features/', '')) + ::File.join(QA::Runtime::Namespace.name, example.file_path.sub('./qa/specs/features/', '')) end Capybara.configure do |config| @@ -94,7 +94,7 @@ module QA config.javascript_driver = :chrome config.default_max_wait_time = 10 # https://github.com/mattheworiordan/capybara-screenshot/issues/164 - config.save_path = File.expand_path('../../tmp', __dir__) + config.save_path = ::File.expand_path('../../tmp', __dir__) end end diff --git a/qa/qa/runtime/key/base.rb b/qa/qa/runtime/key/base.rb index c7e5ebada7b..67a992e2115 100644 --- a/qa/qa/runtime/key/base.rb +++ b/qa/qa/runtime/key/base.rb @@ -25,8 +25,8 @@ module QA end def populate_key_data(path) - @private_key = File.binread(path) - @public_key = File.binread("#{path}.pub") + @private_key = ::File.binread(path) + @public_key = ::File.binread("#{path}.pub") @fingerprint = `ssh-keygen -l -E md5 -f #{path} | cut -d' ' -f2 | cut -d: -f2-`.chomp end diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb index 4f83a773645..b1f7ec482c8 100644 --- a/qa/qa/runtime/release.rb +++ b/qa/qa/runtime/release.rb @@ -13,7 +13,7 @@ module QA end def version - @version ||= File.directory?("#{__dir__}/../ee") ? :EE : :CE + @version ||= ::File.directory?("#{__dir__}/../ee") ? :EE : :CE end def strategy diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb index 567e5fd6cca..46eb2dabb11 100644 --- a/qa/qa/scenario/test/instance.rb +++ b/qa/qa/scenario/test/instance.rb @@ -26,7 +26,7 @@ module QA if rspec_options.any? rspec_options else - File.expand_path('../../specs/features', __dir__) + ::File.expand_path('../../specs/features', __dir__) end end end diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb index ee1f08da238..5c65128d10c 100644 --- a/qa/spec/git/repository_spec.rb +++ b/qa/spec/git/repository_spec.rb @@ -29,7 +29,7 @@ describe QA::Git::Repository do def cd_empty_temp_directory tmp_dir = 'tmp/git-repository-spec/' - FileUtils.rm_r(tmp_dir) if File.exist?(tmp_dir) + FileUtils.rm_r(tmp_dir) if ::File.exist?(tmp_dir) FileUtils.mkdir_p tmp_dir FileUtils.cd tmp_dir end diff --git a/qa/spec/page/view_spec.rb b/qa/spec/page/view_spec.rb index aedbc3863a7..34d2ff11447 100644 --- a/qa/spec/page/view_spec.rb +++ b/qa/spec/page/view_spec.rb @@ -32,7 +32,7 @@ describe QA::Page::View do context 'when pattern is found' do before do - allow(File).to receive(:foreach) + allow(::File).to receive(:foreach) .and_yield('some element').once allow(element).to receive(:matches?) .with('some element').and_return(true) @@ -45,7 +45,7 @@ describe QA::Page::View do context 'when pattern has not been found' do before do - allow(File).to receive(:foreach) + allow(::File).to receive(:foreach) .and_yield('some element').once allow(element).to receive(:matches?) .with('some element').and_return(false) diff --git a/qa/spec/scenario/test/instance_spec.rb b/qa/spec/scenario/test/instance_spec.rb index a74a9538be8..0d0b534911f 100644 --- a/qa/spec/scenario/test/instance_spec.rb +++ b/qa/spec/scenario/test/instance_spec.rb @@ -30,7 +30,7 @@ describe QA::Scenario::Test::Instance do subject.perform("test") expect(runner).to have_received(:options=) - .with(File.expand_path('../../../qa/specs/features', __dir__)) + .with(::File.expand_path('../../../qa/specs/features', __dir__)) end end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index c2c6cf95406..8e6613cd688 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -1,6 +1,6 @@ require_relative '../qa' -Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f } +Dir[::File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f } RSpec.configure do |config| config.expect_with :rspec do |expectations| diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 74f362fd7fc..f1165c73847 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -89,6 +89,32 @@ describe ApplicationController do end end + describe 'session expiration' do + controller(described_class) do + def index + render text: 'authenticated' + end + end + + context 'authenticated user' do + it 'does not set the expire_after option' do + sign_in(create(:user)) + + get :index + + expect(request.env['rack.session.options'][:expire_after]).to be_nil + end + end + + context 'unauthenticated user' do + it 'sets the expire_after option' do + get :index + + expect(request.env['rack.session.options'][:expire_after]).to eq(Settings.gitlab['unauthenticated_session_expire_delay']) + end + end + end + describe 'rescue from Gitlab::Git::Storage::Inaccessible' do controller(described_class) do def index diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 9fdc3e616a6..6844ed8aa4a 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -39,6 +39,7 @@ FactoryBot.define do factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote do association :project, :repository + position '' end factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb index 93d8e672f8c..82f588d1a08 100644 --- a/spec/helpers/icons_helper_spec.rb +++ b/spec/helpers/icons_helper_spec.rb @@ -55,6 +55,29 @@ describe IconsHelper do expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s) .to eq "<svg class=\"s72 icon-danger\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>" end + + describe 'non existing icon' do + non_existing = 'non_existing_icon_sprite' + + it 'should raise in development mode' do + allow(Rails.env).to receive(:development?).and_return(true) + + expect { sprite_icon(non_existing) }.to raise_error(ArgumentError, /is not a known icon/) + end + + it 'should raise in test mode' do + allow(Rails.env).to receive(:test?).and_return(true) + + expect { sprite_icon(non_existing) }.to raise_error(ArgumentError, /is not a known icon/) + end + + it 'should not raise in production mode' do + allow(Rails.env).to receive(:test?).and_return(false) + allow(Rails.env).to receive(:development?).and_return(false) + + expect { sprite_icon(non_existing) }.not_to raise_error + end + end end describe 'file_type_icon_class' do diff --git a/spec/javascripts/autosave_spec.js b/spec/javascripts/autosave_spec.js index 38ae5b7e00c..dcb1c781591 100644 --- a/spec/javascripts/autosave_spec.js +++ b/spec/javascripts/autosave_spec.js @@ -59,12 +59,10 @@ describe('Autosave', () => { Autosave.prototype.restore.call(autosave); - expect( - field.trigger, - ).toHaveBeenCalled(); + expect(field.trigger).toHaveBeenCalled(); }); - it('triggers native event', (done) => { + it('triggers native event', done => { autosave.field.get(0).addEventListener('change', () => { done(); }); @@ -81,9 +79,7 @@ describe('Autosave', () => { it('does not trigger event', () => { spyOn(field, 'trigger').and.callThrough(); - expect( - field.trigger, - ).not.toHaveBeenCalled(); + expect(field.trigger).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js index abe2954d506..839b8a06b48 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/javascripts/clusters/clusters_bundle_spec.js @@ -45,17 +45,33 @@ describe('Clusters', () => { }); describe('showToken', () => { - it('should update tye field type', () => { + it('should update token field type', () => { cluster.showTokenButton.click(); + expect( cluster.tokenField.getAttribute('type'), ).toEqual('text'); cluster.showTokenButton.click(); + expect( cluster.tokenField.getAttribute('type'), ).toEqual('password'); }); + + it('should update show token button text', () => { + cluster.showTokenButton.click(); + + expect( + cluster.showTokenButton.textContent, + ).toEqual('Hide'); + + cluster.showTokenButton.click(); + + expect( + cluster.showTokenButton.textContent, + ).toEqual('Show'); + }); }); describe('checkForNewInstalls', () => { diff --git a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js index 2d136a63c52..cb85d12daf2 100644 --- a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js +++ b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js @@ -18,10 +18,12 @@ describe('DiffLineGutterContent', () => { }; const setDiscussions = component => { component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); + component.$store.commit('diffs/SET_DIFF_DATA', { diffFiles: [getDiffFileMock()] }); }; const resetDiscussions = component => { component.$store.dispatch('setInitialNotes', []); + component.$store.commit('diffs/SET_DIFF_DATA', {}); }; describe('computed', () => { diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js index 4600aaea70b..6fe5fdaf7f9 100644 --- a/spec/javascripts/diffs/components/diff_line_note_form_spec.js +++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js @@ -3,6 +3,7 @@ import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue'; import store from '~/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffFileMockData from '../mock_data/diff_file'; +import { noteableDataMock } from '../../notes/mock_data'; describe('DiffLineNoteForm', () => { let component; @@ -21,10 +22,9 @@ describe('DiffLineNoteForm', () => { noteTargetLine: diffLines[0], }); - Object.defineProperty(component, 'isLoggedIn', { - get() { - return true; - }, + Object.defineProperties(component, { + noteableData: { value: noteableDataMock }, + isLoggedIn: { value: true }, }); component.$mount(); @@ -32,12 +32,37 @@ describe('DiffLineNoteForm', () => { describe('methods', () => { describe('handleCancelCommentForm', () => { - it('should call cancelCommentForm with lineCode', () => { + it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => { + spyOn(window, 'confirm').and.returnValue(false); + + component.handleCancelCommentForm(true, true); + expect(window.confirm).toHaveBeenCalled(); + }); + + it('should ask for confirmation when one of the params false', () => { + spyOn(window, 'confirm').and.returnValue(false); + + component.handleCancelCommentForm(true, false); + expect(window.confirm).not.toHaveBeenCalled(); + + component.handleCancelCommentForm(false, true); + expect(window.confirm).not.toHaveBeenCalled(); + }); + + it('should call cancelCommentForm with lineCode', done => { + spyOn(window, 'confirm'); spyOn(component, 'cancelCommentForm'); + spyOn(component, 'resetAutoSave'); component.handleCancelCommentForm(); - expect(component.cancelCommentForm).toHaveBeenCalledWith({ - lineCode: diffLines[0].lineCode, + expect(window.confirm).not.toHaveBeenCalled(); + component.$nextTick(() => { + expect(component.cancelCommentForm).toHaveBeenCalledWith({ + lineCode: diffLines[0].lineCode, + }); + expect(component.resetAutoSave).toHaveBeenCalled(); + + done(); }); }); }); @@ -66,7 +91,7 @@ describe('DiffLineNoteForm', () => { describe('mounted', () => { it('should init autosave', () => { - const key = 'autosave/Note/issue///DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1'; + const key = 'autosave/Note/Issue/98//DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1'; expect(component.autosave).toBeDefined(); expect(component.autosave.key).toEqual(key); diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js index b02328dd359..90dfa5c5a58 100644 --- a/spec/javascripts/diffs/components/inline_diff_view_spec.js +++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js @@ -33,6 +33,7 @@ describe('InlineDiffView', () => { it('should render discussions', done => { const el = component.$el; component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); + component.$store.commit('diffs/SET_DIFF_DATA', { diffFiles: [getDiffFileMock()] }); Vue.nextTick(() => { expect(el.querySelectorAll('.notes_holder').length).toEqual(1); diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js index 41d0dfd8939..8cd57d2248b 100644 --- a/spec/javascripts/diffs/mock_data/diff_discussions.js +++ b/spec/javascripts/diffs/mock_data/diff_discussions.js @@ -12,6 +12,17 @@ export default { head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', }, }, + original_position: { + formatter: { + old_line: null, + new_line: 2, + old_path: 'CHANGELOG', + new_path: 'CHANGELOG', + base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a', + start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962', + head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + }, + }, line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', expanded: true, notes: [ diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js index 6210d4a7124..f5628a01a55 100644 --- a/spec/javascripts/diffs/store/getters_spec.js +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -2,6 +2,7 @@ import * as getters from '~/diffs/store/getters'; import state from '~/diffs/store/modules/diff_state'; import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; import discussion from '../mock_data/diff_discussions'; +import diffFile from '../mock_data/diff_file'; describe('Diffs Module Getters', () => { let localState; @@ -203,4 +204,38 @@ describe('Diffs Module Getters', () => { expect(getters.getDiffFileByHash(localState)('123')).toBeUndefined(); }); }); + + describe('discussionsByLineCode', () => { + let mockState; + + beforeEach(() => { + mockState = { diffFiles: [diffFile] }; + }); + + it('should return a map of diff lines with their line codes', () => { + const mockGetters = { discussions: [discussionMock] }; + + const map = getters.discussionsByLineCode(mockState, {}, {}, mockGetters); + expect(map['1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2']).toBeDefined(); + expect(Object.keys(map).length).toEqual(1); + }); + + it('should have the diff discussion on the map if the original position matches', () => { + discussionMock.position.formatter.base_sha = 'ff9200'; + const mockGetters = { discussions: [discussionMock] }; + + const map = getters.discussionsByLineCode(mockState, {}, {}, mockGetters); + expect(map['1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2']).toBeDefined(); + expect(Object.keys(map).length).toEqual(1); + }); + + it('should not add an outdated diff discussion to the returned map', () => { + discussionMock.position.formatter.base_sha = 'ff9200'; + discussionMock.original_position.formatter.base_sha = 'ff9200'; + const mockGetters = { discussions: [discussionMock] }; + + const map = getters.discussionsByLineCode(mockState, {}, {}, mockGetters); + expect(Object.keys(map).length).toEqual(0); + }); + }); }); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index 32136d9ebff..8e7bd8afca4 100644 --- a/spec/javascripts/diffs/store/utils_spec.js +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -207,4 +207,24 @@ describe('DiffsStoreUtils', () => { expect(utils.trimFirstCharOfLineContent()).toEqual({}); }); }); + + describe('getDiffRefsByLineCode', () => { + it('should return diffRefs for all highlightedDiffLines', () => { + const diffFile = getDiffFileMock(); + const map = utils.getDiffRefsByLineCode([diffFile]); + const { highlightedDiffLines } = diffFile; + const lineCodeCount = highlightedDiffLines.reduce( + (acc, line) => (line.lineCode ? acc + 1 : acc), + 0, + ); + + const { baseSha, headSha, startSha } = diffFile.diffRefs; + const targetLine = map[highlightedDiffLines[4].lineCode]; + + expect(Object.keys(map).length).toEqual(lineCodeCount); + expect(targetLine.baseSha).toEqual(baseSha); + expect(targetLine.headSha).toEqual(headSha); + expect(targetLine.startSha).toEqual(startSha); + }); + }); }); diff --git a/spec/javascripts/ide/components/ide_status_bar_spec.js b/spec/javascripts/ide/components/ide_status_bar_spec.js index 9895682f388..0e93c5193a1 100644 --- a/spec/javascripts/ide/components/ide_status_bar_spec.js +++ b/spec/javascripts/ide/components/ide_status_bar_spec.js @@ -70,7 +70,7 @@ describe('ideStatusBar', () => { status: { text: 'success', details_path: 'test', - icon: 'success', + icon: 'status_success', }, }, }); diff --git a/spec/javascripts/ide/components/jobs/detail/description_spec.js b/spec/javascripts/ide/components/jobs/detail/description_spec.js index 9b715a41499..babae00d2f7 100644 --- a/spec/javascripts/ide/components/jobs/detail/description_spec.js +++ b/spec/javascripts/ide/components/jobs/detail/description_spec.js @@ -23,6 +23,6 @@ describe('IDE job description', () => { }); it('renders CI icon', () => { - expect(vm.$el.querySelector('.ci-status-icon .ic-status_passed_borderless')).not.toBe(null); + expect(vm.$el.querySelector('.ci-status-icon .ic-status_success_borderless')).not.toBe(null); }); }); diff --git a/spec/javascripts/ide/components/jobs/item_spec.js b/spec/javascripts/ide/components/jobs/item_spec.js index 79e07f00e7b..2f97d39e98e 100644 --- a/spec/javascripts/ide/components/jobs/item_spec.js +++ b/spec/javascripts/ide/components/jobs/item_spec.js @@ -24,7 +24,7 @@ describe('IDE jobs item', () => { }); it('renders CI icon', () => { - expect(vm.$el.querySelector('.ic-status_passed_borderless')).not.toBe(null); + expect(vm.$el.querySelector('.ic-status_success_borderless')).not.toBe(null); }); it('does not render view logs button if not started', done => { diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js index 4bbda4c8e80..7be450a0df7 100644 --- a/spec/javascripts/ide/mock_data.js +++ b/spec/javascripts/ide/mock_data.js @@ -74,7 +74,7 @@ export const jobs = [ name: 'test', path: 'testing', status: { - icon: 'status_passed', + icon: 'status_success', text: 'passed', }, stage: 'test', @@ -86,7 +86,7 @@ export const jobs = [ name: 'test 2', path: 'testing2', status: { - icon: 'status_passed', + icon: 'status_success', text: 'passed', }, stage: 'test', @@ -98,7 +98,7 @@ export const jobs = [ name: 'test 3', path: 'testing3', status: { - icon: 'status_passed', + icon: 'status_success', text: 'passed', }, stage: 'test', @@ -146,7 +146,7 @@ export const fullPipelinesResponse = { }, details: { status: { - icon: 'status_passed', + icon: 'status_success', text: 'passed', }, stages: [...stages], diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js index cef30a007db..e21e2c6d6e3 100644 --- a/spec/javascripts/jobs/header_spec.js +++ b/spec/javascripts/jobs/header_spec.js @@ -20,7 +20,7 @@ describe('Job details header', () => { job: { status: { group: 'failed', - icon: 'ci-status-failed', + icon: 'status_failed', label: 'failed', text: 'failed', details_path: 'path', diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index dd025255bd1..8fdd9b309b7 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -14,7 +14,7 @@ export default { finished_at: threeWeeksAgo.toISOString(), queued: 9.54, status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', @@ -72,7 +72,7 @@ export default { }, details: { status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index 7da931fd9cb..f3f50aed232 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -46,10 +46,15 @@ describe('noteable_discussion component', () => { it('should toggle reply form', done => { vm.$el.querySelector('.js-vue-discussion-reply').click(); + Vue.nextTick(() => { - expect(vm.$refs.noteForm).not.toBeNull(); expect(vm.isReplying).toEqual(true); - done(); + + // There is a watcher for `isReplying` which will init autosave in the next tick + Vue.nextTick(() => { + expect(vm.$refs.noteForm).not.toBeNull(); + done(); + }); }); }); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index 9c55a19ebc7..56476253ad0 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -10,7 +10,7 @@ describe('pipeline graph job component', () => { id: 4256, name: 'test', status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', tooltip: 'passed', @@ -65,7 +65,7 @@ describe('pipeline graph job component', () => { id: 4257, name: 'test', status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', @@ -111,7 +111,7 @@ describe('pipeline graph job component', () => { id: 4258, name: 'test', status: { - icon: 'icon_status_success', + icon: 'status_success', }, }, }); @@ -125,7 +125,7 @@ describe('pipeline graph job component', () => { id: 4259, name: 'test', status: { - icon: 'icon_status_success', + icon: 'status_success', label: 'success', tooltip: 'success', }, diff --git a/spec/javascripts/pipelines/graph/job_name_component_spec.js b/spec/javascripts/pipelines/graph/job_name_component_spec.js index 8e2071ba0b3..c861d452dd0 100644 --- a/spec/javascripts/pipelines/graph/job_name_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_name_component_spec.js @@ -10,7 +10,7 @@ describe('job name component', () => { propsData: { name: 'foo', status: { - icon: 'icon_status_success', + icon: 'status_success', }, }, }).$mount(); diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js index 9e25a4b3fed..b2161d54bce 100644 --- a/spec/javascripts/pipelines/graph/mock_data.js +++ b/spec/javascripts/pipelines/graph/mock_data.js @@ -13,7 +13,7 @@ export default { path: '/root/ci-mock/pipelines/123', details: { status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', @@ -33,7 +33,7 @@ export default { name: 'test', size: 1, status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', @@ -58,7 +58,7 @@ export default { created_at: '2017-04-13T09:25:18.959Z', updated_at: '2017-04-13T09:25:23.118Z', status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', @@ -78,7 +78,7 @@ export default { }, ], status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', @@ -98,7 +98,7 @@ export default { name: 'deploy to production', size: 1, status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', @@ -123,7 +123,7 @@ export default { created_at: '2017-04-19T14:29:46.463Z', updated_at: '2017-04-19T14:30:27.498Z', status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', @@ -145,7 +145,7 @@ export default { name: 'deploy to staging', size: 1, status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', @@ -170,7 +170,7 @@ export default { created_at: '2017-04-18T16:32:08.420Z', updated_at: '2017-04-18T16:32:12.631Z', status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', @@ -190,7 +190,7 @@ export default { }, ], status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js index f744f1af5e6..9d1e71fd117 100644 --- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js +++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js @@ -7,7 +7,7 @@ describe('stage column component', () => { id: 4250, name: 'test', status: { - icon: 'icon_status_success', + icon: 'status_success', text: 'passed', label: 'passed', group: 'success', diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js index cecc7ceb53d..034d3b4957d 100644 --- a/spec/javascripts/pipelines/header_component_spec.js +++ b/spec/javascripts/pipelines/header_component_spec.js @@ -18,7 +18,7 @@ describe('Pipeline details header', () => { details: { status: { group: 'failed', - icon: 'ci-status-failed', + icon: 'status_failed', label: 'failed', text: 'failed', details_path: 'path', diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index 16f6db39d6a..3f6789759ae 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -20,7 +20,7 @@ describe('Pipelines stage component', () => { stage: { status: { group: 'success', - icon: 'icon_status_success', + icon: 'status_success', title: 'success', }, dropdown_path: 'path.json', @@ -84,7 +84,7 @@ describe('Pipelines stage component', () => { component.stage = { status: { group: 'running', - icon: 'running', + icon: 'status_running', title: 'running', }, dropdown_path: 'bar.json', diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index c0b5a7d4455..7fd1a2350f7 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -76,7 +76,7 @@ export default { path: '/root/acets-app/pipelines/172', details: { status: { - icon: 'icon_status_success', + icon: 'status_success', favicon: 'favicon_status_success', text: 'passed', label: 'passed', @@ -91,7 +91,7 @@ export default { name: 'build', title: 'build: failed', status: { - icon: 'icon_status_failed', + icon: 'status_failed', favicon: 'favicon_status_failed', text: 'failed', label: 'failed', @@ -106,7 +106,7 @@ export default { name: 'review', title: 'review: skipped', status: { - icon: 'icon_status_skipped', + icon: 'status_skipped', favicon: 'favicon_status_skipped', text: 'skipped', label: 'skipped', diff --git a/spec/javascripts/vue_shared/components/ci_icon_spec.js b/spec/javascripts/vue_shared/components/ci_icon_spec.js index 423bc746a22..b59a7d7544f 100644 --- a/spec/javascripts/vue_shared/components/ci_icon_spec.js +++ b/spec/javascripts/vue_shared/components/ci_icon_spec.js @@ -13,7 +13,7 @@ describe('CI Icon component', () => { it('should render a span element with an svg', () => { vm = mountComponent(Component, { status: { - icon: 'icon_status_success', + icon: 'status_success', }, }); @@ -24,7 +24,7 @@ describe('CI Icon component', () => { it('should render a success status', () => { vm = mountComponent(Component, { status: { - icon: 'icon_status_success', + icon: 'status_success', group: 'success', }, }); @@ -35,7 +35,7 @@ describe('CI Icon component', () => { it('should render a failed status', () => { vm = mountComponent(Component, { status: { - icon: 'icon_status_failed', + icon: 'status_failed', group: 'failed', }, }); @@ -46,7 +46,7 @@ describe('CI Icon component', () => { it('should render success with warnings status', () => { vm = mountComponent(Component, { status: { - icon: 'icon_status_warning', + icon: 'status_warning', group: 'warning', }, }); @@ -57,7 +57,7 @@ describe('CI Icon component', () => { it('should render pending status', () => { vm = mountComponent(Component, { status: { - icon: 'icon_status_pending', + icon: 'status_pending', group: 'pending', }, }); @@ -68,7 +68,7 @@ describe('CI Icon component', () => { it('should render running status', () => { vm = mountComponent(Component, { status: { - icon: 'icon_status_running', + icon: 'status_running', group: 'running', }, }); @@ -79,7 +79,7 @@ describe('CI Icon component', () => { it('should render created status', () => { vm = mountComponent(Component, { status: { - icon: 'icon_status_created', + icon: 'status_created', group: 'created', }, }); @@ -90,7 +90,7 @@ describe('CI Icon component', () => { it('should render skipped status', () => { vm = mountComponent(Component, { status: { - icon: 'icon_status_skipped', + icon: 'status_skipped', group: 'skipped', }, }); @@ -101,7 +101,7 @@ describe('CI Icon component', () => { it('should render canceled status', () => { vm = mountComponent(Component, { status: { - icon: 'icon_status_canceled', + icon: 'status_canceled', group: 'canceled', }, }); @@ -112,7 +112,7 @@ describe('CI Icon component', () => { it('should render status for manual action', () => { vm = mountComponent(Component, { status: { - icon: 'icon_status_manual', + icon: 'status_manual', group: 'manual', }, }); diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index 65499a2d730..f17818c17c7 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -12,7 +12,7 @@ describe('Header CI Component', () => { props = { status: { group: 'failed', - icon: 'ci-status-failed', + icon: 'status_failed', label: 'failed', text: 'failed', details_path: 'path', diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js index cc030e29d61..882420e602e 100644 --- a/spec/javascripts/vue_shared/components/icon_spec.js +++ b/spec/javascripts/vue_shared/components/icon_spec.js @@ -10,7 +10,7 @@ describe('Sprite Icon Component', function () { const IconComponent = Vue.extend(Icon); icon = mountComponent(IconComponent, { - name: 'test', + name: 'commit', size: 32, cssClasses: 'extraclasses', }); @@ -30,7 +30,7 @@ describe('Sprite Icon Component', function () { it('should have <use> as a child element with the correct href', function () { expect(icon.$el.firstChild.tagName).toBe('use'); - expect(icon.$el.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_icons}#test`); + expect(icon.$el.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_icons}#commit`); }); it('should properly compute iconSizeClass', function () { @@ -50,5 +50,13 @@ describe('Sprite Icon Component', function () { expect(containsSizeClass).toBe(true); expect(containsCustomClass).toBe(true); }); + + it('`name` validator should return false for non existing icons', () => { + expect(Icon.props.name.validator('non_existing_icon_sprite')).toBe(false); + }); + + it('`name` validator should return false for existing icons', () => { + expect(Icon.props.name.validator('commit')).toBe(true); + }); }); }); diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js index aa4c9c4c88c..2a6015fe35f 100644 --- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js +++ b/spec/javascripts/vue_shared/components/notes/system_note_spec.js @@ -19,7 +19,7 @@ describe('system note component', () => { path: '/root', }, note_html: '<p dir="auto">closed</p>', - system_note_icon_name: 'icon_status_closed', + system_note_icon_name: 'status_closed', created_at: '2017-08-02T10:51:58.559Z', }, }; diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 0365c3b20ef..ee385226e65 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -602,40 +602,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#remote_tags' do - let(:remote_name) { 'upstream' } - let(:target_commit_id) { SeedRepo::Commit::ID } - let(:tag_name) { 'v0.0.1' } - let(:tag_message) { 'My tag' } - let(:remote_repository) do - Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - end - - around do |example| - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - subject { repository.remote_tags(remote_name) } - - before do - remote_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { remote_repository.path } - repository.add_remote(remote_name, remote_repository_path) - remote_repository.add_tag(tag_name, user: user, target: target_commit_id) - end - - after do - ensure_seeds - end - - it 'gets the remote tags' do - expect(subject.first).to be_an_instance_of(Gitlab::Git::Tag) - expect(subject.first.name).to eq(tag_name) - expect(subject.first.dereferenced_target.id).to eq(target_commit_id) - end - end - describe "#log" do shared_examples 'repository log' do let(:commit_with_old_name) do diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb deleted file mode 100644 index d9d405e1ccc..00000000000 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::RevList do - let(:repository) { create(:project, :repository).repository.raw } - let(:rev_list) { described_class.new(repository, newrev: 'newrev') } - - def args_for_popen(args_list) - [Gitlab.config.git.bin_path, 'rev-list', *args_list] - end - - def stub_popen_rev_list(*additional_args, with_lazy_block: true, output:) - repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository.path } - - params = [ - args_for_popen(additional_args), - repo_path, - {}, - hash_including(lazy_block: with_lazy_block ? anything : nil) - ] - - expect(repository).to receive(:popen).with(*params) do |*_, lazy_block:| - output = lazy_block.call(output.lines.lazy.map(&:chomp)) if with_lazy_block - - [output, 0] - end - end - - context "#new_refs" do - it 'calls out to `popen`' do - stub_popen_rev_list('newrev', '--not', '--all', with_lazy_block: false, output: "sha1\nsha2") - - expect(rev_list.new_refs).to eq(%w[sha1 sha2]) - end - end -end diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 257e4c50f2d..400d426c949 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -18,6 +18,44 @@ describe Gitlab::GitalyClient::RefService do end end + describe '#remote_branches' do + let(:remote_name) { 'my_remote' } + subject { client.remote_branches(remote_name) } + + it 'sends a find_all_remote_branches message' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_all_remote_branches) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return([]) + + subject + end + + it 'concantes and returns the response branches as Gitlab::Git::Branch objects' do + target_commits = create_list(:gitaly_commit, 4) + response_branches = target_commits.each_with_index.map do |gitaly_commit, i| + Gitaly::Branch.new(name: "#{remote_name}/#{i}", target_commit: gitaly_commit) + end + response = [ + Gitaly::FindAllRemoteBranchesResponse.new(branches: response_branches[0, 2]), + Gitaly::FindAllRemoteBranchesResponse.new(branches: response_branches[2, 2]) + ] + + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_all_remote_branches).and_return(response) + + expect(subject.length).to be(response_branches.length) + + response_branches.each_with_index do |gitaly_branch, i| + branch = subject[i] + commit = Gitlab::Git::Commit.new(repository, gitaly_branch.target_commit) + + expect(branch.name).to eq(i.to_s) # It removes the `remote/` prefix + expect(branch.dereferenced_target).to eq(commit) + end + end + end + describe '#branch_names' do it 'sends a find_all_branch_names message' do expect_any_instance_of(Gitaly::RefService::Stub) diff --git a/spec/lib/uploaded_file_spec.rb b/spec/lib/uploaded_file_spec.rb index cc99e7e8911..a2f5c2e7121 100644 --- a/spec/lib/uploaded_file_spec.rb +++ b/spec/lib/uploaded_file_spec.rb @@ -1,24 +1,28 @@ require 'spec_helper' describe UploadedFile do - describe ".from_params" do - let(:temp_dir) { Dir.tmpdir } - let(:temp_file) { Tempfile.new("test", temp_dir) } - let(:upload_path) { nil } + let(:temp_dir) { Dir.tmpdir } + let(:temp_file) { Tempfile.new("test", temp_dir) } - subject do - described_class.from_params(params, :file, upload_path) - end + before do + FileUtils.touch(temp_file) + end - before do - FileUtils.touch(temp_file) - end + after do + FileUtils.rm_f(temp_file) + end + + describe ".from_params" do + let(:upload_path) { nil } after do - FileUtils.rm_f(temp_file) FileUtils.rm_r(upload_path) if upload_path end + subject do + described_class.from_params(params, :file, upload_path) + end + context 'when valid file is specified' do context 'only local path is specified' do let(:params) do @@ -37,7 +41,7 @@ describe UploadedFile do context 'all parameters are specified' do let(:params) do { 'file.path' => temp_file.path, - 'file.name' => 'my_file.txt', + 'file.name' => 'dir/my file&.txt', 'file.type' => 'my/type', 'file.sha256' => 'sha256', 'file.remote_id' => 'remote_id' } @@ -48,7 +52,7 @@ describe UploadedFile do end it "generates filename from path" do - expect(subject.original_filename).to eq('my_file.txt') + expect(subject.original_filename).to eq('my_file_.txt') expect(subject.content_type).to eq('my/type') expect(subject.sha256).to eq('sha256') expect(subject.remote_id).to eq('remote_id') @@ -113,4 +117,11 @@ describe UploadedFile do end end end + + describe '#sanitize_filename' do + it { expect(described_class.new(temp_file.path).sanitize_filename('spaced name')).to eq('spaced_name') } + it { expect(described_class.new(temp_file.path).sanitize_filename('#$%^&')).to eq('_____') } + it { expect(described_class.new(temp_file.path).sanitize_filename('..')).to eq('_..') } + it { expect(described_class.new(temp_file.path).sanitize_filename('')).to eq('unnamed') } + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 02d31098cfd..5d64602ca56 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -296,24 +296,40 @@ describe Repository do end describe '#new_commits' do - let(:new_refs) do - double(:git_rev_list, new_refs: %w[ - c1acaa58bbcbc3eafe538cb8274ba387047b69f8 - 5937ac0a7beb003549fc5fd26fc247adbce4a52e - ]) - end + shared_examples 'finding unreferenced commits' do + set(:project) { create(:project, :repository) } + let(:repository) { project.repository } - it 'delegates to Gitlab::Git::RevList' do - expect(Gitlab::Git::RevList).to receive(:new).with( - repository.raw, - newrev: 'aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj').and_return(new_refs) + subject { repository.new_commits(rev) } - commits = repository.new_commits('aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj') + context 'when there are no new commits' do + let(:rev) { repository.commit.id } - expect(commits).to eq([ - repository.commit('c1acaa58bbcbc3eafe538cb8274ba387047b69f8'), - repository.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e') - ]) + it 'returns an empty array' do + expect(subject).to eq([]) + end + end + + context 'when new commits are found' do + let(:branch) { 'orphaned-branch' } + let!(:rev) { repository.commit(branch).id } + + it 'returns the commits' do + repository.delete_branch(branch) + + expect(subject).not_to be_empty + expect(subject).to all( be_a(::Commit) ) + expect(subject.size).to eq(1) + end + end + end + + context 'when Gitaly handles the request' do + it_behaves_like 'finding unreferenced commits' + end + + context 'when Gitaly is disabled', :disable_gitaly do + it_behaves_like 'finding unreferenced commits' end end @@ -2209,20 +2225,6 @@ describe Repository do end end - describe '#remote_branches' do - it 'returns the remote branches' do - masterrev = repository.find_branch('master').dereferenced_target - create_remote_branch('joe', 'remote_branch', masterrev) - repository.add_branch(user, 'local_branch', masterrev.id) - - # TODO: move this test to gitaly https://gitlab.com/gitlab-org/gitaly/issues/1243 - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - expect(repository.remote_branches('joe').any? { |branch| branch.name == 'local_branch' }).to eq(false) - expect(repository.remote_branches('joe').any? { |branch| branch.name == 'remote_branch' }).to eq(true) - end - end - end - describe '#commit_count' do context 'with a non-existing repository' do it 'returns 0' do diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index 3efe512a59c..7e24efda5dd 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -166,4 +166,50 @@ describe FileUploader do uploader.upload = upload end end + + describe '#cache!' do + subject do + uploader.store!(uploaded_file) + end + + context 'when remote file is used' do + let(:temp_file) { Tempfile.new("test") } + + let!(:fog_connection) do + stub_uploads_object_storage(described_class) + end + + let(:uploaded_file) do + UploadedFile.new(temp_file.path, filename: "my file.txt", remote_id: "test/123123") + end + + let!(:fog_file) do + fog_connection.directories.get('uploads').files.create( + key: 'tmp/uploads/test/123123', + body: 'content' + ) + end + + before do + FileUtils.touch(temp_file) + end + + after do + FileUtils.rm_f(temp_file) + end + + it 'file is stored remotely in permament location with sanitized name' do + subject + + expect(uploader).to be_exists + expect(uploader).not_to be_cached + expect(uploader).not_to be_file_storage + expect(uploader.path).not_to be_nil + expect(uploader.path).not_to include('tmp/upload') + expect(uploader.path).not_to include('tmp/cache') + expect(uploader.url).to include('/my_file.txt') + expect(uploader.object_store).to eq(described_class::Store::REMOTE) + end + end + end end diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index 318aad4bc1e..f17c5ac6aac 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -100,10 +100,6 @@ describe EmailsOnPushWorker, :mailer do end context "when there are multiple recipients" do - let(:recipients) do - 1.upto(5).map { |i| user.email.sub('@', "+#{i}@") }.join("\n") - end - before do # This is a hack because we modify the mail object before sending, for efficency, # but the TestMailer adapter just appends the objects to an array. To clone a mail @@ -114,16 +110,57 @@ describe EmailsOnPushWorker, :mailer do end end - it "sends the mail to each of the recipients" do - perform - expect(ActionMailer::Base.deliveries.count).to eq(5) - expect(ActionMailer::Base.deliveries.map(&:to).flatten).to contain_exactly(*recipients.split) + context "when the recipient addresses are a list of email addresses" do + let(:recipients) do + 1.upto(5).map { |i| user.email.sub('@', "+#{i}@") }.join("\n") + end + + it "sends the mail to each of the recipients" do + perform + + expect(ActionMailer::Base.deliveries.count).to eq(5) + expect(email_recipients).to contain_exactly(*recipients.split) + end + + it "only generates the mail once" do + expect(Notify).to receive(:repository_push_email).once.and_call_original + expect(Premailer::Rails::CustomizedPremailer).to receive(:new).once.and_call_original + + perform + end end - it "only generates the mail once" do - expect(Notify).to receive(:repository_push_email).once.and_call_original - expect(Premailer::Rails::CustomizedPremailer).to receive(:new).once.and_call_original - perform + context "when the recipient addresses contains angle brackets and are separated by spaces" do + let(:recipients) { "John Doe <johndoe@example.com> Jane Doe <janedoe@example.com>" } + + it "accepts emails separated by whitespace" do + perform + + expect(ActionMailer::Base.deliveries.count).to eq(2) + expect(email_recipients).to contain_exactly("johndoe@example.com", "janedoe@example.com") + end + end + + context "when the recipient addresses contain a mix of emails with and without angle brackets" do + let(:recipients) { "johndoe@example.com Jane Doe <janedoe@example.com>" } + + it "accepts both kind of emails" do + perform + + expect(ActionMailer::Base.deliveries.count).to eq(2) + expect(email_recipients).to contain_exactly("johndoe@example.com", "janedoe@example.com") + end + end + + context "when the recipient addresses contains angle brackets and are separated by newlines" do + let(:recipients) { "John Doe <johndoe@example.com>\nJane Doe <janedoe@example.com>" } + + it "accepts emails separated by newlines" do + perform + + expect(ActionMailer::Base.deliveries.count).to eq(2) + expect(email_recipients).to contain_exactly("johndoe@example.com", "janedoe@example.com") + end end end end diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index ee0df7711e7..b698248bc38 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -677,6 +677,7 @@ rollout 100%: auto_chart=${AUTO_DEVOPS_CHART:-gitlab/auto-deploy-app} auto_chart_name=$(basename $auto_chart) auto_chart_name=${auto_chart_name%.tgz} + auto_chart_name=${auto_chart_name%.tar.gz} else auto_chart="chart" auto_chart_name="chart" diff --git a/yarn.lock b/yarn.lock index 9d0eae5f509..67f9aa98c74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -78,9 +78,9 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.25.0": - version "1.25.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.25.0.tgz#1a82b1be43e1a46e6b0767ef46f26f5fd6bbd101" +"@gitlab-org/gitlab-svgs@^1.26.0": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.26.0.tgz#d89c633e866d031a9e4787b05eacc7144c3a7791" "@sindresorhus/is@^0.7.0": version "0.7.0" |