summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js27
-rw-r--r--app/assets/javascripts/diffs/components/changed_files.vue62
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue50
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue98
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue24
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue18
-rw-r--r--app/assets/javascripts/diffs/constants.js4
-rw-r--r--app/assets/javascripts/diffs/mixins/diff_content.js19
-rw-r--r--app/assets/javascripts/diffs/store/actions.js35
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js25
-rw-r--r--app/assets/javascripts/diffs/store/utils.js103
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js141
-rw-r--r--app/assets/javascripts/merge_request_tabs.js85
-rw-r--r--app/assets/javascripts/mr_notes/index.js14
-rw-r--r--app/assets/javascripts/mr_notes/stores/getters.js4
-rw-r--r--app/assets/javascripts/notes.js259
-rw-r--r--app/assets/javascripts/notes/components/diff_file_header.vue82
-rw-r--r--app/assets/javascripts/notes/stores/actions.js1
-rw-r--r--app/assets/javascripts/notes/stores/getters.js4
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js6
-rw-r--r--app/assets/stylesheets/pages/diff.scss6
-rw-r--r--app/assets/stylesheets/pages/notes.scss29
-rw-r--r--app/controllers/projects/blob_controller.rb39
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb7
-rw-r--r--app/helpers/notes_helper.rb2
-rw-r--r--app/serializers/blob_entity.rb4
-rw-r--r--app/serializers/diff_file_entity.rb70
-rw-r--r--app/serializers/diff_file_serializer.rb3
-rw-r--r--app/serializers/diffs_entity.rb13
-rw-r--r--app/serializers/diffs_serializer.rb3
-rw-r--r--app/serializers/discussion_entity.rb2
-rw-r--r--app/views/projects/merge_requests/show.html.haml3
-rw-r--r--lib/gitlab/diff/file.rb6
-rw-r--r--lib/gitlab/diff/highlight.rb2
-rw-r--r--lib/gitlab/diff/line.rb35
-rw-r--r--lib/gitlab/diff/parser.rb8
-rw-r--r--spec/serializers/discussion_entity_spec.rb2
38 files changed, 875 insertions, 421 deletions
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index e17daec6a92..406091820e9 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -15,12 +15,14 @@ import './components/resolve_count';
import './components/resolve_discussion_btn';
import './components/diff_note_avatars';
import './components/new_issue_for_discussion';
-import { hasVueMRDiscussionsCookie } from '../lib/utils/common_utils';
export default () => {
- const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
+ const projectPathHolder =
+ document.querySelector('.merge-request') ||
+ document.querySelector('.commit-box');
const projectPath = projectPathHolder.dataset.projectPath;
- const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
+ const COMPONENT_SELECTOR =
+ 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
window.gl = window.gl || {};
window.gl.diffNoteApps = {};
@@ -28,9 +30,9 @@ export default () => {
window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
gl.diffNotesCompileComponents = () => {
- $('diff-note-avatars').each(function () {
+ $('diff-note-avatars').each(function() {
const tmp = Vue.extend({
- template: $(this).get(0).outerHTML
+ template: $(this).get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
@@ -41,12 +43,12 @@ export default () => {
});
});
- const $components = $(COMPONENT_SELECTOR).filter(function () {
+ const $components = $(COMPONENT_SELECTOR).filter(function() {
return $(this).closest('resolve-count').length !== 1;
});
if ($components) {
- $components.each(function () {
+ $components.each(function() {
const $this = $(this);
const noteId = $this.attr(':note-id');
const discussionId = $this.attr(':discussion-id');
@@ -54,7 +56,7 @@ export default () => {
if ($this.is('comment-and-resolve-btn') && !discussionId) return;
const tmp = Vue.extend({
- template: $this.get(0).outerHTML
+ template: $this.get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
@@ -69,14 +71,5 @@ export default () => {
gl.diffNotesCompileComponents();
- if (!hasVueMRDiscussionsCookie()) {
- new Vue({
- el: '#resolve-count-app',
- components: {
- 'resolve-count': ResolveCount
- },
- });
- }
-
$(window).trigger('resize.nav');
};
diff --git a/app/assets/javascripts/diffs/components/changed_files.vue b/app/assets/javascripts/diffs/components/changed_files.vue
index 4bab49bfca5..dee3dbf95d0 100644
--- a/app/assets/javascripts/diffs/components/changed_files.vue
+++ b/app/assets/javascripts/diffs/components/changed_files.vue
@@ -1,10 +1,14 @@
<script>
+import _ from 'underscore';
import { mapGetters, mapActions } from 'vuex';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
+import { pluralize } from '~/lib/utils/text_utility';
export default {
components: {
Icon,
+ ClipboardButton,
},
props: {
diffFiles: {
@@ -42,18 +46,15 @@ export default {
},
},
mounted() {
- if (
- typeof CSS === 'undefined' ||
- !CSS.supports('(position: -webkit-sticky) or (position: sticky)')
- )
- return;
-
- document.addEventListener('scroll', this.handleScroll.bind(this), {
- passive: true,
- });
+ this.throttledHandleScroll = _.throttle(this.handleScroll, 100);
+ document.addEventListener('scroll', this.throttledHandleScroll);
+ },
+ beforeDestroy() {
+ document.removeEventListener('scroll', this.throttledHandleScroll);
},
methods: {
- ...mapActions(['setDiffViewType']),
+ ...mapActions(['setInlineDiffViewType', 'setParallelDiffViewType']),
+ pluralize,
handleScroll() {
if (!this.$refs.stickyBar) return;
@@ -88,7 +89,13 @@ export default {
truncatedDiffPath(path) {
const maxLength = 60;
- return path.length > maxLength ? `...${path.slice(0, maxLength)}` : path;
+ if (path.length > maxLength) {
+ const start = path.length - maxLength;
+ const end = start + maxLength;
+ return `...${path.slice(start, end)}`;
+ }
+
+ return path;
},
},
};
@@ -114,26 +121,26 @@ export default {
{{ __('Hide whitespace changes') }}
</a>
<div class="btn-group">
- <a
- @click.prevent="setDiffViewType()"
+ <button
+ type="button"
+ @click="setInlineDiffViewType"
:class="{ active: isInlineView }"
id="inline-diff-btn"
class="btn"
data-view-type="inline"
- href="#"
>
{{ __('Inline') }}
- </a>
- <a
- @click.prevent="setDiffViewType(true)"
+ </button>
+ <button
+ type="button"
+ @click="setParallelDiffViewType"
:class="{ active: isParallelView }"
id="parallel-diff-btn"
class="btn"
data-view-type="parallel"
- href="#"
>
{{ __('Side-by-side') }}
- </a>
+ </button>
</div>
</div>
@@ -146,7 +153,7 @@ export default {
aria-expanded="false"
>
<span>
- {{ n__('%d changed file', '%d changed files', diffFiles.length) }}
+ {{ pluralize(`${diffFiles.length} changed file`, diffFiles.length) }}
</span>
<icon
name="chevron-down"
@@ -239,11 +246,11 @@ export default {
>
with
<strong class="cgreen">
- {{ n__('%d addition', '%d additions', sumAddedLines) }}
+ {{ pluralize(`${sumAddedLines} addition`, sumAddedLines) }}
</strong>
and
<strong class="cred">
- {{ n__('%d deletion', '%d deletions', sumRemovedLines) }}
+ {{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }}
</strong>
</span>
@@ -251,7 +258,16 @@ export default {
v-show="activeFile"
class="prepend-left-5"
>
- {{ truncatedDiffPath(activeFile) }}
+ <strong class="prepend-right-5">
+ {{ truncatedDiffPath(activeFile) }}
+ </strong>
+ <clipboard-button
+ :text="activeFile"
+ :title="s__('Copy file name to clipboard')"
+ tooltip-placement="bottom"
+ tooltip-container="body"
+ class="btn btn-default btn-transparent btn-clipboard"
+ />
</span>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index a884594c202..c15941282ee 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -16,28 +16,52 @@ export default {
data() {
return {
isExpanded: true,
+ isActive: false,
};
},
mounted() {
- document.addEventListener('scroll', () => {
+ document.addEventListener('scroll', this.handleScroll);
+ },
+ beforeDestroy() {
+ document.removeEventListener('scroll', this.handleScroll);
+ },
+ methods: {
+ handleToggle() {
+ this.isExpanded = !this.isExpanded;
+ },
+ handleScroll() {
+ if (!this.updating) {
+ requestAnimationFrame(this.scrollUpdate);
+ this.updating = true;
+ }
+ },
+ scrollUpdate() {
+ const header = document.querySelector('.js-diff-files-changed');
+ if (!header) {
+ this.updating = false;
+ return;
+ }
+
const { top, bottom } = this.$el.getBoundingClientRect();
+ const {
+ top: topOfFixedHeader,
+ bottom: bottomOfFixedHeader,
+ } = header.getBoundingClientRect();
- const topOfFixedHeader = 100;
- const bottomOfFixedHeader = 120;
+ const headerOverlapsContent = top < topOfFixedHeader && bottom > bottomOfFixedHeader;
+ const fullyAboveHeader = bottom < bottomOfFixedHeader;
+ const fullyBelowHeader = top > topOfFixedHeader;
- if (top < topOfFixedHeader && bottom > bottomOfFixedHeader) {
+ if (headerOverlapsContent && !this.isActive) {
this.$emit('setActive');
- }
-
- if (top > bottomOfFixedHeader || bottom < bottomOfFixedHeader) {
+ this.isActive = true;
+ } else if (this.isActive && (fullyAboveHeader || fullyBelowHeader)) {
this.$emit('unsetActive');
+ this.isActive = false;
}
- });
- },
- methods: {
- handleToggle() {
- this.isExpanded = !this.isExpanded;
- },
+
+ this.updating = false
+ }
},
};
</script>
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 d8c57b8a629..ddec37a97b0 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -1,8 +1,15 @@
<script>
-import { MATCH_LINE_TYPE } from '../constants';
+import { mapState, mapGetters, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import { MATCH_LINE_TYPE, UNFOLD_COUNT, CONTEXT_LINE_TYPE } from '../constants';
+import * as utils from '../store/utils';
export default {
props: {
+ fileHash: {
+ type: String,
+ required: true,
+ },
lineType: {
type: String,
required: false,
@@ -23,49 +30,122 @@ export default {
required: false,
default: '',
},
+ metaData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
showCommentButton: {
type: Boolean,
required: false,
default: false,
},
+ contextLinesPath: {
+ type: String,
+ required: true,
+ },
+ isBottom: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ components: {
+ Icon,
},
computed: {
+ ...mapState({
+ diffViewType: state => state.diffs.diffViewType,
+ diffFiles: state => state.diffs.diffFiles,
+ }),
+ ...mapGetters(['isLoggedIn']),
isMatchLine() {
return this.lineType === MATCH_LINE_TYPE;
},
+ isContextLine() {
+ return this.lineType === CONTEXT_LINE_TYPE;
+ },
getLineHref() {
- return `#${this.lineCode}`;
+ return this.lineCode ? `#${this.lineCode}` : '#';
+ },
+ shouldShowCommentButton() {
+ return (
+ this.isLoggedIn &&
+ this.showCommentButton &&
+ !this.isMatchLine &&
+ !this.isContextLine
+ );
},
},
methods: {
+ ...mapActions(['loadMoreLines']),
handleCommentButton() {
this.$emit('showCommentForm', {
lineCode: this.lineCode,
linePosition: this.linePosition,
});
},
+ handleLoadMoreLines() {
+ const endpoint = this.contextLinesPath;
+ const oldLineNumber = this.metaData.oldPos || 0;
+ const newLineNumber = this.metaData.newPos || 0;
+ const offset = newLineNumber - oldLineNumber;
+ const bottom = this.isBottom;
+ const fileHash = this.fileHash;
+ const view = this.diffViewType;
+ let unfold = true;
+ let lineNumber = newLineNumber - 1;
+ let since = lineNumber - UNFOLD_COUNT;
+ let to = lineNumber;
+
+ if (bottom) {
+ lineNumber = newLineNumber + 1;
+ since = lineNumber;
+ to = lineNumber + UNFOLD_COUNT;
+ } else {
+ const diffFile = utils.findDiffFile(this.diffFiles, this.fileHash);
+ const indexForInline = utils.findIndexInInlineLines(
+ diffFile.highlightedDiffLines,
+ { oldLineNumber, newLineNumber },
+ );
+ const prevLine = diffFile.highlightedDiffLines[indexForInline - 2];
+ const prevLineNumber = (prevLine && prevLine.newLine) || 0;
+
+ if (since <= prevLineNumber + 1) {
+ since = prevLineNumber + 1;
+ unfold = false;
+ }
+ }
+
+ const params = { since, to, bottom, offset, unfold, view };
+ const lineNumbers = { oldLineNumber, newLineNumber };
+ this.loadMoreLines({ endpoint, params, lineNumbers, fileHash });
+ },
},
};
</script>
<template>
<div>
- <span v-if="isMatchLine">...</span>
+ <span
+ v-if="isMatchLine"
+ @click="handleLoadMoreLines"
+ class="context-cell"
+ >...</span>
<template
v-else
>
<button
- v-if="showCommentButton"
+ v-if="shouldShowCommentButton"
@click="handleCommentButton"
type="button"
class="add-diff-note js-add-diff-note-button"
title="Add a comment to this line"
>
- <i
- aria-hidden="true"
- class="fa fa-comment-o"
- >
- </i>
+ <icon
+ name="comment"
+ :size="12"
+ />
</button>
<a
v-if="lineNumber"
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index 32de9554ce5..82e4497cd15 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -1,6 +1,11 @@
<script>
import diffContentMixin from '../mixins/diff_content';
-import { MATCH_LINE_TYPE, LINE_HOVER_CLASS_NAME } from '../constants';
+import {
+ MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ LINE_HOVER_CLASS_NAME,
+ LINE_UNFOLD_CLASS_NAME,
+} from '../constants';
export default {
mixins: [diffContentMixin],
@@ -11,10 +16,13 @@ export default {
getLineClass(line) {
const isSameLine = this.hoveredLineCode === line.lineCode;
const isMatchLine = line.type === MATCH_LINE_TYPE;
+ const isContextLine = line.type === CONTEXT_LINE_TYPE;
return {
- [line.type]: true,
- [LINE_HOVER_CLASS_NAME]: isSameLine && !isMatchLine,
+ [line.type]: line.type,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn && isSameLine && !isMatchLine && !isContextLine,
+ [LINE_UNFOLD_CLASS_NAME]: this.isLoggedIn && isMatchLine,
};
},
},
@@ -32,7 +40,7 @@ export default {
<tr
:id="line.lineCode"
:key="line.lineCode"
- :class="line.type"
+ :class="getRowClass(line)"
class="line_holder"
@mouseover="handleMouse(line.lineCode, true)"
@mouseout="handleMouse(line.lineCode, false)"
@@ -42,10 +50,14 @@ export default {
class="diff-line-num old_line"
>
<diff-line-gutter-content
+ :file-hash="fileHash"
:line-type="line.type"
:line-code="line.lineCode"
:line-number="line.oldLine"
+ :meta-data="line.metaData"
:show-comment-button="true"
+ :context-lines-path="diffFile.contextLinesPath"
+ :is-bottom="index + 1 === diffLinesLength"
@showCommentForm="handleShowCommentForm"
/>
</td>
@@ -54,9 +66,13 @@ export default {
class="diff-line-num new_line"
>
<diff-line-gutter-content
+ :file-hash="fileHash"
:line-type="line.type"
:line-code="line.lineCode"
:line-number="line.newLine"
+ :meta-data="line.metaData"
+ :is-bottom="index + 1 === diffLinesLength"
+ :context-lines-path="diffFile.contextLinesPath"
/>
</td>
<td
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index 922f8e58dec..8ab3d7cdf6f 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -3,7 +3,9 @@ import diffContentMixin from '../mixins/diff_content';
import {
EMPTY_CELL_TYPE,
MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
LINE_HOVER_CLASS_NAME,
+ LINE_UNFOLD_CLASS_NAME,
} from '../constants';
export default {
@@ -31,14 +33,17 @@ export default {
},
getClassName(line, position) {
const { type, lineCode } = line[position];
+ const isMatchLine = type === MATCH_LINE_TYPE;
const isContextLine =
- type !== MATCH_LINE_TYPE && type !== EMPTY_CELL_TYPE;
+ !isMatchLine && type !== EMPTY_CELL_TYPE && type !== CONTEXT_LINE_TYPE;
const isSameLine = this.hoveredLineCode === lineCode;
const isSameSection = position === this.hoveredSection;
return {
[type]: type,
- [LINE_HOVER_CLASS_NAME]: isContextLine && isSameLine && isSameSection,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn && isContextLine && isSameLine && isSameSection,
+ [LINE_UNFOLD_CLASS_NAME]: this.isLoggedIn && isMatchLine,
};
},
handleMouse(e, line, isHover) {
@@ -72,6 +77,7 @@ export default {
>
<tr
:key="index"
+ :class="getRowClass(line)"
class="line_holder parallel"
@mouseover="handleMouse($event, line, true)"
@mouseout="handleMouse($event, line, false)"
@@ -82,10 +88,14 @@ export default {
class="diff-line-num old_line"
>
<diff-line-gutter-content
+ :file-hash="fileHash"
:line-type="line.left.type"
:line-code="line.left.lineCode"
:line-number="line.left.oldLine"
+ :meta-data="line.left.metaData"
:show-comment-button="true"
+ :context-lines-path="diffFile.contextLinesPath"
+ :is-bottom="index + 1 === diffLinesLength"
line-position="left"
@showCommentForm="handleShowCommentForm"
/>
@@ -103,10 +113,14 @@ export default {
class="diff-line-num new_line"
>
<diff-line-gutter-content
+ :file-hash="fileHash"
:line-type="line.right.type"
:line-code="line.right.lineCode"
:line-number="line.right.newLine"
+ :meta-data="line.right.metaData"
:show-comment-button="true"
+ :context-lines-path="diffFile.contextLinesPath"
+ :is-bottom="index + 1 === diffLinesLength"
line-position="right"
@showCommentForm="handleShowCommentForm"
/>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index fa8cccbc46c..f93e270575f 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -1,9 +1,12 @@
export const INLINE_DIFF_VIEW_TYPE = 'inline';
export const PARALLEL_DIFF_VIEW_TYPE = 'parallel';
export const MATCH_LINE_TYPE = 'match';
+export const CONTEXT_LINE_TYPE = 'context';
export const DIFF_VIEW_COOKIE_NAME = 'diff_view';
export const EMPTY_CELL_TYPE = 'empty-cell';
export const LINE_HOVER_CLASS_NAME = 'is-over';
+export const LINE_UNFOLD_CLASS_NAME = 'unfold js-unfold';
+export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded';
export const COMMENT_FORM_TYPE = 'commentForm';
export const LINE_POSITION_LEFT = 'left';
export const LINE_POSITION_RIGHT = 'right';
@@ -11,3 +14,4 @@ export const TEXT_DIFF_POSITION_TYPE = 'text';
export const DIFF_NOTE_TYPE = 'DiffNote';
export const NEW_LINE_TYPE = 'new';
export const OLD_LINE_TYPE = 'old';
+export const UNFOLD_COUNT = 20;
diff --git a/app/assets/javascripts/diffs/mixins/diff_content.js b/app/assets/javascripts/diffs/mixins/diff_content.js
index 11db6a10a3e..bb2ece68bc0 100644
--- a/app/assets/javascripts/diffs/mixins/diff_content.js
+++ b/app/assets/javascripts/diffs/mixins/diff_content.js
@@ -2,6 +2,7 @@ import { mapGetters, mapActions } from 'vuex';
import diffDiscussions from '../components/diff_discussions.vue';
import diffLineGutterContent from '../components/diff_line_gutter_content.vue';
import diffLineNoteForm from '../components/diff_line_note_form.vue';
+import { CONTEXT_LINE_TYPE, CONTEXT_LINE_CLASS_NAME } from '../constants';
export default {
props: {
@@ -26,7 +27,7 @@ export default {
diffLineGutterContent,
},
computed: {
- ...mapGetters(['discussionsByLineCode']),
+ ...mapGetters(['discussionsByLineCode', 'isLoggedIn']),
userColorScheme() {
return window.gon.user_color_scheme;
},
@@ -47,9 +48,25 @@ export default {
return line;
});
},
+ diffLinesLength() {
+ return this.normalizedDiffLines.length;
+ },
+ fileHash() {
+ return this.diffFile.fileHash;
+ },
},
methods: {
...mapActions(['showCommentForm', 'cancelCommentForm']),
+ getRowClass(line) {
+ const isContextLine = line.left
+ ? line.left.type === CONTEXT_LINE_TYPE
+ : line.type === CONTEXT_LINE_TYPE;
+
+ return {
+ [line.type]: line.type,
+ [CONTEXT_LINE_CLASS_NAME]: isContextLine,
+ };
+ },
trimFirstChar(line) {
if (!line.richText) {
return line;
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 321967a5479..3185c90cecb 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import VueResource from 'vue-resource';
+import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
import { handleLocationHash } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
@@ -9,8 +9,6 @@ import {
DIFF_VIEW_COOKIE_NAME,
} from '../constants';
-Vue.use(VueResource);
-
export const setEndpoint = ({ commit }, endpoint) => {
commit(types.SET_ENDPOINT, endpoint);
};
@@ -22,22 +20,24 @@ export const setLoadingState = ({ commit }, state) => {
export const fetchDiffFiles = ({ state, commit }) => {
commit(types.SET_LOADING, true);
- return Vue.http
+ return axios
.get(state.endpoint)
- .then(res => res.json())
.then(res => {
commit(types.SET_LOADING, false);
- commit(types.SET_DIFF_FILES, res.diff_files);
+ commit(types.SET_DIFF_FILES, res.data.diff_files);
return Vue.nextTick();
})
.then(handleLocationHash);
};
-export const setDiffViewType = ({ commit }, isParallel) => {
- const type = isParallel ? PARALLEL_DIFF_VIEW_TYPE : INLINE_DIFF_VIEW_TYPE;
+export const setInlineDiffViewType = ({ commit }) => {
+ commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE);
+ Cookies.set(DIFF_VIEW_COOKIE_NAME, INLINE_DIFF_VIEW_TYPE);
+};
- commit(types.SET_DIFF_VIEW_TYPE, type);
- Cookies.set(DIFF_VIEW_COOKIE_NAME, type);
+export const setParallelDiffViewType = ({ commit }) => {
+ commit(types.SET_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE);
+ Cookies.set(DIFF_VIEW_COOKIE_NAME, PARALLEL_DIFF_VIEW_TYPE);
};
export const showCommentForm = ({ commit }, params) => {
@@ -47,3 +47,18 @@ export const showCommentForm = ({ commit }, params) => {
export const cancelCommentForm = ({ commit }, params) => {
commit(types.REMOVE_COMMENT_FORM_LINE, params);
};
+
+export const loadMoreLines = ({ commit }, options) => {
+ const { endpoint, params, lineNumbers, fileHash } = options;
+
+ return axios.get(endpoint, { params }).then(res => {
+ const contextLines = res.data || [];
+
+ commit(types.ADD_CONTEXT_LINES, {
+ lineNumbers,
+ contextLines,
+ params,
+ fileHash,
+ });
+ });
+};
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index cec771fc04e..805fa510a56 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -4,3 +4,4 @@ export const SET_DIFF_FILES = 'SET_DIFF_FILES';
export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE';
export const ADD_COMMENT_FORM_LINE = 'ADD_COMMENT_FORM_LINE';
export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE';
+export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index d83a966fed9..8d7c26ed212 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -8,8 +8,8 @@ export default {
Object.assign(state, { endpoint });
},
- [types.SET_LOADING](state, loadingState) {
- Object.assign(state, { isLoading: loadingState });
+ [types.SET_LOADING](state, isLoading) {
+ Object.assign(state, { isLoading });
},
[types.SET_DIFF_FILES](state, diffFiles) {
@@ -20,8 +20,8 @@ export default {
});
},
- [types.SET_DIFF_VIEW_TYPE](state, type) {
- Object.assign(state, { diffViewType: type });
+ [types.SET_DIFF_VIEW_TYPE](state, diffViewType) {
+ Object.assign(state, { diffViewType });
},
[types.ADD_COMMENT_FORM_LINE](state, { diffLines, lineCode, linePosition }) {
@@ -109,4 +109,21 @@ export default {
}
}
},
+
+ [types.ADD_CONTEXT_LINES](state, options) {
+ const { lineNumbers, contextLines, fileHash } = options;
+ const { bottom } = options.params;
+ const diffFile = utils.findDiffFile(state.diffFiles, fileHash);
+ const { highlightedDiffLines, parallelDiffLines } = diffFile;
+
+ utils.removeMatchLine(diffFile, lineNumbers, bottom);
+ const lines = utils.addLineReferences(contextLines, lineNumbers, bottom);
+ utils.addContextLines({
+ inlineLines: highlightedDiffLines,
+ parallelLines: parallelDiffLines,
+ contextLines: lines,
+ bottom,
+ lineNumbers,
+ });
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 9aa44cd4fdb..f7a4eddf60d 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -1,4 +1,5 @@
import _ from 'underscore';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
@@ -6,6 +7,7 @@ import {
DIFF_NOTE_TYPE,
NEW_LINE_TYPE,
OLD_LINE_TYPE,
+ MATCH_LINE_TYPE,
} from '../constants';
export const findDiffLineIndex = options => {
@@ -26,6 +28,9 @@ export const findDiffLineIndex = options => {
});
};
+export const findDiffFile = (files, hash) =>
+ files.filter(file => file.fileHash === hash)[0];
+
export const getReversePosition = linePosition => {
if (linePosition === LINE_POSITION_RIGHT) {
return LINE_POSITION_LEFT;
@@ -81,3 +86,101 @@ export const getNoteFormData = params => {
data: postData,
};
};
+
+export const findIndexInInlineLines = (lines, lineNumbers) => {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+
+ return _.findIndex(
+ lines,
+ line => line.oldLine === oldLineNumber && line.newLine === newLineNumber,
+ );
+};
+
+export const findIndexInParallelLines = (lines, lineNumbers) => {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+
+ return _.findIndex(
+ lines,
+ line =>
+ line.left &&
+ line.right &&
+ line.left.oldLine === oldLineNumber &&
+ line.right.newLine === newLineNumber,
+ );
+};
+
+export const removeMatchLine = (diffFile, lineNumbers, bottom) => {
+ const indexForInline = findIndexInInlineLines(
+ diffFile.highlightedDiffLines,
+ lineNumbers,
+ );
+
+ const indexForParallel = findIndexInParallelLines(
+ diffFile.parallelDiffLines,
+ lineNumbers,
+ );
+
+ const factor = bottom ? 1 : -1;
+
+ diffFile.highlightedDiffLines.splice(indexForInline + factor, 1);
+ diffFile.parallelDiffLines.splice(indexForParallel + factor, 1);
+};
+
+export const addLineReferences = (lines, lineNumbers, bottom) => {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+ const lineCount = lines.length;
+ let matchLineIndex = -1;
+
+ const linesWithNumbers = lines.map((l, index) => {
+ const line = convertObjectPropsToCamelCase(l);
+
+ if (line.type === MATCH_LINE_TYPE) {
+ matchLineIndex = index;
+ } else {
+ Object.assign(line, {
+ oldLine: bottom
+ ? oldLineNumber + index + 1
+ : oldLineNumber + index - lineCount,
+ newLine: bottom
+ ? newLineNumber + index + 1
+ : newLineNumber + index - lineCount,
+ });
+ }
+
+ return line;
+ });
+
+ if (matchLineIndex > -1) {
+ const line = linesWithNumbers[matchLineIndex];
+ const targetLine = bottom
+ ? linesWithNumbers[matchLineIndex - 1]
+ : linesWithNumbers[matchLineIndex + 1];
+
+ Object.assign(line, {
+ metaData: {
+ oldPos: targetLine.oldLine,
+ newPos: targetLine.newLine,
+ },
+ });
+ }
+
+ return linesWithNumbers;
+};
+
+export const addContextLines = options => {
+ const { inlineLines, parallelLines, contextLines, lineNumbers } = options;
+ const inlineIndex = findIndexInInlineLines(inlineLines, lineNumbers);
+ const parallelIndex = findIndexInParallelLines(parallelLines, lineNumbers);
+ const normalizedParallelLines = contextLines.map(line => ({
+ left: line,
+ right: line,
+ }));
+
+ if (options.bottom) {
+ inlineLines.push(...contextLines);
+ parallelLines.push(...normalizedParallelLines);
+ } else {
+ inlineLines.splice(inlineIndex, 0, ...contextLines);
+ parallelLines.splice(parallelIndex, 0, ...normalizedParallelLines);
+ }
+};
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 081c8c48365..606c836acee 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -5,7 +5,10 @@ import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
import { isObject } from './type_utility';
-export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
+export const getPagePath = (index = 0) =>
+ $('body')
+ .attr('data-page')
+ .split(':')[index];
export const isInGroupsPage = () => getPagePath() === 'groups';
@@ -35,32 +38,39 @@ export const checkPageAndAction = (page, action) => {
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
-export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
-export const ajaxGet = url => axios.get(url, {
- params: { format: 'js' },
- responseType: 'text',
-}).then(({ data }) => {
- $.globalEval(data);
-});
+export const ajaxGet = url =>
+ axios
+ .get(url, {
+ params: { format: 'js' },
+ responseType: 'text',
+ })
+ .then(({ data }) => {
+ $.globalEval(data);
+ });
-export const rstrip = (val) => {
+export const rstrip = val => {
if (val) {
return val.replace(/\s+$/, '');
}
return val;
};
-export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
+export const updateTooltipTitle = ($tooltipEl, newTitle) =>
+ $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
-export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => {
+export const disableButtonIfEmptyField = (
+ fieldSelector,
+ buttonSelector,
+ eventName = 'input',
+) => {
const field = $(fieldSelector);
const closestSubmit = field.closest('form').find(buttonSelector);
if (rstrip(field.val()) === '') {
closestSubmit.disable();
}
// eslint-disable-next-line func-names
- return field.on(eventName, function () {
+ return field.on(eventName, function() {
if (rstrip($(this).val()) === '') {
return closestSubmit.disable();
}
@@ -77,7 +87,9 @@ export const handleLocationHash = () => {
// This is required to handle non-unicode characters in hash
hash = decodeURIComponent(hash);
- const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`);
+ const target =
+ document.getElementById(hash) ||
+ document.getElementById(`user-content-${hash}`);
const fixedTabs = document.querySelector('.js-tabs-affix');
const fixedDiffStats = document.querySelector('.js-diff-files-changed');
const fixedNav = document.querySelector('.navbar-gitlab');
@@ -102,7 +114,7 @@ export const handleLocationHash = () => {
// Check if element scrolled into viewport from above or below
// Courtesy http://stackoverflow.com/a/7557433/414749
-export const isInViewport = (el) => {
+export const isInViewport = el => {
const rect = el.getBoundingClientRect();
return (
@@ -113,25 +125,31 @@ export const isInViewport = (el) => {
);
};
-export const parseUrl = (url) => {
+export const parseUrl = url => {
const parser = document.createElement('a');
parser.href = url;
return parser;
};
-export const parseUrlPathname = (url) => {
+export const parseUrlPathname = url => {
const parsedUrl = parseUrl(url);
// parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
// We have to make sure we always have an absolute path.
- return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`;
+ return parsedUrl.pathname.charAt(0) === '/'
+ ? parsedUrl.pathname
+ : `/${parsedUrl.pathname}`;
};
// We can trust that each param has one & since values containing & will be encoded
// Remove the first character of search as it is always ?
-export const getUrlParamsArray = () => window.location.search.slice(1).split('&').map((param) => {
- const split = param.split('=');
- return [decodeURI(split[0]), split[1]].join('=');
-});
+export const getUrlParamsArray = () =>
+ window.location.search
+ .slice(1)
+ .split('&')
+ .map(param => {
+ const split = param.split('=');
+ return [decodeURI(split[0]), split[1]].join('=');
+ });
export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
@@ -141,7 +159,7 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
-export const scrollToElement = (element) => {
+export const scrollToElement = element => {
let $el = element;
if (!(element instanceof $)) {
$el = $(element);
@@ -150,9 +168,12 @@ export const scrollToElement = (element) => {
const mrTabsHeight = $('.merge-request-tabs').height() || 0;
const headerHeight = $('.navbar-gitlab').height() || 0;
- return $('body, html').animate({
- scrollTop: top - mrTabsHeight - headerHeight,
- }, 200);
+ return $('body, html').animate(
+ {
+ scrollTop: top - mrTabsHeight - headerHeight,
+ },
+ 200,
+ );
};
/**
@@ -191,13 +212,15 @@ export const insertText = (target, text) => {
const textBefore = value.substring(0, selectionStart);
const textAfter = value.substring(selectionEnd, value.length);
- const insertedText = text instanceof Function ? text(textBefore, textAfter) : text;
+ const insertedText =
+ text instanceof Function ? text(textBefore, textAfter) : text;
const newText = textBefore + insertedText + textAfter;
// eslint-disable-next-line no-param-reassign
target.value = newText;
// eslint-disable-next-line no-param-reassign
- target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
+ target.selectionStart = target.selectionEnd =
+ selectionStart + insertedText.length;
// Trigger autosave
target.dispatchEvent(new Event('input'));
@@ -209,7 +232,8 @@ export const insertText = (target, text) => {
};
export const nodeMatchesSelector = (node, selector) => {
- const matches = Element.prototype.matches ||
+ const matches =
+ Element.prototype.matches ||
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
@@ -238,10 +262,10 @@ export const nodeMatchesSelector = (node, selector) => {
this will take in the headers from an API response and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
-export const normalizeHeaders = (headers) => {
+export const normalizeHeaders = headers => {
const upperCaseHeaders = {};
- Object.keys(headers || {}).forEach((e) => {
+ Object.keys(headers || {}).forEach(e => {
upperCaseHeaders[e.toUpperCase()] = headers[e];
});
@@ -252,11 +276,11 @@ export const normalizeHeaders = (headers) => {
this will take in the getAllResponseHeaders result and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
-export const normalizeCRLFHeaders = (headers) => {
+export const normalizeCRLFHeaders = headers => {
const headersObject = {};
const headersArray = headers.split('\n');
- headersArray.forEach((header) => {
+ headersArray.forEach(header => {
const keyValue = header.split(': ');
headersObject[keyValue[0]] = keyValue[1];
});
@@ -292,15 +316,13 @@ export const parseIntPagination = paginationInformation => ({
export const parseQueryStringIntoObject = (query = '') => {
if (query === '') return {};
- return query
- .split('&')
- .reduce((acc, element) => {
- const val = element.split('=');
- Object.assign(acc, {
- [val[0]]: decodeURIComponent(val[1]),
- });
- return acc;
- }, {});
+ return query.split('&').reduce((acc, element) => {
+ const val = element.split('=');
+ Object.assign(acc, {
+ [val[0]]: decodeURIComponent(val[1]),
+ });
+ return acc;
+ }, {});
};
/**
@@ -309,9 +331,13 @@ export const parseQueryStringIntoObject = (query = '') => {
*
* @param {Object} params
*/
-export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&');
+export const objectToQueryString = (params = {}) =>
+ Object.keys(params)
+ .map(param => `${param}=${params[param]}`)
+ .join('&');
-export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
+export const buildUrlWithCurrentLocation = param =>
+ param ? `${window.location.pathname}${param}` : window.location.pathname;
/**
* Based on the current location and the string parameters provided
@@ -319,7 +345,7 @@ export const buildUrlWithCurrentLocation = param => (param ? `${window.location.
*
* @param {String} param
*/
-export const historyPushState = (newUrl) => {
+export const historyPushState = newUrl => {
window.history.pushState({}, document.title, newUrl);
};
@@ -368,7 +394,7 @@ export const backOff = (fn, timeout = 60000) => {
let timeElapsed = 0;
return new Promise((resolve, reject) => {
- const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
+ const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => {
if (timeElapsed < timeout) {
@@ -384,7 +410,7 @@ export const backOff = (fn, timeout = 60000) => {
});
};
-export const setFavicon = (faviconPath) => {
+export const setFavicon = faviconPath => {
const faviconEl = document.getElementById('favicon');
if (faviconEl && faviconPath) {
faviconEl.setAttribute('href', faviconPath);
@@ -400,7 +426,8 @@ export const resetFavicon = () => {
};
export const setCiStatusFavicon = pageUrl =>
- axios.get(pageUrl)
+ axios
+ .get(pageUrl)
.then(({ data }) => {
if (data && data.favicon) {
setFavicon(data.favicon);
@@ -413,7 +440,9 @@ export const setCiStatusFavicon = pageUrl =>
export const spriteIcon = (icon, className = '') => {
const classAttribute = className.length > 0 ? `class="${className}"` : '';
- return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
+ return `<svg ${classAttribute}><use xlink:href="${
+ gon.sprite_icons
+ }#${icon}" /></svg>`;
};
/**
@@ -435,7 +464,10 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
const val = obj[prop];
if (options.deep && (isObject(val) || Array.isArray(val))) {
- result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options);
+ result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(
+ val,
+ options,
+ );
} else {
result[convertToCamelCase(prop)] = obj[prop];
}
@@ -443,15 +475,18 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
}, initial);
};
-export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
+export const imagePath = imgUrl =>
+ `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
$(selector).on('focusin', function selectOnFocusCallback() {
- $(this).select().one('mouseup', (e) => {
- e.preventDefault();
- });
+ $(this)
+ .select()
+ .one('mouseup', e => {
+ e.preventDefault();
+ });
});
};
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index a98b0b1c0b1..9f6af8c7a0d 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -12,7 +12,6 @@ import {
parseUrlPathname,
handleLocationHash,
isMetaClick,
- hasVueMRDiscussionsCookie,
} from './lib/utils/common_utils';
import { getLocationHash } from './lib/utils/url_utility';
import initDiscussionTab from './image_diff/init_discussion_tab';
@@ -71,12 +70,12 @@ import Notes from './notes';
let location = window.location;
export default class MergeRequestTabs {
-
constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab');
const peek = document.getElementById('peek');
const paddingTop = 16;
+ this.commitsTab = document.querySelector('.tab-content .commits.tab-pane');
this.diffsLoaded = false;
this.pipelinesLoaded = false;
@@ -109,21 +108,27 @@ export default class MergeRequestTabs {
bindEvents() {
$(document)
- .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
+ .on(
+ 'shown.bs.tab',
+ '.merge-request-tabs a[data-toggle="tab"]',
+ this.tabShown,
+ )
.on('click', '.js-show-tab', this.showTab);
- $('.merge-request-tabs a[data-toggle="tab"]')
- .on('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tab"]').on('click', this.clickTab);
}
// Used in tests
unbindEvents() {
$(document)
- .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
+ .off(
+ 'shown.bs.tab',
+ '.merge-request-tabs a[data-toggle="tab"]',
+ this.tabShown,
+ )
.off('click', '.js-show-tab', this.showTab);
- $('.merge-request-tabs a[data-toggle="tab"]')
- .off('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tab"]').off('click', this.clickTab);
}
destroyPipelinesView() {
@@ -159,10 +164,6 @@ export default class MergeRequestTabs {
this.resetViewContainer();
this.destroyPipelinesView();
} else if (this.isDiffAction(action)) {
- if (!hasVueMRDiscussionsCookie()) {
- this.loadDiff($target.attr('href'));
- }
-
if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
@@ -170,7 +171,7 @@ export default class MergeRequestTabs {
this.expandViewContainer();
}
this.destroyPipelinesView();
- $('.tab-content .commits.tab-pane').removeClass('active');
+ this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') {
this.resetViewContainer();
this.mountPipelinesView();
@@ -192,10 +193,9 @@ export default class MergeRequestTabs {
scrollToElement(container) {
if (location.hash) {
- const offset = 0 - (
- $('.navbar-gitlab').outerHeight() +
- $('.js-tabs-affix').outerHeight()
- );
+ const offset =
+ 0 -
+ ($('.navbar-gitlab').outerHeight() + $('.js-tabs-affix').outerHeight());
const $el = $(`${container} ${location.hash}:not(.match)`);
if ($el.length) {
$.scrollTo($el[0], { offset });
@@ -233,7 +233,10 @@ export default class MergeRequestTabs {
this.currentAction = action;
// Remove a trailing '/commits' '/diffs' '/pipelines'
- let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, '');
+ let newState = location.pathname.replace(
+ /\/(commits|diffs|pipelines)(\.html)?\/?$/,
+ '',
+ );
// Append the new action if we're on a tab other than 'notes'
if (this.currentAction !== 'show' && this.currentAction !== 'new') {
@@ -249,9 +252,13 @@ export default class MergeRequestTabs {
// Turbolinks' history.
//
// See https://github.com/rails/turbolinks/issues/363
- window.history.replaceState({
- url: newState,
- }, document.title, newState);
+ window.history.replaceState(
+ {
+ url: newState,
+ },
+ document.title,
+ newState,
+ );
return newState;
}
@@ -267,7 +274,8 @@ export default class MergeRequestTabs {
this.toggleLoading(true);
- axios.get(`${source}.json`)
+ axios
+ .get(`${source}.json`)
.then(({ data }) => {
document.querySelector('div#commits').innerHTML = data.html;
localTimeAgo($('.js-timeago', 'div#commits'));
@@ -283,7 +291,9 @@ export default class MergeRequestTabs {
}
mountPipelinesView() {
- const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
+ const pipelineTableViewEl = document.querySelector(
+ '#commit-pipeline-table-view',
+ );
const CommitPipelinesTable = gl.CommitPipelinesTable;
this.commitPipelinesTable = new CommitPipelinesTable({
propsData: {
@@ -314,7 +324,8 @@ export default class MergeRequestTabs {
this.toggleLoading(true);
- axios.get(`${urlPathname}.json${location.search}`)
+ axios
+ .get(`${urlPathname}.json${location.search}`)
.then(({ data }) => {
const $container = $('#diffs');
$container.html(data.html);
@@ -328,7 +339,10 @@ export default class MergeRequestTabs {
localTimeAgo($('.js-timeago', 'div#diffs'));
syntaxHighlight($('#diffs .js-syntax-highlight'));
- if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
+ if (
+ this.diffViewType() === 'parallel' &&
+ this.isDiffAction(this.currentAction)
+ ) {
this.expandViewContainer();
}
this.diffsLoaded = true;
@@ -342,9 +356,10 @@ export default class MergeRequestTabs {
forkButtons: $(el).find('.js-fork-suggestion-button'),
cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
- actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
- })
- .init();
+ actionTextPieces: $(el).find(
+ '.js-file-fork-suggestion-section-action',
+ ),
+ }).init();
});
// Scroll any linked note into view
@@ -399,8 +414,10 @@ export default class MergeRequestTabs {
resetViewContainer() {
if (this.fixedLayoutPref !== null) {
- $('.content-wrapper .container-fluid')
- .toggleClass('container-limited', this.fixedLayoutPref);
+ $('.content-wrapper .container-fluid').toggleClass(
+ 'container-limited',
+ this.fixedLayoutPref,
+ );
}
}
@@ -449,12 +466,12 @@ export default class MergeRequestTabs {
const $diffTabs = $('#diff-notes-app');
- $tabs.off('affix.bs.affix affix-top.bs.affix')
+ $tabs
+ .off('affix.bs.affix affix-top.bs.affix')
.affix({
offset: {
- top: () => (
- $diffTabs.offset().top - $tabs.height() - $fixedNav.height()
- ),
+ top: () =>
+ $diffTabs.offset().top - $tabs.height() - $fixedNav.height(),
},
})
.on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index 33492f7be65..c09952364f4 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import Vuex, { mapActions, mapGetters } from 'vuex';
+import Vuex, { mapActions, mapState } from 'vuex';
import notesApp from '../notes/components/notes_app.vue';
import diffsApp from '../diffs/components/app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue';
@@ -14,12 +14,14 @@ const store = new Vuex.Store({
modules: {
page: mrPageStoreConfig,
notes: notesStoreConfig,
+ diffs: diffsStoreConfig,
},
});
export default function initMrNotes() {
const mrShowNode = document.querySelector('.merge-request');
- window.mergeRequest = new MergeRequest({
+ // eslint-disable-next-line no-new
+ new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
@@ -42,7 +44,9 @@ export default function initMrNotes() {
};
},
computed: {
- ...mapGetters(['activeTab']),
+ ...mapState({
+ activeTab: state => state.page.activeTab,
+ }),
},
mounted() {
this.setActiveTab(window.mrTabs.getCurrentAction());
@@ -95,7 +99,9 @@ export default function initMrNotes() {
};
},
computed: {
- ...mapGetters(['activeTab']),
+ ...mapState({
+ activeTab: state => state.page.activeTab,
+ }),
},
render(createElement) {
return createElement('diffs-app', {
diff --git a/app/assets/javascripts/mr_notes/stores/getters.js b/app/assets/javascripts/mr_notes/stores/getters.js
index c68a84ce7af..b10e9f9f9f1 100644
--- a/app/assets/javascripts/mr_notes/stores/getters.js
+++ b/app/assets/javascripts/mr_notes/stores/getters.js
@@ -1,5 +1,5 @@
export default {
- activeTab(state) {
- return state.activeTab;
+ isLoggedIn(state, getters) {
+ return !!getters.getUserData.id;
},
};
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index b37b30efdc0..e2b3f6b1806 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -33,7 +33,7 @@ import {
getPagePath,
scrollToElement,
isMetaKey,
- hasVueMRDiscussionsCookie,
+ isInMRPage,
} from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
@@ -48,21 +48,9 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export default class Notes {
- static initialize(
- notes_url,
- note_ids,
- last_fetched_at,
- view,
- enableGFM = true,
- ) {
+ static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
if (!this.instance) {
- this.instance = new Notes(
- notes_url,
- note_ids,
- last_fetched_at,
- view,
- enableGFM,
- );
+ this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
}
}
@@ -105,9 +93,7 @@ export default class Notes {
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
- this.$wrapperEl = hasVueMRDiscussionsCookie()
- ? $(document).find('.diffs')
- : $(document);
+ this.$wrapperEl = isInMRPage() ? $(document).find('.diffs') : $(document);
this.cleanBinding();
this.addBinding();
this.setPollingInterval();
@@ -147,55 +133,27 @@ export default class Notes {
// Reopen and close actions for Issue/MR combined with note form submit
this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment);
this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment);
- this.$wrapperEl.on(
- 'keyup input',
- '.js-note-text',
- this.updateTargetButtons,
- );
+ this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons);
// resolve a discussion
this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general)
this.$wrapperEl.on('click', '.js-note-delete', this.removeNote);
// delete note attachment
- this.$wrapperEl.on(
- 'click',
- '.js-note-attachment-delete',
- this.removeAttachment,
- );
+ this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment);
// reset main target form when clicking discard
this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected
- this.$wrapperEl.on(
- 'change',
- '.js-note-attachment-input',
- this.updateFormAttachment,
- );
+ this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
- this.$wrapperEl.on(
- 'click',
- '.js-discussion-reply-button',
- this.onReplyToDiscussionNote,
- );
+ this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
- this.$wrapperEl.on(
- 'click',
- '.js-add-image-diff-note-button',
- this.onAddImageDiffNote,
- );
+ this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
// hide diff note form
- this.$wrapperEl.on(
- 'click',
- '.js-close-discussion-note-form',
- this.cancelDiscussionForm,
- );
+ this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list
- this.$wrapperEl.on(
- 'click',
- '.system-note-commit-list-toggler',
- this.toggleCommitList,
- );
+ this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
// fetch notes when tab becomes visible
@@ -204,16 +162,8 @@ export default class Notes {
this.$wrapperEl.on('issuable:change', this.refresh);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote);
- this.$wrapperEl.on(
- 'ajax:success',
- '.js-discussion-note-form',
- this.addDiscussionNote,
- );
- this.$wrapperEl.on(
- 'ajax:success',
- '.js-main-target-form',
- this.resetMainTargetForm,
- );
+ this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
+ this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
this.$wrapperEl.on(
'ajax:complete',
'.js-main-target-form',
@@ -223,8 +173,6 @@ export default class Notes {
this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText);
// When the URL fragment/hash has changed, `#note_xxx`
$(window).on('hashchange', this.onHashChange);
- this.boundGetContent = this.getContent.bind(this);
- this.eventsBound = true;
}
cleanBinding() {
@@ -251,16 +199,10 @@ export default class Notes {
}
static initCommentTypeToggle(form) {
- const dropdownTrigger = form.querySelector(
- '.js-comment-type-dropdown .dropdown-toggle',
- );
- const dropdownList = form.querySelector(
- '.js-comment-type-dropdown .dropdown-menu',
- );
+ const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle');
+ const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu');
const noteTypeInput = form.querySelector('#note_type');
- const submitButton = form.querySelector(
- '.js-comment-type-dropdown .js-comment-submit-button',
- );
+ const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button');
const closeButton = form.querySelector('.js-note-target-close');
const reopenButton = form.querySelector('.js-note-target-reopen');
@@ -296,9 +238,7 @@ export default class Notes {
return;
}
myLastNote = $(
- `li.note[data-author-id='${
- gon.current_user_id
- }'][data-editable]:last`,
+ `li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`,
$textarea.closest('.note, .notes_holder, #notes'),
);
if (myLastNote.length) {
@@ -311,9 +251,7 @@ export default class Notes {
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
if (discussionNoteForm.length) {
if ($textarea.val() !== '') {
- if (
- !confirm('Are you sure you want to cancel creating this comment?')
- ) {
+ if (!confirm('Are you sure you want to cancel creating this comment?')) {
return;
}
}
@@ -325,9 +263,7 @@ export default class Notes {
originalText = $textarea.closest('form').data('originalNote');
newText = $textarea.val();
if (originalText !== newText) {
- if (
- !confirm('Are you sure you want to cancel editing this comment?')
- ) {
+ if (!confirm('Are you sure you want to cancel editing this comment?')) {
return;
}
}
@@ -395,8 +331,7 @@ export default class Notes {
if (shouldReset == null) {
shouldReset = true;
}
- nthInterval =
- this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
+ nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
if (shouldReset) {
this.pollingInterval = this.basePollingInterval;
} else if (this.pollingInterval < nthInterval) {
@@ -417,10 +352,7 @@ export default class Notes {
loadAwardsHandler()
.then(awardsHandler => {
- awardsHandler.addAwardToEmojiBar(
- votesBlock,
- noteEntity.commands_changes.emoji_award,
- );
+ awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
awardsHandler.scrollToAwards();
})
.catch(() => {
@@ -470,17 +402,10 @@ export default class Notes {
if (!noteEntity.valid) {
if (noteEntity.errors && noteEntity.errors.commands_only) {
- if (
- noteEntity.commands_changes &&
- Object.keys(noteEntity.commands_changes).length > 0
- ) {
+ if (noteEntity.commands_changes && Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
- this.addFlash(
- noteEntity.errors.commands_only,
- 'notice',
- this.parentTimeline.get(0),
- );
+ this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0));
this.refresh();
}
return;
@@ -488,7 +413,7 @@ export default class Notes {
const $note = $notesList.find(`#note_${noteEntity.id}`);
if (Notes.isNewNote(noteEntity, this.note_ids)) {
- if (hasVueMRDiscussionsCookie()) {
+ if (isInMRPage()) {
return;
}
@@ -516,8 +441,7 @@ export default class Notes {
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
const isTextareaUntouched =
- currentContent === initialContent ||
- currentContent === sanitizedNoteNote;
+ currentContent === initialContent || currentContent === sanitizedNoteNote;
if (isEditing && isTextareaUntouched) {
$textarea.val(noteEntity.note);
@@ -547,13 +471,7 @@ export default class Notes {
}
this.note_ids.push(noteEntity.id);
- form =
- $form ||
- $(
- `.js-discussion-note-form[data-discussion-id="${
- noteEntity.discussion_id
- }"]`,
- );
+ form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row =
form.length || !noteEntity.discussion_line_code
? form.closest('tr')
@@ -569,9 +487,7 @@ export default class Notes {
.first()
.find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
- discussionContainer = $(
- `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
- );
+ discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes');
}
@@ -579,18 +495,12 @@ export default class Notes {
if (noteEntity.diff_discussion_html) {
var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
- if (
- !this.isParallelView() ||
- row.hasClass('js-temp-notes-holder') ||
- noteEntity.on_image
- ) {
+ if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
// insert the note and the reply button after the temp row
row.after($discussion);
} else {
// Merge new discussion HTML in
- var $notes = $discussion.find(
- `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
- );
+ var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
var contentContainerClass =
'.' +
$notes
@@ -604,28 +514,12 @@ export default class Notes {
.append($notes.closest('.content').children());
}
}
- // Init discussion on 'Discussion' page if it is merge request page
- const page = $('body').attr('data-page');
- if (
- (page && page.indexOf('projects:merge_request') !== -1) ||
- !noteEntity.diff_discussion_html
- ) {
- if (!hasVueMRDiscussionsCookie()) {
- Notes.animateAppendNote(
- noteEntity.discussion_html,
- $('.main-notes-list'),
- );
- }
- }
} else {
// append new note to all matching discussions
Notes.animateAppendNote(noteEntity.html, discussionContainer);
}
- if (
- typeof gl.diffNotesCompileComponents !== 'undefined' &&
- noteEntity.discussion_resolvable
- ) {
+ if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
@@ -934,9 +828,7 @@ export default class Notes {
form.removeClass('current-note-edit-form');
form.find('.js-finish-edit-warning').hide();
// Replace markdown textarea text with original note text.
- return form
- .find('.js-note-text')
- .val(form.find('form.edit-note').data('originalNote'));
+ return form.find('.js-note-text').val(form.find('form.edit-note').data('originalNote'));
}
/**
@@ -984,21 +876,15 @@ export default class Notes {
// The notes tr can contain multiple lists of notes, like on the parallel diff
// notesTr does not exist for image diffs
- if (
- notesTr.find('.discussion-notes').length > 1 ||
- notesTr.length === 0
- ) {
+ if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
const $diffFile = $notes.closest('.diff-file');
if ($diffFile.length > 0) {
- const removeBadgeEvent = new CustomEvent(
- 'removeBadge.imageDiff',
- {
- detail: {
- // badgeNumber's start with 1 and index starts with 0
- badgeNumber: $notes.index() + 1,
- },
+ const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
+ detail: {
+ // badgeNumber's start with 1 and index starts with 0
+ badgeNumber: $notes.index() + 1,
},
- );
+ });
$diffFile[0].dispatchEvent(removeBadgeEvent);
}
@@ -1101,9 +987,7 @@ export default class Notes {
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
- form
- .removeClass('js-main-target-form')
- .addClass('discussion-form js-discussion-note-form');
+ form.removeClass('js-main-target-form').addClass('discussion-form js-discussion-note-form');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn');
@@ -1113,9 +997,7 @@ export default class Notes {
}
form.find('.js-note-text').focus();
- form
- .find('.js-comment-resolve-button')
- .attr('data-discussion-id', discussionID);
+ form.find('.js-comment-resolve-button').attr('data-discussion-id', discussionID);
}
/**
@@ -1148,9 +1030,7 @@ export default class Notes {
// Setup comment form
let newForm;
- const $noteContainer = $link
- .closest('.diff-viewer')
- .find('.note-container');
+ const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
const $form = $noteContainer.find('> .discussion-form');
if ($form.length === 0) {
@@ -1219,9 +1099,7 @@ export default class Notes {
notesContent = targetRow.find(notesContentSelector);
addForm = true;
} else {
- const isCurrentlyShown = targetRow
- .find('.content:not(:empty)')
- .is(':visible');
+ const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
@@ -1386,9 +1264,7 @@ export default class Notes {
if ($note.find('.js-conflict-edit-warning').length === 0) {
const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
This comment has changed since you started editing, please review the
- <a href="#note_${
- noteEntity.id
- }" target="_blank" rel="noopener noreferrer">
+ <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">
updated comment
</a>
to ensure information is not lost
@@ -1398,9 +1274,7 @@ export default class Notes {
}
updateNotesCount(updateCount) {
- return this.notesCountBadge.text(
- parseInt(this.notesCountBadge.text(), 10) + updateCount,
- );
+ return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
}
static renderPlaceholderComponent($container) {
@@ -1463,9 +1337,7 @@ export default class Notes {
toggleCommitList(e) {
const $element = $(e.currentTarget);
- const $closestSystemCommitList = $element.siblings(
- '.system-note-commit-list',
- );
+ const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
$element
.find('.fa')
@@ -1498,9 +1370,7 @@ export default class Notes {
$systemNote.find('.note-text').addClass('system-note-commit-list');
$systemNote.find('.system-note-commit-list-toggler').show();
} else {
- $systemNote
- .find('.note-text')
- .addClass('system-note-commit-list hide-shade');
+ $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade');
}
});
}
@@ -1651,12 +1521,8 @@ export default class Notes {
<div class="note-header">
<div class="note-header-info">
<a href="/${_.escape(currentUsername)}">
- <span class="hidden-xs">${_.escape(
- currentUsername,
- )}</span>
- <span class="note-headline-light">${_.escape(
- currentUsername,
- )}</span>
+ <span class="hidden-xs">${_.escape(currentUsername)}</span>
+ <span class="note-headline-light">${_.escape(currentUsername)}</span>
</a>
</div>
</div>
@@ -1671,9 +1537,7 @@ export default class Notes {
);
$tempNote.find('.hidden-xs').text(_.escape(currentUserFullname));
- $tempNote
- .find('.note-headline-light')
- .text(`@${_.escape(currentUsername)}`);
+ $tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`);
return $tempNote;
}
@@ -1729,15 +1593,8 @@ export default class Notes {
.attr('id') === 'discussion';
const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
- const isDiscussionResolve = $submitBtn.hasClass(
- 'js-comment-resolve-button',
- );
- const {
- formData,
- formContent,
- formAction,
- formContentOriginal,
- } = this.getFormData($form);
+ const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
+ const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
let noteUniqueId;
let systemNoteUniqueId;
let hasQuickActions = false;
@@ -1825,9 +1682,7 @@ export default class Notes {
// Reset cached commands list when command is applied
if (hasQuickActions) {
- $form
- .find('textarea.js-note-text')
- .trigger('clear-commands-cache.atwho');
+ $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
}
// Clear previous form errors
@@ -1872,9 +1727,7 @@ export default class Notes {
// append flash-container to the Notes list
if ($notesContainer.length) {
- $notesContainer.append(
- '<div class="flash-container" style="display: none;"></div>',
- );
+ $notesContainer.append('<div class="flash-container" style="display: none;"></div>');
}
} else if (isMainForm) {
// Check if this was main thread comment
@@ -1909,9 +1762,7 @@ export default class Notes {
// Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) {
- const replyButton = $notesContainer
- .parent()
- .find('.js-discussion-reply-button');
+ const replyButton = $notesContainer.parent().find('.js-discussion-reply-button');
this.replyToDiscussionNote(replyButton[0]);
$form = $notesContainer.parent().find('form');
}
@@ -1954,9 +1805,7 @@ export default class Notes {
// Show updated comment content temporarily
$noteBodyText.html(formContent);
- $editingNote
- .removeClass('is-editing fade-in-full')
- .addClass('being-posted fade-in-half');
+ $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
$editingNote
.find('.note-headline-meta a')
.html(
diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue
index 3e09391a9ac..5cc1367600e 100644
--- a/app/assets/javascripts/notes/components/diff_file_header.vue
+++ b/app/assets/javascripts/notes/components/diff_file_header.vue
@@ -27,6 +27,24 @@ export default {
titleTag() {
return this.diffFile.fileHash ? 'a' : 'span';
},
+ baseSha() {
+ return this.diffFile.diffRefs.baseSha;
+ },
+ contentSha() {
+ return this.diffFile.contentSha;
+ },
+ truncatedBaseSha() {
+ return this.truncate(this.baseSha);
+ },
+ truncatedContentSha() {
+ return this.truncate(this.contentSha);
+ },
+ imageDiff() {
+ return !this.diffFile.text;
+ },
+ replacedFile() {
+ return !(this.diffFile.newFile || this.diffFile.deletedFile);
+ },
},
methods: {
handleToggle(e, checkTarget) {
@@ -38,7 +56,9 @@ export default {
this.$emit('toggleFile');
}
},
- noop() {},
+ truncate(sha) {
+ return sha.slice(0, 8);
+ },
},
};
</script>
@@ -49,11 +69,14 @@ export default {
ref="header"
class="file-header-content"
>
- <i
+ <icon
v-if="collapsible"
@click.stop="handleToggle"
- class="fa diff-toggle-caret fa-fw fa-caret-down"
- ></i>
+ name="chevron-down"
+ aria-hidden="true"
+ :size="16"
+ class="diff-toggle-caret"
+ />
<div
v-if="diffFile.submodule"
>
@@ -76,7 +99,7 @@ export default {
:is="titleTag"
:href="`#${diffFile.fileHash}`"
>
- <span v-html="diffFile.blobIcon"></span>
+ <i class="fa fa-fw" :class="`fa-${diffFile.blob.icon}`"></i>
<span v-if="diffFile.renamedFile">
<strong
class="file-title-name has-tooltip"
@@ -121,5 +144,54 @@ export default {
{{ diffFile.aMode }} → {{ diffFile.bMode }}
</small>
</template>
+
+
+ <div
+ v-if="!diffFile.submodule"
+ class="file-actions hidden-xs"
+ >
+ <template
+ v-if="diffFile.blob && diffFile.blob.readableText"
+ >
+ <button
+ class="js-toggle-diff-comments btn"
+ title="Toggle comments for this file"
+ type="button"
+ :class="{
+ active: 'todo'
+ }"
+ >
+ <icon name="comment" />
+ </button>
+
+ <a
+ :href="diffFile.editPath"
+ class="btn btn-default js-edit-blob"
+ >
+ Edit
+ </a>
+ </template>
+
+ <a
+ v-if="imageDiff && replacedFile"
+ class="btn view-file js-view-file"
+ :href="baseSha"
+ >
+ View replaced file @ <span class="commit-sha">{{ truncatedBaseSha }}</span>
+ </a>
+ <a
+ class="btn view-file js-view-file"
+ :href="contentSha"
+ >
+ View file @ <span class="commit-sha">{{ truncatedContentSha }}</span>
+ </a>
+
+ <button
+ v-if="diffFile.environment"
+ >
+ View on environment
+ <!-- = view_on_environment_button(diff_file.content_sha, diff_file.file_path, environment) if environment -->
+ </button>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index c2ce453d34a..b2e27f6dd3f 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -140,6 +140,7 @@ export const toggleIssueLocalState = ({ commit }, newState) => {
};
export const saveNote = ({ commit, dispatch }, noteData) => {
+ // For MR discussuions we need to post as `note[note]` and issue we use `note.note`.
const note = noteData.data['note[note]'] || noteData.data.note.note;
let placeholderText = note;
const hasQuickActions = utils.hasQuickActions(placeholderText);
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 718634595e3..82f31bee174 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -28,8 +28,8 @@ export const notesById = state =>
export const discussionsByLineCode = state =>
state.notes.reduce((acc, note) => {
- if (note.diff_discussion) {
- // For context line notes, there might be multiple notes with the same line code
+ if (note.diff_discussion && note.line_code) {
+ // For context about line notes: there might be multiple notes with the same line code
const items = acc[note.line_code] || [];
items.push(note);
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index e5b2827b50c..f61f4db78d5 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,4 +1,3 @@
-import { hasVueMRDiscussionsCookie } from '~/lib/utils/common_utils';
import initMrNotes from '~/mr_notes';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initShow from '../init_merge_request_show';
@@ -6,8 +5,5 @@ import initShow from '../init_merge_request_show';
document.addEventListener('DOMContentLoaded', () => {
initShow();
initSidebarBundle();
-
- if (hasVueMRDiscussionsCookie()) {
- initMrNotes();
- }
+ initMrNotes();
});
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 7f037582ca0..7a94e5d27bc 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -71,6 +71,12 @@
span {
white-space: pre-wrap;
+
+ &.context-cell {
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ }
}
.line {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index a698a11fb9b..35ecd05e34e 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -3,9 +3,15 @@
*/
@-webkit-keyframes targe3-note {
- from { background: $note-targe3-outside; }
- 50% { background: $note-targe3-inside; }
- to { background: $note-targe3-outside; }
+ from {
+ background: $note-targe3-outside;
+ }
+ 50% {
+ background: $note-targe3-inside;
+ }
+ to {
+ background: $note-targe3-outside;
+ }
}
ul.notes {
@@ -36,7 +42,8 @@ ul.notes {
}
}
- > li { // .timeline-entry
+ > li {
+ // .timeline-entry
padding: 0;
display: block;
position: relative;
@@ -153,7 +160,6 @@ ul.notes {
}
.note-header {
-
@include notes-media('max', $screen-xs-min) {
.inline {
display: block;
@@ -282,7 +288,10 @@ ul.notes {
position: absolute;
left: 0;
bottom: 0;
- background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%);
+ background: linear-gradient(
+ rgba($white-light, 0.1) -100px,
+ $white-light 100%
+ );
}
}
}
@@ -666,7 +675,6 @@ ul.notes {
background-color: $white-light;
}
-
a {
color: $gl-link-color;
}
@@ -745,7 +753,8 @@ ul.notes {
border: 0;
outline: 0;
color: $gray-darkest;
- transition: color $general-hover-transition-duration $general-hover-transition-curve;
+ transition: color $general-hover-transition-duration
+ $general-hover-transition-curve;
&.is-disabled {
cursor: default;
@@ -807,4 +816,8 @@ ul.notes {
.diff-comment-form {
display: block;
}
+
+ .add-diff-note svg {
+ margin-top: 4px;
+ }
}
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 0c1c286a0a4..a366559ae1d 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -3,8 +3,8 @@ class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
include CreatesCommit
include RendersBlob
+ include NotesHelper
include ActionView::Helpers::SanitizeHelper
-
prepend_before_action :authenticate_user!, only: [:edit]
before_action :require_non_empty_project, except: [:new, :create]
@@ -92,6 +92,7 @@ class Projects::BlobController < Projects::ApplicationController
@lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines
@form = UnfoldForm.new(params)
+
@lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe)
if @form.bottom?
@@ -102,11 +103,45 @@ class Projects::BlobController < Projects::ApplicationController
@match_line = "@@ -#{line}+#{line} @@"
end
- render layout: false
+ if has_vue_discussions_cookie?
+ render_diff_lines
+ else
+ render layout: false
+ end
end
private
+ # Converts a String array to Gitlab::Diff::Line array
+ def render_diff_lines
+ @lines.map! do |line|
+ diff_line = Gitlab::Diff::Line.new(line ,'context', nil, nil, nil)
+ diff_line.rich_text = line
+ diff_line
+ end
+
+ add_match_line
+
+ render json: @lines
+ end
+
+ def add_match_line
+ return unless @form.unfold?
+
+ if @form.bottom? && @form.to < @blob.lines.size
+ old_pos = @form.to - @form.offset
+ new_pos = @form.to
+ elsif @form.since != 1
+ old_pos = new_pos = @form.since
+ end
+
+ return unless new_pos
+
+ @match_line = Gitlab::Diff::Line.new(@match_line ,'match', nil, old_pos, new_pos)
+
+ @form.bottom? ? @lines.push(@match_line) : @lines.unshift(@match_line)
+ end
+
def blob
@blob ||= @repository.blob_at(@commit.id, @path)
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index fe8525a488c..242bcfb574d 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -1,6 +1,7 @@
class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController
include DiffForPath
include DiffHelper
+ include NotesHelper
include RendersNotes
before_action :apply_diff_view_cookie!
@@ -11,7 +12,11 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def show
@environment = @merge_request.environments_for(current_user).last
- render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
+ if has_vue_discussions_cookie?
+ render json: DiffsSerializer.new.represent(@diffs, merge_request: @merge_request)
+ else
+ render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
+ end
end
def diff_for_path
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 20aed60cb7a..a30ba37c568 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -179,7 +179,7 @@ module NotesHelper
end
def has_vue_discussions_cookie?
- cookies[:vue_mr_discussions] == 'true'
+ true
end
def serialize_notes?
diff --git a/app/serializers/blob_entity.rb b/app/serializers/blob_entity.rb
index ad039a2623d..b501fd5e964 100644
--- a/app/serializers/blob_entity.rb
+++ b/app/serializers/blob_entity.rb
@@ -3,11 +3,13 @@ class BlobEntity < Grape::Entity
expose :id, :path, :name, :mode
+ expose :readable_text?, as: :readable_text
+
expose :icon do |blob|
IconsHelper.file_type_icon_class('file', blob.mode, blob.name)
end
- expose :url do |blob|
+ expose :url, if: -> (*) { request.respond_to?(:ref) } do |blob|
project_blob_path(request.project, File.join(request.ref, blob.path))
end
end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index 6e68d275047..793a28a7beb 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -1,7 +1,9 @@
class DiffFileEntity < Grape::Entity
+ include RequestAwareEntity
include DiffHelper
include SubmoduleHelper
include BlobHelper
+ include TreeHelper
include IconsHelper
include ActionView::Helpers::TagHelper
@@ -11,15 +13,26 @@ class DiffFileEntity < Grape::Entity
submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository).first
end
+ expose :blob, using: BlobEntity
+
expose :blob_path do |diff_file|
diff_file.blob.path
end
+ expose :blob_name do |diff_file|
+ diff_file.blob.name
+ end
+
expose :blob_icon do |diff_file|
blob_icon(diff_file.b_mode, diff_file.file_path)
end
+ expose :file_hash do |diff_file|
+ Digest::SHA1.hexdigest(diff_file.file_path)
+ end
+
expose :file_path
+ expose :new_file?, as: :new_file
expose :deleted_file?, as: :deleted_file
expose :renamed_file?, as: :renamed_file
expose :old_path
@@ -28,6 +41,10 @@ class DiffFileEntity < Grape::Entity
expose :a_mode
expose :b_mode
expose :text?, as: :text
+ expose :added_lines
+ expose :removed_lines
+ expose :diff_refs
+ expose :content_sha
expose :old_path_html do |diff_file|
old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
@@ -38,4 +55,57 @@ class DiffFileEntity < Grape::Entity
_, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
new_path
end
+
+ # TODO check if these are not creating a n+1 call
+ # we should probably also pass project as parameter
+ expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ edit_blob_path(merge_request.source_project, merge_request.source_branch, diff_file.new_path)
+ end
+
+ expose :view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ project_blob_path(merge_request.source_project, tree_join(merge_request.source_branch, diff_file.new_path))
+ end
+
+ expose :context_lines_path, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ project_blob_diff_path(diff_file.repository.project, tree_join(diff_file.content_sha, diff_file.file_path))
+ end
+
+ # Used for inline diffs
+ expose :highlighted_diff_lines, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ lines = diff_file.highlighted_diff_lines
+
+ add_bottom_match_line(lines, diff_file)
+
+ lines
+ end
+
+ # Used for parallel diffs
+ expose :parallel_diff_lines, if: -> (diff_file, _) { diff_file.text? }
+
+ private
+
+ # This adds the bottom line to the array which contains
+ # the data to load more context lines.
+ def add_bottom_match_line(lines, diff_file)
+ return if lines.empty?
+
+ last_line = lines.last
+
+ return unless last_line.new_pos < total_lines(diff_file.blob)
+
+ match_line = Gitlab::Diff::Line.new("" ,'match', nil, last_line.old_pos, last_line.new_pos)
+ lines.push(match_line)
+ end
+
+ def total_lines(blob)
+ @total_lines ||= begin
+ line_count = blob.lines.size
+ line_count -= 1 if line_count > 0 && blob.lines.last.blank?
+ line_count
+ end
+ end
end
diff --git a/app/serializers/diff_file_serializer.rb b/app/serializers/diff_file_serializer.rb
new file mode 100644
index 00000000000..8fcb7d67e34
--- /dev/null
+++ b/app/serializers/diff_file_serializer.rb
@@ -0,0 +1,3 @@
+class DiffFileSerializer < BaseSerializer
+ entity DiffFileEntity
+end
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
new file mode 100644
index 00000000000..fb8027059cb
--- /dev/null
+++ b/app/serializers/diffs_entity.rb
@@ -0,0 +1,13 @@
+class DiffsEntity < Grape::Entity
+ expose :real_size
+
+ expose :added_lines do |diffs|
+ diffs.diff_files.sum(&:added_lines)
+ end
+
+ expose :removed_lines do |diffs|
+ diffs.diff_files.sum(&:removed_lines)
+ end
+
+ expose :diff_files, using: DiffFileEntity
+end
diff --git a/app/serializers/diffs_serializer.rb b/app/serializers/diffs_serializer.rb
new file mode 100644
index 00000000000..6771e10c5ac
--- /dev/null
+++ b/app/serializers/diffs_serializer.rb
@@ -0,0 +1,3 @@
+class DiffsSerializer < BaseSerializer
+ entity DiffsEntity
+end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index bbbcf6a97c1..6af0973c1dd 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -2,6 +2,8 @@ class DiscussionEntity < Grape::Entity
include RequestAwareEntity
expose :id, :reply_id
+ expose :position, if: -> (d, _) { defined? d.diff_file }
+ expose :line_code, if: -> (d, _) { defined? d.diff_file }
expose :expanded?, as: :expanded
expose :notes, using: NoteEntity
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 112f27ef513..6029738f437 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -76,11 +76,12 @@
%section.col-md-12
%script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe
.issuable-discussion.js-vue-notes-event
- = render "projects/merge_requests/discussion"
- if has_vue_discussions_cookie?
#js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
noteable_data: serialize_issuable(@merge_request),
current_user_data: UserSerializer.new.represent(current_user).to_json} }
+ - else
+ = render "projects/merge_requests/discussion"
#commits.commits.tab-pane
-# This tab is always loaded via AJAX
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 014854da55c..30eab47575c 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -120,11 +120,13 @@ module Gitlab
# Array of Gitlab::Diff::Line objects
def diff_lines
- @diff_lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a
+ @diff_lines ||=
+ Gitlab::Diff::Parser.new.parse(raw_diff.each_line, diff_file: self).to_a
end
def highlighted_diff_lines
- @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
+ @highlighted_diff_lines ||=
+ Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
end
# Array[<Hash>] with right/left keys that contains Gitlab::Diff::Line objects which text is hightlighted
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 269016daac2..16da773bb4a 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -5,7 +5,7 @@ module Gitlab
delegate :old_path, :new_path, :old_sha, :new_sha, to: :diff_file, prefix: :diff
- def initialize(diff_lines, repository: nil)
+ def initialize(diff_lines, since: nil, from: nil, repository: nil)
@repository = repository
if diff_lines.is_a?(Gitlab::Diff::File)
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 0603141e441..b8966110f5a 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -1,22 +1,26 @@
module Gitlab
module Diff
class Line
- attr_reader :type, :index, :old_pos, :new_pos
+ attr_reader :line_code, :type, :index, :old_pos, :new_pos, :meta_data
attr_writer :rich_text
attr_accessor :text
- def initialize(text, type, index, old_pos, new_pos, parent_file: nil)
+ def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil)
@text, @type, @index = text, type, index
@old_pos, @new_pos = old_pos, new_pos
@parent_file = parent_file
+
+ # When line code is not provided from cache store we build it
+ # using the parent_file(Diff::File or Conflict::File).
+ @line_code = line_code || set_line_code
end
def self.init_from_hash(hash)
- new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos])
+ new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos], line_code: hash[:line_code])
end
def serialize_keys
- @serialize_keys ||= %i(text type index old_pos new_pos)
+ @serialize_keys ||= %i(line_code text type index old_pos new_pos)
end
def to_hash
@@ -58,20 +62,39 @@ module Gitlab
end
def rich_text
- @parent_file.highlight_lines! if @parent_file && !@rich_text
+ @parent_file.try(:highlight_lines!) if @parent_file && !@rich_text
@rich_text
end
+ def meta_data
+ return unless meta?
+
+ {
+ old_pos: old_pos,
+ new_pos: new_pos
+ }
+ end
+
def as_json(opts = nil)
{
+ line_code: line_code,
type: type,
old_line: old_line,
new_line: new_line,
text: text,
- rich_text: rich_text || text
+ rich_text: rich_text || text,
+ meta_data: meta_data
}
end
+
+ private
+
+ def set_line_code
+ return nil unless @parent_file
+
+ @parent_file.line_code(self)
+ end
end
end
end
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 8302f30a0a2..7ae7ed286ed 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -3,7 +3,7 @@ module Gitlab
class Parser
include Enumerable
- def parse(lines)
+ def parse(lines, diff_file: nil)
return [] if lines.blank?
@lines = lines
@@ -31,17 +31,17 @@ module Gitlab
next if line_old <= 1 && line_new <= 1 # top of file
- yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: diff_file)
line_obj_index += 1
next
elsif line[0] == '\\'
type = "#{context}-nonewline"
- yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: diff_file)
line_obj_index += 1
else
type = identification_type(line)
- yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
+ yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: diff_file)
line_obj_index += 1
end
diff --git a/spec/serializers/discussion_entity_spec.rb b/spec/serializers/discussion_entity_spec.rb
index 7ee8e38af1c..c0db71c693f 100644
--- a/spec/serializers/discussion_entity_spec.rb
+++ b/spec/serializers/discussion_entity_spec.rb
@@ -30,7 +30,7 @@ describe DiscussionEntity do
let(:note) { create(:diff_note_on_merge_request) }
it 'exposes diff file attributes' do
- expect(subject).to include(:diff_file, :truncated_diff_lines, :image_diff_html)
+ expect(subject).to include(:diff_file, :truncated_diff_lines, :image_diff_html, :diff_lines, :highlighted_diff_lines)
end
end
end