diff options
92 files changed, 1055 insertions, 437 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e7b28e540a5..afe9da08495 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -350,25 +350,6 @@ retrieve-tests-metadata: - wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH - '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}' -danger-review: - image: registry.gitlab.com/gitlab-org/gitaly/dangercontainer:latest - stage: prepare - allow_failure: true - before_script: - - source scripts/utils.sh - - retry gem install danger --no-ri --no-rdoc - cache: {} - only: - refs: - - branches@gitlab-org/gitlab-ce - - branches@gitlab-org/gitlab-ee - except: - variables: - - $CI_COMMIT_REF_NAME =~ /^ce-to-ee-.*/ - script: - - git version - - danger --fail-on-errors=true - update-tests-metadata: <<: *tests-metadata-state <<: *only-canonical-masters @@ -457,6 +438,27 @@ setup-test-env: - config/secrets.yml - vendor/gitaly-ruby +danger-review: + image: registry.gitlab.com/gitlab-org/gitaly/dangercontainer:latest + stage: test + allow_failure: true + before_script: + - source scripts/utils.sh + - retry gem install danger --no-ri --no-rdoc + cache: {} + only: + refs: + - branches@gitlab-org/gitlab-ce + - branches@gitlab-org/gitlab-ee + except: + refs: + - master + variables: + - $CI_COMMIT_REF_NAME =~ /^ce-to-ee-.*/ + script: + - git version + - danger --fail-on-errors=true + rspec-pg 0 30: *rspec-metadata-pg rspec-pg 1 30: *rspec-metadata-pg rspec-pg 2 30: *rspec-metadata-pg diff --git a/.gitlab/merge_request_templates/Database changes.md b/.gitlab/merge_request_templates/Database changes.md index d14d52e1b6b..e636ec313df 100644 --- a/.gitlab/merge_request_templates/Database changes.md +++ b/.gitlab/merge_request_templates/Database changes.md @@ -34,17 +34,17 @@ When removing columns, tables, indexes or other structures: ## General checklist - [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary -- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html) -- [ ] API support added -- [ ] Tests added for this feature/bug -- Conform by the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html) - - [ ] Has been reviewed by a Backend maintainer - - [ ] Has been reviewed by a Database specialist -- [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html) -- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides) +- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/documentation/index.html#contributing-to-docs) +- [ ] [API support added](https://docs.gitlab.com/ee/development/api_styleguide.html) +- [ ] [Tests added for this feature/bug](https://docs.gitlab.com/ee/development/testing_guide/index.html) +- Conforms to the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html) + - [ ] Has been reviewed by a Backend [maintainer](https://about.gitlab.com/handbook/engineering/#maintainer) + - [ ] Has been reviewed by a Database [specialist](https://about.gitlab.com/team/structure/#specialist) +- [ ] Conforms to the [merge request performance guidelines](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html) +- [ ] Conforms to the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides) - [ ] If you have multiple commits, please combine them into a few logically organized commits by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) -- [ ] Internationalization required/considered -- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan -- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job) +- [ ] [Internationalization required/considered](https://docs.gitlab.com/ee/development/i18n/index.html) +- [ ] For a paid feature, have we considered GitLab.com plans, how it works for groups, and is there a design for promoting it to users who aren't on the correct plan? +- [ ] [End-to-end tests](https://docs.gitlab.com/ee/development/testing_guide/end_to_end_tests.html#testing-code-in-merge-requests) pass (`package-and-qa` manual pipeline job) /label ~database diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index da38a703c3c..531035b3766 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -1,4 +1,4 @@ -<!--See the general Documentation guidelines https://docs.gitlab.com/ce/development/writing_documentation.html --> +<!--See the general Documentation guidelines https://docs.gitlab.com/ee/development/documentation/index.html --> ## What does this MR do? @@ -13,17 +13,17 @@ Closes ## Moving docs to a new location? Read the guidelines: -https://docs.gitlab.com/ce/development/writing_documentation.html#changing-document-location +https://docs.gitlab.com/ee/development/documentation/#changing-document-location - [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location. - [ ] Make sure internal links pointing to the document in question are not broken. -- [ ] Search and replace any links referring to old docs in GitLab Rails app, - specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories. -- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments) +- [ ] Search and replace any links referring to the old docs in the GitLab Rails app, + specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories. +- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/documentation/index.html#redirections-for-pages-with-disqus-comments) to the new document if there are any Disqus comments on the old document thread. -- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE - with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee). +- [ ] If working on CE and the `ee-compat-check` jobs fails, [submit an MR to EE + with the changes](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee) as well. - [ ] Ping one of the technical writers for review. /label ~Documentation diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 1d1415fe6ca..7cc4e6a2c3a 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -85,6 +85,9 @@ export default { } return __('Show latest version'); }, + canCurrentUserFork() { + return this.currentUser.canFork === true && this.currentUser.canCreateMergeRequest; + }, }, watch: { diffViewType() { @@ -192,7 +195,7 @@ export default { v-for="file in diffFiles" :key="file.newPath" :file="file" - :current-user="currentUser" + :can-current-user-fork="canCurrentUserFork" /> </div> <no-changes v-else /> diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 944084f05c9..7e7058d8d08 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -18,8 +18,8 @@ export default { type: Object, required: true, }, - currentUser: { - type: Object, + canCurrentUserFork: { + type: Boolean, required: true, }, }, @@ -87,7 +87,7 @@ export default { class="diff-file file-holder" > <diff-file-header - :current-user="currentUser" + :can-current-user-fork="canCurrentUserFork" :diff-file="file" :collapsible="true" :expanded="!isCollapsed" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index c5abd0a9568..c494d3bcd3e 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -39,8 +39,8 @@ export default { required: false, default: true, }, - currentUser: { - type: Object, + canCurrentUserFork: { + type: Boolean, required: true, }, }, @@ -228,7 +228,7 @@ export default { <edit-button v-if="!diffFile.deletedFile" - :current-user="currentUser" + :can-current-user-fork="canCurrentUserFork" :edit-path="diffFile.editPath" :can-modify-blob="diffFile.canModifyBlob" @showForkMessage="showForkMessage" 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 db380e68bd1..32f9516d332 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -13,12 +13,8 @@ export default { noteForm, }, props: { - diffFile: { - type: Object, - required: true, - }, - diffLines: { - type: Array, + diffFileHash: { + type: String, required: true, }, line: { @@ -40,6 +36,7 @@ export default { noteableData: state => state.notes.noteableData, diffViewType: state => state.diffs.diffViewType, }), + ...mapGetters('diffs', ['getDiffFileByHash']), ...mapGetters(['isLoggedIn', 'noteableType', 'getNoteableData', 'getNotesDataByProp']), }, mounted() { @@ -68,13 +65,14 @@ export default { }); }, handleSaveNote(note) { + const selectedDiffFile = this.getDiffFileByHash(this.diffFileHash); const postData = getNoteFormData({ note, noteableData: this.noteableData, noteableType: this.noteableType, noteTargetLine: this.noteTargetLine, diffViewType: this.diffViewType, - diffFile: this.diffFile, + diffFile: selectedDiffFile, linePosition: this.position, }); diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue index bd02b45a63c..5962f30d9bb 100644 --- a/app/assets/javascripts/diffs/components/diff_table_cell.vue +++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue @@ -24,8 +24,12 @@ export default { type: Object, required: true, }, - diffFile: { - type: Object, + fileHash: { + type: String, + required: true, + }, + contextLinesPath: { + type: String, required: true, }, diffViewType: { @@ -120,14 +124,14 @@ export default { :class="classNameMap" > <diff-line-gutter-content - :file-hash="diffFile.fileHash" + :file-hash="fileHash" + :context-lines-path="contextLinesPath" :line-type="normalizedLine.type" :line-code="normalizedLine.lineCode" :line-position="linePosition" :line-number="lineNumber" :meta-data="normalizedLine.metaData" :show-comment-button="showCommentButton" - :context-lines-path="diffFile.contextLinesPath" :is-bottom="isBottom" :is-match-line="isMatchLine" :is-context-line="isContentLine" diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue index ebf90631d76..2fb85ca2f07 100644 --- a/app/assets/javascripts/diffs/components/edit_button.vue +++ b/app/assets/javascripts/diffs/components/edit_button.vue @@ -5,8 +5,8 @@ export default { type: String, required: true, }, - currentUser: { - type: Object, + canCurrentUserFork: { + type: Boolean, required: true, }, canModifyBlob: { @@ -17,12 +17,12 @@ export default { }, methods: { handleEditClick(evt) { - if (!this.currentUser || this.canModifyBlob) { + if (!this.canCurrentUserFork || this.canModifyBlob) { // if we can Edit, do default Edit button behavior return; } - if (this.currentUser.canFork && this.currentUser.canCreateMergeRequest) { + if (this.canCurrentUserFork) { evt.preventDefault(); this.$emit('showForkMessage'); } 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 1e8f2eecd76..ca265dd892c 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -13,12 +13,8 @@ export default { type: Object, required: true, }, - diffFile: { - type: Object, - required: true, - }, - diffLines: { - type: Array, + diffFileHash: { + type: String, required: true, }, lineIndex: { @@ -58,10 +54,9 @@ export default { /> <diff-line-note-form v-if="diffLineCommentForms[line.lineCode]" - :diff-file="diffFile" - :diff-lines="diffLines" + :diff-file-hash="diffFileHash" :line="line" - :note-target-line="diffLines[lineIndex]" + :note-target-line="line" /> </div> </td> 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 8e4715c9862..0197a510ef1 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -16,8 +16,12 @@ export default { DiffTableCell, }, props: { - diffFile: { - type: Object, + fileHash: { + type: String, + required: true, + }, + contextLinesPath: { + type: String, required: true, }, line: { @@ -50,7 +54,7 @@ export default { inlineRowId() { const { lineCode, oldLine, newLine } = this.line; - return lineCode || `${this.diffFile.fileHash}_${oldLine}_${newLine}`; + return lineCode || `${this.fileHash}_${oldLine}_${newLine}`; }, }, created() { @@ -78,7 +82,8 @@ export default { @mouseout="handleMouseMove" > <diff-table-cell - :diff-file="diffFile" + :file-hash="fileHash" + :context-lines-path="contextLinesPath" :line="line" :line-type="oldLineType" :is-bottom="isBottom" @@ -87,7 +92,8 @@ export default { class="diff-line-num old_line" /> <diff-table-cell - :diff-file="diffFile" + :file-hash="fileHash" + :context-lines-path="contextLinesPath" :line="line" :line-type="newLineType" :is-bottom="isBottom" diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue index 9c1359f7c89..9fd19b74cd7 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_view.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue @@ -60,15 +60,15 @@ export default { v-for="(line, index) in normalizedDiffLines" > <inline-diff-table-row - :diff-file="diffFile" + :file-hash="diffFile.fileHash" + :context-lines-path="diffFile.contextLinesPath" :line="line" :is-bottom="index + 1 === diffLinesLength" :key="line.lineCode" /> <inline-diff-comment-row v-if="shouldRenderCommentRow(line)" - :diff-file="diffFile" - :diff-lines="normalizedDiffLines" + :diff-file-hash="diffFile.fileHash" :line="line" :line-index="index" :key="index" 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 1e20792b647..cc5248c25d9 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -13,12 +13,8 @@ export default { type: Object, required: true, }, - diffFile: { - type: Object, - required: true, - }, - diffLines: { - type: Array, + diffFileHash: { + type: String, required: true, }, lineIndex: { @@ -91,10 +87,9 @@ export default { <diff-line-note-form v-if="diffLineCommentForms[leftLineCode] && diffLineCommentForms[leftLineCode]" - :diff-file="diffFile" - :diff-lines="diffLines" + :diff-file-hash="diffFileHash" :line="line.left" - :note-target-line="diffLines[lineIndex].left" + :note-target-line="line.left" position="left" /> </td> @@ -112,10 +107,9 @@ export default { <diff-line-note-form v-if="diffLineCommentForms[rightLineCode] && diffLineCommentForms[rightLineCode] && line.right.type" - :diff-file="diffFile" - :diff-lines="diffLines" + :diff-file-hash="diffFileHash" :line="line.right" - :note-target-line="diffLines[lineIndex].right" + :note-target-line="line.right" position="right" /> </td> 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 b76fc63205b..ee5bb4d8d05 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue @@ -19,8 +19,12 @@ export default { DiffTableCell, }, props: { - diffFile: { - type: Object, + fileHash: { + type: String, + required: true, + }, + contextLinesPath: { + type: String, required: true, }, line: { @@ -103,7 +107,8 @@ export default { @mouseout="handleMouseMove" > <diff-table-cell - :diff-file="diffFile" + :file-hash="fileHash" + :context-lines-path="contextLinesPath" :line="line" :line-type="oldLineType" :line-position="linePositionLeft" @@ -123,7 +128,8 @@ export default { > </td> <diff-table-cell - :diff-file="diffFile" + :file-hash="fileHash" + :context-lines-path="contextLinesPath" :line="line" :line-type="newLineType" :line-position="linePositionRight" diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue index 216865474a6..32528c9e7ab 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue @@ -93,17 +93,17 @@ export default { v-for="(line, index) in parallelDiffLines" > <parallel-diff-table-row - :diff-file="diffFile" + :file-hash="diffFile.fileHash" + :context-lines-path="diffFile.contextLinesPath" :line="line" :is-bottom="index + 1 === diffLinesLength" :key="index" /> <parallel-diff-comment-row v-if="shouldRenderCommentRow(line)" - :key="line.left.lineCode || line.right.lineCode" + :key="`dcr-${index}`" :line="line" - :diff-file="diffFile" - :diff-lines="parallelDiffLines" + :diff-file-hash="diffFile.fileHash" :line-index="index" /> </template> diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index f89acb73ed8..855de79adf8 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -57,4 +57,8 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) = ) || []; // prevent babel-plugin-rewire from generating an invalid default during karma∂ tests +export const getDiffFileByHash = state => fileHash => + state.diffFiles.find(file => file.fileHash === fileHash); + +// prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 9c2908c477e..27ff7dea909 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -1,94 +1,90 @@ <script> - import { mapState, mapActions } from 'vuex'; - import imageDiffHelper from '~/image_diff/helpers/index'; - import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; - import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; - import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; - import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; +import { mapState, mapActions } from 'vuex'; +import imageDiffHelper from '~/image_diff/helpers/index'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; +import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; - export default { - components: { - DiffFileHeader, - SkeletonLoadingContainer, +export default { + components: { + DiffFileHeader, + SkeletonLoadingContainer, + }, + props: { + discussion: { + type: Object, + required: true, }, - props: { - discussion: { - type: Object, - required: true, - }, + }, + data() { + return { + error: false, + }; + }, + computed: { + ...mapState({ + noteableData: state => state.notes.noteableData, + }), + hasTruncatedDiffLines() { + return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0; }, - data() { - return { - error: false, - }; + isDiscussionsExpanded() { + return true; // TODO: @fatihacet - Fix this. }, - computed: { - ...mapState({ - noteableData: state => state.notes.noteableData, - }), - hasTruncatedDiffLines() { - return this.discussion.truncatedDiffLines && - this.discussion.truncatedDiffLines.length !== 0; - }, - isDiscussionsExpanded() { - return true; // TODO: @fatihacet - Fix this. - }, - isCollapsed() { - return this.diffFile.collapsed || false; - }, - isImageDiff() { - return !this.diffFile.text; - }, - diffFileClass() { - const { text } = this.diffFile; - return text ? 'text-file' : 'js-image-file'; - }, - diffFile() { - return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true }); - }, - imageDiffHtml() { - return this.discussion.imageDiffHtml; - }, - currentUser() { - return this.noteableData.current_user; - }, - userColorScheme() { - return window.gon.user_color_scheme; - }, - normalizedDiffLines() { - if (this.discussion.truncatedDiffLines) { - return this.discussion.truncatedDiffLines.map(line => - trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)), - ); - } - - return []; - }, + isCollapsed() { + return this.diffFile.collapsed || false; + }, + isImageDiff() { + return !this.diffFile.text; + }, + diffFileClass() { + const { text } = this.diffFile; + return text ? 'text-file' : 'js-image-file'; + }, + diffFile() { + return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true }); }, - mounted() { - if (this.isImageDiff) { - const canCreateNote = false; - const renderCommentBadge = true; - imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge); - } else if (!this.hasTruncatedDiffLines) { - this.fetchDiff(); + imageDiffHtml() { + return this.discussion.imageDiffHtml; + }, + userColorScheme() { + return window.gon.user_color_scheme; + }, + normalizedDiffLines() { + if (this.discussion.truncatedDiffLines) { + return this.discussion.truncatedDiffLines.map(line => + trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)), + ); } + + return []; + }, + }, + mounted() { + if (this.isImageDiff) { + const canCreateNote = false; + const renderCommentBadge = true; + imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge); + } else if (!this.hasTruncatedDiffLines) { + this.fetchDiff(); + } + }, + methods: { + ...mapActions(['fetchDiscussionDiffLines']), + rowTag(html) { + return html.outerHTML ? 'tr' : 'template'; }, - methods: { - ...mapActions(['fetchDiscussionDiffLines']), - rowTag(html) { - return html.outerHTML ? 'tr' : 'template'; - }, - fetchDiff() { - this.error = false; - this.fetchDiscussionDiffLines(this.discussion) - .then(this.highlight) - .catch(() => { - this.error = true; - }); - }, + fetchDiff() { + this.error = false; + this.fetchDiscussionDiffLines(this.discussion) + .then(this.highlight) + .catch(() => { + this.error = true; + }); }, - }; + }, +}; </script> <template> @@ -99,7 +95,7 @@ > <diff-file-header :diff-file="diffFile" - :current-user="currentUser" + :can-current-user-fork="false" :discussions-expanded="isDiscussionsExpanded" :expanded="!isCollapsed" /> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 56fdb858088..c7df69c69ed 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -175,6 +175,7 @@ export default { <span :aria-label="stage.title" aria-hidden="true" + class="no-pointer-events" > <icon :name="borderlessIcon" /> </span> diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index ded33e8b151..d28ad407734 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -110,7 +110,7 @@ code { padding: 2px 4px; color: $red-600; background-color: $red-100; - border-radius: 3px; + border-radius: $border-radius-default; .code > & { background-color: inherit; @@ -128,7 +128,8 @@ table { border-spacing: 0; } -.tooltip { +.tooltip, +.no-pointer-events { // Fix bootstrap4 bug whereby tooltips flicker when they are hovered over their borders pointer-events: none; } diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index 1cf12b1a015..d2ba76f5160 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -9,12 +9,8 @@ .gfm-project_member { padding: 0 2px; - border-radius: #{$border-radius-default / 2}; - background-color: $user-mention-bg; - - &:hover { - background-color: $user-mention-bg-hover; - } + background-color: $blue-100; + border-radius: $border-radius-default; } .gfm-color_chip { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 5835b8b8c9b..c8349a4ef79 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -551,6 +551,7 @@ @include media-breakpoint-up(lg) { .branch-actions { align-self: center; + margin-left: $gl-padding; } } } diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index c01066c688a..9dc0c31be49 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -116,7 +116,12 @@ class Projects::WikisController < Projects::ApplicationController # Call #wiki to make sure the Wiki Repo is initialized @project_wiki.wiki - @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15)) + + @sidebar_page = @project_wiki.find_sidebar(params[:version_id]) + + unless @sidebar_page # Fallback to default sidebar + @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15)) + end rescue ProjectWiki::CouldNotCreateWikiError flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." redirect_to project_path(@project) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index b0f381db5ab..221f1aa9dd8 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -413,20 +413,6 @@ module ProjectsHelper @ref || @repository.try(:root_ref) end - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1235 - def sanitize_repo_path(project, message) - return '' unless message.present? - - exports_path = File.join(Settings.shared['path'], 'tmp/project_exports') - filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]") - - disk_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do - Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path - end - - filtered_message.gsub(disk_path.chomp('/'), "[REPOS PATH]") - end - def project_child_container_class(view_path) view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}" end diff --git a/app/models/note.rb b/app/models/note.rb index abc40d9016e..fe3507adcb3 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -202,7 +202,7 @@ class Note < ActiveRecord::Base end def hook_attrs - attributes + Gitlab::HookData::NoteBuilder.new(self).build end def for_commit? diff --git a/app/models/project.rb b/app/models/project.rb index e29bca365a4..ae0a13b17dc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2171,10 +2171,13 @@ class Project < ActiveRecord::Base merge_requests = source_of_merge_requests.opened .where(allow_collaboration: true) - if branch_name - merge_requests.find_by(source_branch: branch_name)&.can_be_merged_by?(user) - else - merge_requests.any? { |merge_request| merge_request.can_be_merged_by?(user) } + # Issue for N+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/49322 + Gitlab::GitalyClient.allow_n_plus_1_calls do + if branch_name + merge_requests.find_by(source_branch: branch_name)&.can_be_merged_by?(user) + else + merge_requests.any? { |merge_request| merge_request.can_be_merged_by?(user) } + end end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 9ae2fb0013a..3aa56b3983f 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProjectWiki include Gitlab::ShellAdapter include Storage::LegacyProjectWiki @@ -9,6 +11,7 @@ class ProjectWiki }.freeze unless defined?(MARKUPS) CouldNotCreateWikiError = Class.new(StandardError) + SIDEBAR = '_sidebar' # Returns a string describing what went wrong after # an operation fails. @@ -98,6 +101,10 @@ class ProjectWiki end end + def find_sidebar(version = nil) + find_page(SIDEBAR, version) + end + def find_file(name, version = nil) wiki.file(name, version) end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 4b49edb01a5..55243136140 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -60,7 +60,7 @@ class WikiPage attr_accessor :attributes def hook_attrs - attributes + Gitlab::HookData::WikiPageBuilder.new(self).build end def initialize(wiki, page = nil, persisted = false) diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml index 3ae9ce6c11f..13f96b9747c 100644 --- a/app/views/ci/runner/_how_to_setup_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner.html.haml @@ -1,16 +1,17 @@ -- link = link_to _("GitLab Runner section"), 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank' +- link = link_to _("Install GitLab Runner"), 'https://docs.gitlab.com/runner/install/', target: '_blank' .append-bottom-10 %h4= _("Setup a #{type} Runner manually") %ol %li - = _("Install a Runner compatible with GitLab CI") - = (_("(check out the %{link} for information on how to install it).") % { link: link }).html_safe + = link.html_safe %li = _("Specify the following URL during the Runner setup:") %code#coordinator_address= root_url(only_path: false) + = clipboard_button(target: '#coordinator_address', title: _("Copy URL to clipboard"), class: "btn-transparent btn-clipboard") %li = _("Use the following registration token during setup:") %code#registration_token= registration_token + = clipboard_button(target: '#registration_token', title: _("Copy token to clipboard"), class: "btn-transparent btn-clipboard") %li = _("Start the Runner!") diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 3d97e93c9e9..14a7e84394a 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -11,7 +11,7 @@ - branch_label = s_('ChangeTypeActionLabel|Pick into branch') - title = commit.merged_merge_request(current_user) ? _('Cherry-pick this merge request') : _('Cherry-pick this commit') -.modal{ id: "modal-#{type}-commit" } +.modal{ id: "modal-#{type}-commit", tabindex: -1 } .modal-dialog .modal-content .modal-header diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 16c4f21279d..ca82054d799 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -10,7 +10,7 @@ .card-body %pre :preserve - #{h(sanitize_repo_path(@project, @project.import_error))} + #{h(@project.import_error)} = form_for @project, url: project_import_path(@project), method: :post do |f| = render "shared/import_form", f: f diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index a23396dc0d8..28353927135 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -11,9 +11,11 @@ .blocks-container .block.block-first - %ul.wiki-pages - = render @sidebar_wiki_entries, context: 'sidebar' - + - if @sidebar_page + = render_wiki_content(@sidebar_page) + - else + %ul.wiki-pages + = render @sidebar_wiki_entries, context: 'sidebar' .block = link_to project_wikis_pages_path(@project), class: 'btn btn-block' do = s_("Wiki|More Pages") diff --git a/changelogs/custom_wiki_sidebar.yml b/changelogs/custom_wiki_sidebar.yml new file mode 100644 index 00000000000..988fccc929c --- /dev/null +++ b/changelogs/custom_wiki_sidebar.yml @@ -0,0 +1,5 @@ +--- +title: "Custom Wiki Sidebar Support Issue 14995" +merge_request: +author: Josh Sooter +type: added diff --git a/changelogs/unreleased/add-merge-request-header-branch-details-right-margin.yml b/changelogs/unreleased/add-merge-request-header-branch-details-right-margin.yml new file mode 100644 index 00000000000..4f9a551d13e --- /dev/null +++ b/changelogs/unreleased/add-merge-request-header-branch-details-right-margin.yml @@ -0,0 +1,5 @@ +--- +title: Add merge request header branch actions left margin +merge_request: 20643 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/close-revert-and-cherry-pick-modal-on-escape-keypress.yml b/changelogs/unreleased/close-revert-and-cherry-pick-modal-on-escape-keypress.yml new file mode 100644 index 00000000000..49648cdfcfc --- /dev/null +++ b/changelogs/unreleased/close-revert-and-cherry-pick-modal-on-escape-keypress.yml @@ -0,0 +1,5 @@ +--- +title: Close revert and cherry pick modal on escape keypress +merge_request: 20341 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/feature-gb-email-delivery-metrics.yml b/changelogs/unreleased/feature-gb-email-delivery-metrics.yml new file mode 100644 index 00000000000..9d0d08a471d --- /dev/null +++ b/changelogs/unreleased/feature-gb-email-delivery-metrics.yml @@ -0,0 +1,5 @@ +--- +title: Add emails delivery Prometheus metrics +merge_request: 20638 +author: +type: added diff --git a/changelogs/unreleased/satishperala-gitlab-ce-20720_webhooks_full_image_url.yml b/changelogs/unreleased/satishperala-gitlab-ce-20720_webhooks_full_image_url.yml new file mode 100644 index 00000000000..7bfe1b5778f --- /dev/null +++ b/changelogs/unreleased/satishperala-gitlab-ce-20720_webhooks_full_image_url.yml @@ -0,0 +1,5 @@ +--- +title: Include full image URL in webhooks for uploaded images +merge_request: 18109 +author: Satish Perala +type: changed diff --git a/changelogs/unreleased/update-specific-runners-help-url.yml b/changelogs/unreleased/update-specific-runners-help-url.yml new file mode 100644 index 00000000000..0ccbc3b2d65 --- /dev/null +++ b/changelogs/unreleased/update-specific-runners-help-url.yml @@ -0,0 +1,5 @@ +--- +title: Update specific runners help URL +merge_request: 20213 +author: George Tsiolis +type: other diff --git a/config/initializers/action_mailer_hooks.rb b/config/initializers/action_mailer_hooks.rb new file mode 100644 index 00000000000..f1b3c1f8ae8 --- /dev/null +++ b/config/initializers/action_mailer_hooks.rb @@ -0,0 +1,12 @@ +unless Gitlab.config.gitlab.email_enabled + ActionMailer::Base.register_interceptor(::Gitlab::Email::Hook::DisableEmailInterceptor) + ActionMailer::Base.logger = nil +end + +ActionMailer::Base.register_interceptors( + ::Gitlab::Email::Hook::AdditionalHeadersInterceptor, + ::Gitlab::Email::Hook::EmailTemplateInterceptor, + ::Gitlab::Email::Hook::DeliveryMetricsObserver +) + +ActionMailer::Base.register_observer(::Gitlab::Email::Hook::DeliveryMetricsObserver) diff --git a/config/initializers/additional_headers_interceptor.rb b/config/initializers/additional_headers_interceptor.rb deleted file mode 100644 index b9159e7c06c..00000000000 --- a/config/initializers/additional_headers_interceptor.rb +++ /dev/null @@ -1 +0,0 @@ -ActionMailer::Base.register_interceptor(AdditionalEmailHeadersInterceptor) diff --git a/config/initializers/disable_email_interceptor.rb b/config/initializers/disable_email_interceptor.rb deleted file mode 100644 index e8770c8d460..00000000000 --- a/config/initializers/disable_email_interceptor.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Interceptor in lib/disable_email_interceptor.rb -unless Gitlab.config.gitlab.email_enabled - ActionMailer::Base.register_interceptor(DisableEmailInterceptor) - ActionMailer::Base.logger = nil -end diff --git a/config/initializers/email_template_interceptor.rb b/config/initializers/email_template_interceptor.rb deleted file mode 100644 index f195ca9bcd6..00000000000 --- a/config/initializers/email_template_interceptor.rb +++ /dev/null @@ -1,2 +0,0 @@ -# Interceptor in lib/email_template_interceptor.rb -ActionMailer::Base.register_interceptor(EmailTemplateInterceptor) diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile index 0374de24520..a1f94dc6004 100644 --- a/danger/changelog/Dangerfile +++ b/danger/changelog/Dangerfile @@ -2,15 +2,13 @@ require 'yaml' -NO_CHANGELOG_LABELS = %w[backstage QA test].freeze +NO_CHANGELOG_LABELS = %w[backstage Documentation QA test].freeze SEE_DOC = "See [the documentation](https://docs.gitlab.com/ce/development/changelog.html).".freeze -MISSING_CHANGELOG_MESSAGE = <<~MSG.freeze -**[CHANGELOG missing](https://docs.gitlab.com/ce/development/changelog.html).** - +CREATE_CHANGELOG_MESSAGE = <<~MSG.freeze You can create one with: ``` -bin/changelog -m %<mr_iid>s +bin/changelog -m %<mr_iid>s "%<mr_title>s" ``` If your merge request doesn't warrant a CHANGELOG entry, @@ -56,13 +54,15 @@ changelog_needed = (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty? changelog_found = git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} } if git.modified_files.include?("CHANGELOG.md") - fail "CHANGELOG.md was edited. Please remove the additions and create an entry with `bin/changelog -m #{gitlab.mr_json["iid"]}` instead." + fail "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n" + + format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: gitlab.mr_json["title"], labels: presented_no_changelog_labels) end if changelog_needed if changelog_found check_changelog(changelog_found) else - warn format(MISSING_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], labels: presented_no_changelog_labels) + warn "**[CHANGELOG missing](https://docs.gitlab.com/ce/development/changelog.html).**\n\n" + + format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: gitlab.mr_json["title"], labels: presented_no_changelog_labels) end end diff --git a/danger/specs/Dangerfile b/danger/specs/Dangerfile index 934ea0beadb..97188df8785 100644 --- a/danger/specs/Dangerfile +++ b/danger/specs/Dangerfile @@ -1,12 +1,18 @@ +NO_SPECS_LABELS = %w[backstage Documentation QA].freeze NO_NEW_SPEC_MESSAGE = <<~MSG.freeze You've made some app changes, but didn't add any tests. That's OK as long as you're refactoring existing code, -but please consider adding the ~backstage label in that case. +but please consider adding any of the %<labels>s labels. MSG +def presented_no_changelog_labels + NO_SPECS_LABELS.map { |label| "~#{label}" }.join(', ') +end + has_app_changes = !git.modified_files.grep(%r{\A(ee/)?(app|lib|db/(geo/)?(post_)?migrate)/}).empty? -has_spec_changes = !git.modified_files.grep(/spec/).empty? +has_spec_changes = !git.modified_files.grep(%r{\A(ee/)?spec/}).empty? +new_specs_needed = (gitlab.mr_labels & NO_SPECS_LABELS).empty? -if has_app_changes && !has_spec_changes - warn NO_NEW_SPEC_MESSAGE, sticky: false +if has_app_changes && !has_spec_changes && new_specs_needed + warn format(NO_NEW_SPEC_MESSAGE, labels: presented_no_changelog_labels), sticky: false end diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index ebae68fe389..22cf9afbcd2 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -151,7 +151,7 @@ POST /projects/:id/pipelines/:pipeline_id/retry | `pipeline_id` | integer | yes | The ID of a pipeline | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/retry" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/retry" ``` Response: @@ -197,7 +197,7 @@ POST /projects/:id/pipelines/:pipeline_id/cancel | `pipeline_id` | integer | yes | The ID of a pipeline | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/cancel" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/cancel" ``` Response: diff --git a/doc/api/users.md b/doc/api/users.md index ca5afa04687..72fdaaa2c74 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -33,6 +33,20 @@ GET /users ] ``` +You can also search for users by email or username with: `/users?search=John` + +In addition, you can lookup users by username: + +``` +GET /users?username=:username +``` + +For example: + +``` +GET /users?username=jack_smith +``` + In addition, you can filter users based on states eg. `blocked`, `active` This works only to filter users who are `blocked` or `active`. It does not support `active=false` or `blocked=false`. @@ -126,21 +140,7 @@ GET /users ] ``` -You can search for users by email or username with: `/users?search=John` - -In addition, you can lookup users by username: - -``` -GET /users?username=:username -``` - -For example: - -``` -GET /users?username=jack_smith -``` - -You can also lookup users by external UID and provider: +You can lookup users by external UID and provider: ``` GET /users?extern_uid=:extern_uid&provider=:provider diff --git a/doc/development/sql.md b/doc/development/sql.md index 974b1d99dff..e1e1d31a85f 100644 --- a/doc/development/sql.md +++ b/doc/development/sql.md @@ -243,3 +243,45 @@ WHERE EXISTS ( ``` [gin-index]: http://www.postgresql.org/docs/current/static/gin.html + +## `.find_or_create_by` is not atomic + +The inherent pattern with methods like `.find_or_create_by` and +`.first_or_create` and others is that they are not atomic. This means, +it first runs a `SELECT`, and if there are no results an `INSERT` is +performed. With concurrent processes in mind, there is a race condition +which may lead to trying to insert two similar records. This may not be +desired, or may cause one of the queries to fail due to a constraint +violation, for example. + +Using transactions does not solve this problem. + +The following pattern should be used to avoid the problem: + +```ruby +Project.transaction do + begin + User.find_or_create_by(username: "foo") + rescue ActiveRecord::RecordNotUnique + retry + end +end +``` + +If the above block is run inside a transaction and hits the race +condition, the transaction is aborted and we cannot simply retry (any +further queries inside the aborted transaction are going to fail). We +can employ [nested transactions](http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions) +here to only rollback the "inner transaction". Note that `requires_new: true` is required here. + +```ruby +Project.transaction do + begin + User.transaction(requires_new: true) do + User.find_or_create_by(username: "foo") + end + rescue ActiveRecord::RecordNotUnique + retry + end +end +``` diff --git a/doc/user/project/import/manifest.md b/doc/user/project/import/manifest.md index 812ecf05faf..06171f11e12 100644 --- a/doc/user/project/import/manifest.md +++ b/doc/user/project/import/manifest.md @@ -1,7 +1,8 @@ # Import multiple repositories by uploading a manifest file GitLab allows you to import all the required git repositories -based a manifest file like the one used by the Android repository. +based a manifest file like the one used by the [Android repository](https://android.googlesource.com/platform/manifest/+/2d6f081a3b05d8ef7a2b1b52b0d536b2b74feab4/default.xml). +This feature can be very handy when you need to import a project with many repositories like Android Open Source Project (AOSP). >**Note:** diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 8c09927e2df..8e486318980 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -10,6 +10,13 @@ Starting from GitLab 8.5: Starting from GitLab 11.1, the logs of web hooks are automatically removed after one month. +>**Note** +Starting from GitLab 11.2: +- The `description` field for issues, merge requests, comments, and wiki pages + is rewritten so that simple Markdown image references (like + `![](/uploads/...)`) have their target URL changed to an absolute URL. See + [image URL rewriting](#image-url-rewriting) for more details. + Project webhooks allow you to trigger a URL if for example new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data @@ -1125,6 +1132,27 @@ X-Gitlab-Event: Build Hook } ``` +## Image URL rewriting + +From GitLab 11.2, simple image references are rewritten to use an absolute URL +in webhooks. So if an image, merge request, comment, or wiki page has this in +its description: + +```markdown +![image](/uploads/$sha/image.png) +``` + +It will appear in the webhook body as the below (assuming that GitLab is +installed at gitlab.example.com): + +```markdown +![image](https://gitlab.example.com/uploads/$sha/image.png) +``` + +This will not rewrite URLs that already are pointing to HTTP, HTTPS, or +protocol-relative URLs. It will also not rewrite image URLs using advanced +Markdown features, like link labels. + ## Testing webhooks You can trigger the webhook manually. Sample data from the project will be used.Sample data will take from the project. diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md index d084ee41d8a..ad0ef60373c 100644 --- a/doc/user/project/wiki/index.md +++ b/doc/user/project/wiki/index.md @@ -107,3 +107,10 @@ On the right sidebar, click on **Clone repository** and follow the on-screen instructions. [permissions]: ../../permissions.md + +## Customizing sidebar + +By default, the wiki would render a sidebar which lists all the pages for the +wiki. You could as well provide a `_sidebar` page to replace this default +sidebar. When this customized sidebar page is provided, the default sidebar +would not be rendered, but the customized one. diff --git a/lib/additional_email_headers_interceptor.rb b/lib/additional_email_headers_interceptor.rb deleted file mode 100644 index 3cb1694b9f1..00000000000 --- a/lib/additional_email_headers_interceptor.rb +++ /dev/null @@ -1,6 +0,0 @@ -class AdditionalEmailHeadersInterceptor - def self.delivering_email(message) - message.header['Auto-Submitted'] ||= 'auto-generated' - message.header['X-Auto-Response-Suppress'] ||= 'All' - end -end diff --git a/lib/banzai/filter/blockquote_fence_filter.rb b/lib/banzai/filter/blockquote_fence_filter.rb index fbfcd72c916..7108e828c6d 100644 --- a/lib/banzai/filter/blockquote_fence_filter.rb +++ b/lib/banzai/filter/blockquote_fence_filter.rb @@ -2,27 +2,7 @@ module Banzai module Filter class BlockquoteFenceFilter < HTML::Pipeline::TextFilter REGEX = %r{ - (?<code> - # Code blocks: - # ``` - # Anything, including `>>>` blocks which are ignored by this filter - # ``` - - ^``` - .+? - \n```\ *$ - ) - | - (?<html> - # HTML block: - # <tag> - # Anything, including `>>>` blocks which are ignored by this filter - # </tag> - - ^<[^>]+?>\ *\n - .+? - \n<\/[^>]+?>\ *$ - ) + #{::Gitlab::Regex.markdown_code_or_html_blocks} | (?: # Blockquote: diff --git a/lib/disable_email_interceptor.rb b/lib/disable_email_interceptor.rb deleted file mode 100644 index cee664b8951..00000000000 --- a/lib/disable_email_interceptor.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Read about interceptors in http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails -class DisableEmailInterceptor - def self.delivering_email(message) - message.perform_deliveries = false - Rails.logger.info "Emails disabled! Interceptor prevented sending mail #{message.subject}" - end -end diff --git a/lib/email_template_interceptor.rb b/lib/email_template_interceptor.rb deleted file mode 100644 index 3978a6d9fe4..00000000000 --- a/lib/email_template_interceptor.rb +++ /dev/null @@ -1,11 +0,0 @@ -# Read about interceptors in http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails -class EmailTemplateInterceptor - def self.delivering_email(message) - # Remove HTML part if HTML emails are disabled. - unless Gitlab::CurrentSettings.html_emails_enabled - message.parts.delete_if do |part| - part.content_type.start_with?('text/html') - end - end - end -end diff --git a/lib/gitlab/email/hook/additional_headers_interceptor.rb b/lib/gitlab/email/hook/additional_headers_interceptor.rb new file mode 100644 index 00000000000..064cb5e659a --- /dev/null +++ b/lib/gitlab/email/hook/additional_headers_interceptor.rb @@ -0,0 +1,12 @@ +module Gitlab + module Email + module Hook + class AdditionalHeadersInterceptor + def self.delivering_email(message) + message.header['Auto-Submitted'] ||= 'auto-generated' + message.header['X-Auto-Response-Suppress'] ||= 'All' + end + end + end + end +end diff --git a/lib/gitlab/email/hook/delivery_metrics_observer.rb b/lib/gitlab/email/hook/delivery_metrics_observer.rb new file mode 100644 index 00000000000..1c2985f6045 --- /dev/null +++ b/lib/gitlab/email/hook/delivery_metrics_observer.rb @@ -0,0 +1,31 @@ +module Gitlab + module Email + module Hook + class DeliveryMetricsObserver + extend Gitlab::Utils::StrongMemoize + + def self.delivering_email(_message) + delivery_attempts_counter.increment + end + + def self.delivered_email(_message) + delivered_emails_counter.increment + end + + def self.delivery_attempts_counter + strong_memoize(:delivery_attempts_counter) do + Gitlab::Metrics.counter(:gitlab_emails_delivery_attempts_total, + 'Counter of total emails delivery attempts') + end + end + + def self.delivered_emails_counter + strong_memoize(:delivered_emails_counter) do + Gitlab::Metrics.counter(:gitlab_emails_delivered_total, + 'Counter of total emails delievered') + end + end + end + end + end +end diff --git a/lib/gitlab/email/hook/disable_email_interceptor.rb b/lib/gitlab/email/hook/disable_email_interceptor.rb new file mode 100644 index 00000000000..7bb8b53f0c8 --- /dev/null +++ b/lib/gitlab/email/hook/disable_email_interceptor.rb @@ -0,0 +1,13 @@ +module Gitlab + module Email + module Hook + class DisableEmailInterceptor + def self.delivering_email(message) + message.perform_deliveries = false + + Rails.logger.info "Emails disabled! Interceptor prevented sending mail #{message.subject}" + end + end + end + end +end diff --git a/lib/gitlab/email/hook/email_template_interceptor.rb b/lib/gitlab/email/hook/email_template_interceptor.rb new file mode 100644 index 00000000000..be0c4dd862e --- /dev/null +++ b/lib/gitlab/email/hook/email_template_interceptor.rb @@ -0,0 +1,18 @@ +module Gitlab + module Email + module Hook + class EmailTemplateInterceptor + ## + # Remove HTML part if HTML emails are disabled. + # + def self.delivering_email(message) + unless Gitlab::CurrentSettings.html_emails_enabled + message.parts.delete_if do |part| + part.content_type.start_with?('text/html') + end + end + end + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 3c23b588f77..2cbd9c218d4 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -443,12 +443,8 @@ module Gitlab # Returns the SHA of the most recent common ancestor of +from+ and +to+ def merge_base(from, to) - gitaly_migrate(:merge_base) do |is_enabled| - if is_enabled - gitaly_repository_client.find_merge_base(from, to) - else - rugged_merge_base(from, to) - end + wrapped_gitaly_errors do + gitaly_repository_client.find_merge_base(from, to) end end @@ -464,12 +460,8 @@ module Gitlab return [] unless root_sha - branches = gitaly_migrate(:merged_branch_names) do |is_enabled| - if is_enabled - gitaly_merged_branch_names(branch_names, root_sha) - else - git_merged_branch_names(branch_names, root_sha) - end + branches = wrapped_gitaly_errors do + gitaly_merged_branch_names(branch_names, root_sha) end Set.new(branches) @@ -848,12 +840,8 @@ module Gitlab def write_ref(ref_path, ref, old_ref: nil, shell: true) ref_path = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{ref_path}" unless ref_path.start_with?("refs/") || ref_path == "HEAD" - gitaly_migrate(:write_ref) do |is_enabled| - if is_enabled - gitaly_repository_client.write_ref(ref_path, ref, old_ref, shell) - else - local_write_ref(ref_path, ref, old_ref: old_ref, shell: shell) - end + wrapped_gitaly_errors do + gitaly_repository_client.write_ref(ref_path, ref, old_ref, shell) end end @@ -1188,37 +1176,6 @@ module Gitlab end end - def local_write_ref(ref_path, ref, old_ref: nil, shell: true) - if shell - shell_write_ref(ref_path, ref, old_ref) - else - rugged_write_ref(ref_path, ref) - end - end - - def rugged_write_config(full_path:) - rugged.config['gitlab.fullpath'] = full_path - end - - def shell_write_ref(ref_path, ref, old_ref) - raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ') - raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00") - raise ArgumentError, "invalid old_ref #{old_ref.inspect}" if !old_ref.nil? && old_ref.include?("\x00") - - input = "update #{ref_path}\x00#{ref}\x00#{old_ref}\x00" - run_git!(%w[update-ref --stdin -z]) { |stdin| stdin.write(input) } - end - - def rugged_write_ref(ref_path, ref) - rugged.references.create(ref_path, ref, force: true) - rescue Rugged::ReferenceError => ex - Rails.logger.error "Unable to create #{ref_path} reference for repository #{path}: #{ex}" - rescue Rugged::OSError => ex - raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/ - - Rails.logger.error "Unable to create #{ref_path} reference for repository #{path}: #{ex}" - end - def run_git(args, chdir: path, env: {}, nice: false, lazy_block: nil, &block) cmd = [Gitlab.config.git.bin_path, *args] cmd.unshift("nice") if nice @@ -1289,20 +1246,6 @@ module Gitlab } end - def git_merged_branch_names(branch_names, root_sha) - git_arguments = - %W[branch --merged #{root_sha} - --format=%(refname:short)\ %(objectname)] + branch_names - - lines = run_git(git_arguments).first.lines - - lines.each_with_object([]) do |line, branches| - name, sha = line.strip.split(' ', 2) - - branches << name if sha != root_sha - end - end - def gitaly_merged_branch_names(branch_names, root_sha) qualified_branch_names = branch_names.map { |b| "refs/heads/#{b}" } @@ -1448,12 +1391,6 @@ module Gitlab raise CommandError, @gitlab_projects.output end - def rugged_merge_base(from, to) - rugged.merge_base(from, to) - rescue Rugged::ReferenceError - nil - end - def rev_list_param(spec) spec == :all ? ['--all'] : spec end diff --git a/lib/gitlab/hook_data/base_builder.rb b/lib/gitlab/hook_data/base_builder.rb new file mode 100644 index 00000000000..4ffca356b29 --- /dev/null +++ b/lib/gitlab/hook_data/base_builder.rb @@ -0,0 +1,38 @@ +module Gitlab + module HookData + class BaseBuilder + attr_accessor :object + + MARKDOWN_SIMPLE_IMAGE = %r{ + #{::Gitlab::Regex.markdown_code_or_html_blocks} + | + (?<image> + ! + \[(?<title>[^\n]*?)\] + \((?<url>(?!(https?://|//))[^\n]+?)\) + ) + }mx.freeze + + def initialize(object) + @object = object + end + + private + + def absolute_image_urls(markdown_text) + return markdown_text unless markdown_text.present? + + markdown_text.gsub(MARKDOWN_SIMPLE_IMAGE) do + if $~[:image] + url = $~[:url] + url = "/#{url}" unless url.start_with?('/') + + "![#{$~[:title]}](#{Gitlab.config.gitlab.url}#{url})" + else + $~[0] + end + end + end + end + end +end diff --git a/lib/gitlab/hook_data/issuable_builder.rb b/lib/gitlab/hook_data/issuable_builder.rb index 6ab36676127..f2eda398b8f 100644 --- a/lib/gitlab/hook_data/issuable_builder.rb +++ b/lib/gitlab/hook_data/issuable_builder.rb @@ -1,13 +1,9 @@ module Gitlab module HookData - class IssuableBuilder + class IssuableBuilder < BaseBuilder CHANGES_KEYS = %i[previous current].freeze - attr_accessor :issuable - - def initialize(issuable) - @issuable = issuable - end + alias_method :issuable, :object def build(user: nil, changes: {}) hook_data = { diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb index f9b1a3caf5e..0d71c748dc6 100644 --- a/lib/gitlab/hook_data/issue_builder.rb +++ b/lib/gitlab/hook_data/issue_builder.rb @@ -1,6 +1,6 @@ module Gitlab module HookData - class IssueBuilder + class IssueBuilder < BaseBuilder SAFE_HOOK_ATTRIBUTES = %i[ assignee_id author_id @@ -30,14 +30,11 @@ module Gitlab total_time_spent ].freeze - attr_accessor :issue - - def initialize(issue) - @issue = issue - end + alias_method :issue, :object def build attrs = { + description: absolute_image_urls(issue.description), url: Gitlab::UrlBuilder.build(issue), total_time_spent: issue.total_time_spent, human_total_time_spent: issue.human_total_time_spent, diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb index aff786864f2..dfbed0597ed 100644 --- a/lib/gitlab/hook_data/merge_request_builder.rb +++ b/lib/gitlab/hook_data/merge_request_builder.rb @@ -1,6 +1,6 @@ module Gitlab module HookData - class MergeRequestBuilder + class MergeRequestBuilder < BaseBuilder SAFE_HOOK_ATTRIBUTES = %i[ assignee_id author_id @@ -35,14 +35,11 @@ module Gitlab total_time_spent ].freeze - attr_accessor :merge_request - - def initialize(merge_request) - @merge_request = merge_request - end + alias_method :merge_request, :object def build attrs = { + description: absolute_image_urls(merge_request.description), url: Gitlab::UrlBuilder.build(merge_request), source: merge_request.source_project.try(:hook_attrs), target: merge_request.target_project.hook_attrs, diff --git a/lib/gitlab/hook_data/note_builder.rb b/lib/gitlab/hook_data/note_builder.rb new file mode 100644 index 00000000000..81873e345d5 --- /dev/null +++ b/lib/gitlab/hook_data/note_builder.rb @@ -0,0 +1,43 @@ +module Gitlab + module HookData + class NoteBuilder < BaseBuilder + SAFE_HOOK_ATTRIBUTES = %i[ + attachment + author_id + change_position + commit_id + created_at + discussion_id + id + line_code + note + noteable_id + noteable_type + original_position + position + project_id + resolved_at + resolved_by_id + resolved_by_push + st_diff + system + type + updated_at + updated_by_id + ].freeze + + alias_method :note, :object + + def build + note + .attributes + .with_indifferent_access + .slice(*SAFE_HOOK_ATTRIBUTES) + .merge( + description: absolute_image_urls(note.note), + url: Gitlab::UrlBuilder.build(note) + ) + end + end + end +end diff --git a/lib/gitlab/hook_data/wiki_page_builder.rb b/lib/gitlab/hook_data/wiki_page_builder.rb new file mode 100644 index 00000000000..59c94a61cf2 --- /dev/null +++ b/lib/gitlab/hook_data/wiki_page_builder.rb @@ -0,0 +1,15 @@ +module Gitlab + module HookData + class WikiPageBuilder < BaseBuilder + alias_method :wiki_page, :object + + def build + wiki_page + .attributes + .merge( + 'content' => absolute_image_urls(wiki_page.content) + ) + end + end + end +end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index ac3de2a8f71..e1a958c508a 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -73,5 +73,31 @@ module Gitlab def build_trace_section_regex @build_trace_section_regexp ||= /section_((?:start)|(?:end)):(\d+):([a-zA-Z0-9_.-]+)\r\033\[0K/.freeze end + + def markdown_code_or_html_blocks + @markdown_code_or_html_blocks ||= %r{ + (?<code> + # Code blocks: + # ``` + # Anything, including `>>>` blocks which are ignored by this filter + # ``` + + ^``` + .+? + \n```\ *$ + ) + | + (?<html> + # HTML block: + # <tag> + # Anything, including `>>>` blocks which are ignored by this filter + # </tag> + + ^<[^>]+?>\ *\n + .+? + \n<\/[^>]+?>\ *$ + ) + }mx + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1b68156a503..8e23383609f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -129,9 +129,6 @@ msgstr "" msgid "%{unstaged} unstaged and %{staged} staged changes" msgstr "" -msgid "(check out the %{link} for information on how to install it)." -msgstr "" - msgid "+ %{moreCount} more" msgstr "" @@ -1774,6 +1771,9 @@ msgstr "" msgid "Copy to clipboard" msgstr "" +msgid "Copy token to clipboard" +msgstr "" + msgid "Create" msgstr "" @@ -2594,9 +2594,6 @@ msgstr "" msgid "GitLab Import" msgstr "" -msgid "GitLab Runner section" -msgstr "" - msgid "GitLab User" msgstr "" @@ -2917,10 +2914,10 @@ msgstr "" msgid "Inline" msgstr "" -msgid "Install Runner on Kubernetes" +msgid "Install GitLab Runner" msgstr "" -msgid "Install a Runner compatible with GitLab CI" +msgid "Install Runner on Kubernetes" msgstr "" msgid "Instance does not support multiple Kubernetes clusters" @@ -53,6 +53,7 @@ module QA autoload :User, 'qa/factory/resource/user' autoload :ProjectMilestone, 'qa/factory/resource/project_milestone' autoload :Wiki, 'qa/factory/resource/wiki' + autoload :File, 'qa/factory/resource/file' autoload :Fork, 'qa/factory/resource/fork' end @@ -136,6 +137,15 @@ module QA autoload :Show, 'qa/page/group/show' end + module File + autoload :Form, 'qa/page/file/form' + autoload :Show, 'qa/page/file/show' + + module Shared + autoload :CommitMessage, 'qa/page/file/shared/commit_message' + end + end + module Project autoload :New, 'qa/page/project/new' autoload :Show, 'qa/page/project/show' diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb new file mode 100644 index 00000000000..2016d10ddae --- /dev/null +++ b/qa/qa/factory/resource/file.rb @@ -0,0 +1,34 @@ +module QA + module Factory + module Resource + class File < Factory::Base + attr_accessor :name, + :content, + :commit_message + + dependency Factory::Resource::Project, as: :project do |project| + project.name = 'project-with-new-file' + end + + def initialize + @name = 'QA Test - File name' + @content = 'QA Test - File content' + @commit_message = 'QA Test - Commit message' + end + + def fabricate! + project.visit! + + Page::Project::Show.act { go_to_new_file! } + + Page::File::Form.perform do |page| + page.add_name(@name) + page.add_content(@content) + page.add_commit_message(@commit_message) + page.commit_changes + end + end + end + end + end +end diff --git a/qa/qa/page/file/form.rb b/qa/qa/page/file/form.rb new file mode 100644 index 00000000000..f6e502f500b --- /dev/null +++ b/qa/qa/page/file/form.rb @@ -0,0 +1,40 @@ +module QA + module Page + module File + class Form < Page::Base + include Shared::CommitMessage + + view 'app/views/projects/blob/_editor.html.haml' do + element :file_name, "text_field_tag 'file_name'" + element :editor, '#editor' + end + + view 'app/views/projects/_commit_button.html.haml' do + element :commit_changes, "button_tag 'Commit changes'" + end + + def add_name(name) + fill_in 'file_name', with: name + end + + def add_content(content) + text_area.set content + end + + def remove_content + text_area.send_keys([:command, 'a'], :backspace) + end + + def commit_changes + click_on 'Commit changes' + end + + private + + def text_area + find('#editor>textarea', visible: false) + end + end + end + end +end diff --git a/qa/qa/page/file/shared/commit_message.rb b/qa/qa/page/file/shared/commit_message.rb new file mode 100644 index 00000000000..5af1a55e2ef --- /dev/null +++ b/qa/qa/page/file/shared/commit_message.rb @@ -0,0 +1,19 @@ +module QA + module Page + module File + module Shared + module CommitMessage + def self.included(base) + base.view 'app/views/shared/_commit_message_container.html.haml' do + element :commit_message, "text_area_tag 'commit_message'" + end + end + + def add_commit_message(message) + fill_in 'commit_message', with: message + end + end + end + end + end +end diff --git a/qa/qa/page/file/show.rb b/qa/qa/page/file/show.rb new file mode 100644 index 00000000000..99f5924b67f --- /dev/null +++ b/qa/qa/page/file/show.rb @@ -0,0 +1,30 @@ +module QA + module Page + module File + class Show < Page::Base + include Shared::CommitMessage + + view 'app/helpers/blob_helper.rb' do + element :edit_button, "_('Edit')" + element :delete_button, /label:\s+"Delete"/ + end + + view 'app/views/projects/blob/_remove.html.haml' do + element :delete_file_button, "button_tag 'Delete file'" + end + + def click_edit + click_on 'Edit' + end + + def click_delete + click_on 'Delete' + end + + def click_delete_file + click_on 'Delete file' + end + end + end + end +end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 88861d5772d..1d3dad4cda0 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -31,10 +31,18 @@ module QA element :tree_holder, '.tree-holder' end + view 'app/presenters/project_presenter.rb' do + element :new_file_button, "label: _('New file')," + end + def project_name find('.qa-project-name').text end + def go_to_new_file! + click_on 'New file' + end + def switch_to_branch(branch_name) find_element(:branches_select).click diff --git a/qa/qa/page/view.rb b/qa/qa/page/view.rb index 6635e1ce039..b2a2da4dbf3 100644 --- a/qa/qa/page/view.rb +++ b/qa/qa/page/view.rb @@ -9,7 +9,7 @@ module QA end def pathname - @pathname ||= Pathname.new(File.join(__dir__, '../../../', @path)) + @pathname ||= Pathname.new(::File.join(__dir__, '../../../', @path)) .cleanpath.expand_path end @@ -23,7 +23,7 @@ module QA # elements' existence. # @missing ||= @elements.dup.tap do |elements| - File.foreach(pathname.to_s) do |line| + ::File.foreach(pathname.to_s) do |line| elements.reject! { |element| element.matches?(line) } end end diff --git a/qa/qa/specs/features/project/file_spec.rb b/qa/qa/specs/features/project/file_spec.rb new file mode 100644 index 00000000000..5659784cd5c --- /dev/null +++ b/qa/qa/specs/features/project/file_spec.rb @@ -0,0 +1,54 @@ +module QA + describe 'File Functionality', :core do + it 'lets a user create, edit and delete a file via WebUI' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + # Create + file_name = 'QA Test - File name' + file_content = 'QA Test - File content' + commit_message_for_create = 'QA Test - Create new file' + + Factory::Resource::File.fabricate! do |file| + file.name = file_name + file.content = file_content + file.commit_message = commit_message_for_create + end + + expect(page).to have_content('The file has been successfully created.') + expect(page).to have_content(file_name) + expect(page).to have_content(file_content) + expect(page).to have_content(commit_message_for_create) + + # Edit + updated_file_content = 'QA Test - Updated file content' + commit_message_for_update = 'QA Test - Update file' + + Page::File::Show.act { click_edit } + + Page::File::Form.act do + remove_content + add_content(updated_file_content) + add_commit_message(commit_message_for_update) + commit_changes + end + + expect(page).to have_content('Your changes have been successfully committed.') + expect(page).to have_content(updated_file_content) + expect(page).to have_content(commit_message_for_update) + + # Delete + commit_message_for_delete = 'QA Test - Delete file' + + Page::File::Show.act do + click_delete + add_commit_message(commit_message_for_delete) + click_delete_file + end + + expect(page).to have_content('The file has been successfully deleted.') + expect(page).to have_content(commit_message_for_delete) + expect(page).to have_no_content(file_name) + end + end +end diff --git a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb index c40c720d168..86086a58f18 100644 --- a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb +++ b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Merge request > User sees Check out branch modal', :js do +describe 'Merge request > User sees check out branch modal', :js do let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } let(:merge_request) { create(:merge_request, source_project: project) } @@ -16,7 +16,7 @@ describe 'Merge request > User sees Check out branch modal', :js do expect(page).to have_content('Check out, review, and merge locally') end - it 'closes the check out branch model with Escape keypress' do + it 'closes the check out branch modal with escape keypress' do find('#modal_merge_info').send_keys(:escape) expect(page).not_to have_content('Check out, review, and merge locally') diff --git a/spec/features/merge_request/user_cherry_picks_spec.rb b/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb index c6ec3f08cc5..aa499493dbe 100644 --- a/spec/features/merge_request/user_cherry_picks_spec.rb +++ b/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb @@ -21,7 +21,7 @@ describe 'Merge request > User cherry-picks', :js do end # Fast-forward merge, or merged before GitLab 8.5. - context 'Without a merge commit' do + context 'without a merge commit' do before do merge_request.merge_commit_sha = nil merge_request.save @@ -34,7 +34,7 @@ describe 'Merge request > User cherry-picks', :js do end end - context 'With a merge commit' do + context 'with a merge commit' do it 'shows a Cherry-pick button' do visit project_merge_request_path(project, merge_request) @@ -49,5 +49,23 @@ describe 'Merge request > User cherry-picks', :js do expect(page).not_to have_link 'Cherry-pick' end end + + context 'and seeing the cherry-pick modal' do + before do + visit project_merge_request_path(project, merge_request) + + click_link('Cherry-pick') + end + + it 'shows the cherry-pick modal' do + expect(page).to have_content('Cherry-pick this merge request') + end + + it 'closes the cherry-pick modal with escape keypress' do + find('#modal-cherry-pick-commit').send_keys(:escape) + + expect(page).not_to have_content('Start a new merge request with these changes') + end + end end end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 830565620d6..149eeb4f9ba 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -2,16 +2,22 @@ require "spec_helper" describe "User creates wiki page" do let(:user) { create(:user) } + let(:wiki) { ProjectWiki.new(project, user) } + let(:project) { create(:project) } before do project.add_maintainer(user) - sign_in(user) - visit(project_wikis_path(project)) - click_link "Create your first page" + sign_in(user) end context "when wiki is empty" do + before do + visit(project_wikis_path(project)) + + click_link "Create your first page" + end + context "in a user namespace" do let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } @@ -165,7 +171,9 @@ describe "User creates wiki page" do context "when wiki is not empty", :js do before do - create(:wiki_page, wiki: create(:project, :wiki_repo, namespace: user.namespace).wiki, attrs: { title: "home", content: "Home page" }) + create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: 'Home page' }) + + visit(project_wikis_path(project)) end context "in a user namespace" do @@ -290,4 +298,34 @@ describe "User creates wiki page" do end end end + + describe 'sidebar feature' do + context 'when there are some existing pages' do + before do + create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: 'home' }) + create(:wiki_page, wiki: wiki, attrs: { title: 'another', content: 'another' }) + end + + it 'renders a default sidebar when there is no customized sidebar' do + visit(project_wikis_path(project)) + + expect(page).to have_content('Another') + expect(page).to have_content('More Pages') + end + + context 'when there is a customized sidebar' do + before do + create(:wiki_page, wiki: wiki, attrs: { title: '_sidebar', content: 'My customized sidebar' }) + end + + it 'renders my customized sidebar instead of the default one' do + visit(project_wikis_path(project)) + + expect(page).to have_content('My customized sidebar') + expect(page).to have_content('More Pages') + expect(page).not_to have_content('Another') + end + end + end + end end diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb new file mode 100644 index 00000000000..11a9e470f76 --- /dev/null +++ b/spec/features/user_sees_revert_modal_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +describe 'Merge request > User sees revert modal', :js do + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:merge_request) { create(:merge_request, source_project: project) } + + before do + sign_in(user) + visit(project_merge_request_path(project, merge_request)) + click_button('Merge') + visit(merge_request_path(merge_request)) + click_link('Revert') + end + + it 'shows the revert modal' do + expect(page).to have_content('Revert this merge request') + end + + it 'closes the revert modal with escape keypress' do + find('#modal-revert-commit').send_keys(:escape) + + expect(page).not_to have_content('Revert this merge request') + end +end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index beb6e8ea273..cbd4ff0fb4a 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -290,33 +290,6 @@ describe ProjectsHelper do end end - describe '#sanitizerepo_repo_path' do - let(:project) { create(:project, :repository) } - let(:storage_path) do - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - Gitlab.config.repositories.storages.default.legacy_disk_path - end - end - - before do - allow(Settings.shared).to receive(:[]).with('path').and_return('/base/repo/export/path') - end - - it 'removes the repo path' do - repo = File.join(storage_path, 'namespace/test.git') - import_error = "Could not clone #{repo}\n" - - expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git') - end - - it 'removes the temporary repo path used for uploads/exports' do - repo = '/base/repo/export/path/tmp/project_exports/uploads/test.tar.gz' - import_error = "Unable to decompress #{repo}\n" - - expect(sanitize_repo_path(project, import_error)).to eq('Unable to decompress [REPO EXPORT PATH]/uploads/test.tar.gz') - end - end - describe '#last_push_event' do let(:user) { double(:user, fork_of: nil) } let(:project) { double(:project, id: 1) } diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 0f3a95da5bf..241ff07026e 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -24,7 +24,7 @@ describe('diff_file_header', () => { const diffFile = convertObjectPropsToCamelCase(diffDiscussionMock.diff_file, { deep: true }); props = { diffFile, - currentUser: {}, + canCurrentUserFork: false, }; }); diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index 9b994543e19..7a4616ec8eb 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -11,7 +11,7 @@ describe('DiffFile', () => { beforeEach(() => { vm = createComponentWithStore(Vue.extend(DiffFileComponent), store, { file: getDiffFileMock(), - currentUser: {}, + canCurrentUserFork: false, }).$mount(); }); 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 81cd4f9769a..4600aaea70b 100644 --- a/spec/javascripts/diffs/components/diff_line_note_form_spec.js +++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js @@ -15,7 +15,7 @@ describe('DiffLineNoteForm', () => { diffLines = diffFile.highlightedDiffLines; component = createComponentWithStore(Vue.extend(DiffLineNoteForm), store, { - diffFile, + diffFileHash: diffFile.fileHash, diffLines, line: diffLines[0], noteTargetLine: diffLines[0], diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js index 919b612bb6a..6210d4a7124 100644 --- a/spec/javascripts/diffs/store/getters_spec.js +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -184,4 +184,23 @@ describe('Diffs Module Getters', () => { ).toEqual(0); }); }); + + describe('getDiffFileByHash', () => { + it('returns file by hash', () => { + const fileA = { + fileHash: '123', + }; + const fileB = { + fileHash: '456', + }; + localState.diffFiles = [fileA, fileB]; + + expect(getters.getDiffFileByHash(localState)('456')).toEqual(fileB); + }); + + it('returns null if no matching file is found', () => { + localState.diffFiles = []; + expect(getters.getDiffFileByHash(localState)('123')).toBeUndefined(); + }); + }); }); diff --git a/spec/lib/additional_email_headers_interceptor_spec.rb b/spec/lib/gitlab/email/hook/additional_headers_interceptor_spec.rb index b5c1a360ba9..ae61ece8029 100644 --- a/spec/lib/additional_email_headers_interceptor_spec.rb +++ b/spec/lib/gitlab/email/hook/additional_headers_interceptor_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe AdditionalEmailHeadersInterceptor do +describe Gitlab::Email::Hook::AdditionalHeadersInterceptor do let(:mail) do ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello') end diff --git a/spec/lib/gitlab/email/hook/delivery_metrics_observer_spec.rb b/spec/lib/gitlab/email/hook/delivery_metrics_observer_spec.rb new file mode 100644 index 00000000000..4497d4002da --- /dev/null +++ b/spec/lib/gitlab/email/hook/delivery_metrics_observer_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Gitlab::Email::Hook::DeliveryMetricsObserver do + let(:email) do + ActionMailer::Base.mail(to: 'test@example.com', + from: 'info@example.com', + body: 'hello') + end + + context 'when email has been delivered' do + it 'increments both email delivery metrics' do + expect(described_class.delivery_attempts_counter).to receive(:increment) + expect(described_class.delivered_emails_counter).to receive(:increment) + + email.deliver_now + end + end + + context 'when email has not been delivered due to an error' do + before do + allow(email.delivery_method).to receive(:deliver!) + .and_raise(StandardError, 'Some SMTP error') + end + + it 'increments only delivery attempt metric' do + expect(described_class.delivery_attempts_counter) + .to receive(:increment) + expect(described_class.delivered_emails_counter) + .not_to receive(:increment) + + expect { email.deliver_now } + .to raise_error(StandardError, 'Some SMTP error') + end + end +end diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb index 3652d928c43..91aa3bc7c2e 100644 --- a/spec/lib/disable_email_interceptor_spec.rb +++ b/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe DisableEmailInterceptor do +describe Gitlab::Email::Hook::DisableEmailInterceptor do before do Mail.register_interceptor(described_class) end diff --git a/spec/lib/gitlab/hook_data/base_builder_spec.rb b/spec/lib/gitlab/hook_data/base_builder_spec.rb new file mode 100644 index 00000000000..a921dd766c3 --- /dev/null +++ b/spec/lib/gitlab/hook_data/base_builder_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Gitlab::HookData::BaseBuilder do + describe '#absolute_image_urls' do + let(:subclass) do + Class.new(described_class) do + public :absolute_image_urls + end + end + + subject { subclass.new(nil) } + + using RSpec::Parameterized::TableSyntax + + where do + { + 'relative image URL' => { + input: '![an image](foo.png)', + output: "![an image](#{Gitlab.config.gitlab.url}/foo.png)" + }, + 'HTTP URL' => { + input: '![an image](http://example.com/foo.png)', + output: '![an image](http://example.com/foo.png)' + }, + 'HTTPS URL' => { + input: '![an image](https://example.com/foo.png)', + output: '![an image](https://example.com/foo.png)' + }, + 'protocol-relative URL' => { + input: '![an image](//example.com/foo.png)', + output: '![an image](//example.com/foo.png)' + }, + 'URL reference by title' => { + input: "![foo]\n\n[foo]: foo.png", + output: "![foo]\n\n[foo]: foo.png" + }, + 'URL reference by label' => { + input: "![][foo]\n\n[foo]: foo.png", + output: "![][foo]\n\n[foo]: foo.png" + }, + 'in Markdown inline code block' => { + input: '`![an image](foo.png)`', + output: "`![an image](#{Gitlab.config.gitlab.url}/foo.png)`" + }, + 'in HTML tag on the same line' => { + input: '<p>![an image](foo.png)</p>', + output: "<p>![an image](#{Gitlab.config.gitlab.url}/foo.png)</p>" + }, + 'in Markdown multi-line code block' => { + input: "```\n![an image](foo.png)\n```", + output: "```\n![an image](foo.png)\n```" + }, + 'in HTML tag on different lines' => { + input: "<p>\n![an image](foo.png)\n</p>", + output: "<p>\n![an image](foo.png)\n</p>" + } + } + end + + with_them do + it { expect(subject.absolute_image_urls(input)).to eq(output) } + end + end +end diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb index 506b2c0be20..60093474f8a 100644 --- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb @@ -40,5 +40,14 @@ describe Gitlab::HookData::IssueBuilder do expect(data).to include(:human_total_time_spent) expect(data).to include(:assignee_ids) end + + context 'when the issue has an image in the description' do + let(:issue_with_description) { create(:issue, description: 'test![Issue_Image](/uploads/abc/Issue_Image.png)') } + let(:builder) { described_class.new(issue_with_description) } + + it 'sets the image to use an absolute URL' do + expect(data[:description]).to eq("test![Issue_Image](#{Settings.gitlab.url}/uploads/abc/Issue_Image.png)") + end + end end end diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index b61614e4790..dd586af6118 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -56,5 +56,14 @@ describe Gitlab::HookData::MergeRequestBuilder do expect(data).to include(:human_time_estimate) expect(data).to include(:human_total_time_spent) end + + context 'when the MR has an image in the description' do + let(:mr_with_description) { create(:merge_request, description: 'test![Issue_Image](/uploads/abc/Issue_Image.png)') } + let(:builder) { described_class.new(mr_with_description) } + + it 'sets the image to use an absolute URL' do + expect(data[:description]).to eq("test![Issue_Image](#{Settings.gitlab.url}/uploads/abc/Issue_Image.png)") + end + end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 581132b6672..ff1a5aa2536 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -1366,7 +1366,8 @@ describe Notify do it 'only sends the text template' do stub_application_setting(html_emails_enabled: false) - EmailTemplateInterceptor.delivering_email(multipart_mail) + Gitlab::Email::Hook::EmailTemplateInterceptor + .delivering_email(multipart_mail) expect(multipart_mail).to have_part_with('text/plain') expect(multipart_mail).not_to have_part_with('text/html') @@ -1377,7 +1378,8 @@ describe Notify do it 'sends a multipart message' do stub_application_setting(html_emails_enabled: true) - EmailTemplateInterceptor.delivering_email(multipart_mail) + Gitlab::Email::Hook::EmailTemplateInterceptor + .delivering_email(multipart_mail) expect(multipart_mail).to have_part_with('text/plain') expect(multipart_mail).to have_part_with('text/html') diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index a544940800a..528f5b610d7 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -189,6 +189,22 @@ describe ProjectWiki do end end + describe '#find_sidebar' do + before do + create_page(described_class::SIDEBAR, 'This is an awesome Sidebar') + end + + after do + subject.pages.each { |page| destroy_page(page.page) } + end + + it 'finds the page defined as _sidebar' do + page = subject.find_page('_sidebar') + + expect(page.content).to eq('This is an awesome Sidebar') + end + end + describe '#find_file' do shared_examples 'finding a wiki file' do let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) } diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 1c765ceac2f..63850939be1 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -554,6 +554,16 @@ describe WikiPage do end end + describe '#hook_attrs' do + it 'adds absolute urls for images in the content' do + create_page("test page", "test![WikiPage_Image](/uploads/abc/WikiPage_Image.png)") + page = wiki.wiki.page(title: "test page") + wiki_page = described_class.new(wiki, page, true) + + expect(wiki_page.hook_attrs['content']).to eq("test![WikiPage_Image](#{Settings.gitlab.url}/uploads/abc/WikiPage_Image.png)") + end + end + private def remove_temp_repo(path) |