Commit f4ab02b1 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'osw-support-multi-line-suggestions' into 'master'

Support multi-line suggestions

Closes #53310

See merge request gitlab-org/gitlab-ce!25211
parents 30988aec e540c0d7
...@@ -48,10 +48,13 @@ export default { ...@@ -48,10 +48,13 @@ export default {
noteableType: this.noteableType, noteableType: this.noteableType,
noteTargetLine: this.noteTargetLine, noteTargetLine: this.noteTargetLine,
diffViewType: this.diffViewType, diffViewType: this.diffViewType,
diffFile: this.getDiffFileByHash(this.diffFileHash), diffFile: this.diffFile,
linePosition: this.linePosition, linePosition: this.linePosition,
}; };
}, },
diffFile() {
return this.getDiffFileByHash(this.diffFileHash);
},
}, },
mounted() { mounted() {
if (this.isLoggedIn) { if (this.isLoggedIn) {
...@@ -102,6 +105,7 @@ export default { ...@@ -102,6 +105,7 @@ export default {
:line-code="line.line_code" :line-code="line.line_code"
:line="line" :line="line"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:diff-file="diffFile"
save-button-title="Comment" save-button-title="Comment"
class="diff-comment-form" class="diff-comment-form"
@handleFormUpdateAddToReview="addToReview" @handleFormUpdateAddToReview="addToReview"
......
...@@ -61,6 +61,11 @@ export default { ...@@ -61,6 +61,11 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
diffFile: {
type: Object,
required: false,
default: null,
},
helpPagePath: { helpPagePath: {
type: String, type: String,
required: false, required: false,
...@@ -102,9 +107,42 @@ export default { ...@@ -102,9 +107,42 @@ export default {
} }
return '#'; return '#';
}, },
diffParams() {
if (this.diffFile) {
return {
filePath: this.diffFile.file_path,
refs: this.diffFile.diff_refs,
};
} else if (this.note && this.note.position) {
return {
filePath: this.note.position.new_path,
refs: this.note.position,
};
} else if (this.discussion && this.discussion.diff_file) {
return {
filePath: this.discussion.diff_file.file_path,
refs: this.discussion.diff_file.diff_refs,
};
}
return null;
},
markdownPreviewPath() { markdownPreviewPath() {
const notable = this.getNoteableDataByProp('preview_note_path'); const notable = this.getNoteableDataByProp('preview_note_path');
return mergeUrlParams({ preview_suggestions: true }, notable);
const previewSuggestions = this.line && this.diffParams;
const params = previewSuggestions
? {
preview_suggestions: previewSuggestions,
line: this.line.new_line,
file_path: this.diffParams.filePath,
base_sha: this.diffParams.refs.base_sha,
start_sha: this.diffParams.refs.start_sha,
head_sha: this.diffParams.refs.head_sha,
}
: {};
return mergeUrlParams(params, notable);
}, },
markdownDocsPath() { markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath'); return this.getNotesDataByProp('markdownDocsPath');
...@@ -234,8 +272,8 @@ export default { ...@@ -234,8 +272,8 @@ export default {
placeholder="Write a comment or drag your files here…" placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleKeySubmit()" @keydown.meta.enter="handleKeySubmit()"
@keydown.ctrl.enter="handleKeySubmit()" @keydown.ctrl.enter="handleKeySubmit()"
@keydown.up="editMyLastNote()" @keydown.exact.up="editMyLastNote()"
@keydown.esc="cancelHandler(true)" @keydown.exact.esc="cancelHandler(true)"
@input="onInput" @input="onInput"
></textarea> ></textarea>
</markdown-field> </markdown-field>
......
/* eslint-disable import/prefer-default-export */
function trimFirstCharOfLineContent(text) {
if (!text) {
return text;
}
return text.replace(/^( |\+|-)/, '');
}
function cleanSuggestionLine(line = {}) {
return {
...line,
text: trimFirstCharOfLineContent(line.text),
};
}
export function selectDiffLines(lines) {
return lines.filter(line => line.type !== 'match').map(line => cleanSuggestionLine(line));
}
...@@ -76,6 +76,7 @@ export default { ...@@ -76,6 +76,7 @@ export default {
hasSuggestion: false, hasSuggestion: false,
markdownPreviewLoading: false, markdownPreviewLoading: false,
previewMarkdown: false, previewMarkdown: false,
suggestions: this.note.suggestions || [],
}; };
}, },
computed: { computed: {
...@@ -109,9 +110,6 @@ export default { ...@@ -109,9 +110,6 @@ export default {
} }
return lineNumber; return lineNumber;
}, },
suggestions() {
return this.note.suggestions || [];
},
lineType() { lineType() {
return this.line ? this.line.type : ''; return this.line ? this.line.type : '';
}, },
...@@ -175,6 +173,7 @@ export default { ...@@ -175,6 +173,7 @@ export default {
this.referencedCommands = data.references.commands; this.referencedCommands = data.references.commands;
this.referencedUsers = data.references.users; this.referencedUsers = data.references.users;
this.hasSuggestion = data.references.suggestions && data.references.suggestions.length; this.hasSuggestion = data.references.suggestions && data.references.suggestions.length;
this.suggestions = data.references.suggestions;
} }
this.$nextTick() this.$nextTick()
......
...@@ -38,7 +38,7 @@ export default { ...@@ -38,7 +38,7 @@ export default {
].join('\n'); ].join('\n');
}, },
mdSuggestion() { mdSuggestion() {
return ['```suggestion', `{text}`, '```'].join('\n'); return ['```suggestion:-0+0', `{text}`, '```'].join('\n');
}, },
}, },
mounted() { mounted() {
......
<script> <script>
import SuggestionDiffHeader from './suggestion_diff_header.vue'; import SuggestionDiffHeader from './suggestion_diff_header.vue';
import SuggestionDiffRow from './suggestion_diff_row.vue';
import { selectDiffLines } from '../lib/utils/diff_utils';
export default { export default {
components: { components: {
SuggestionDiffHeader, SuggestionDiffHeader,
SuggestionDiffRow,
}, },
props: { props: {
newLines: {
type: Array,
required: true,
},
fromContent: {
type: String,
required: false,
default: '',
},
fromLine: {
type: Number,
required: true,
},
suggestion: { suggestion: {
type: Object, type: Object,
required: true, required: true,
...@@ -33,6 +23,11 @@ export default { ...@@ -33,6 +23,11 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
lines() {
return selectDiffLines(this.suggestion.diff_lines);
},
},
methods: { methods: {
applySuggestion(callback) { applySuggestion(callback) {
this.$emit('apply', { suggestionId: this.suggestion.id, callback }); this.$emit('apply', { suggestionId: this.suggestion.id, callback });
...@@ -52,22 +47,11 @@ export default { ...@@ -52,22 +47,11 @@ export default {
/> />
<table class="mb-3 md-suggestion-diff js-syntax-highlight code"> <table class="mb-3 md-suggestion-diff js-syntax-highlight code">
<tbody> <tbody>
<!-- Old Line --> <suggestion-diff-row
<tr class="line_holder old"> v-for="(line, index) of lines"
<td class="diff-line-num old_line qa-old-diff-line-number old">{{ fromLine }}</td> :key="`${index}-${line.text}`"
<td class="diff-line-num new_line old"></td> :line="line"
<td class="line_content old"> />
<span>{{ fromContent }}</span>
</td>
</tr>
<!-- New Line(s) -->
<tr v-for="(line, key) of newLines" :key="key" class="line_holder new">
<td class="diff-line-num old_line new"></td>
<td class="diff-line-num new_line qa-new-diff-line-number new">{{ line.lineNumber }}</td>
<td class="line_content new">
<span>{{ line.content }}</span>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
......
<script>
export default {
name: 'SuggestionDiffRow',
props: {
line: {
type: Object,
required: true,
},
},
computed: {
lineType() {
return this.line.type;
},
},
};
</script>
<template>
<tr class="line_holder" :class="lineType">
<td class="diff-line-num old_line" :class="lineType">
{{ line.old_line }}
</td>
<td class="diff-line-num new_line" :class="lineType">
{{ line.new_line }}
</td>
<td class="line_content" :class="lineType">
<span v-if="line.text">{{ line.text }}</span>
<!-- TODO: replace this hack with zero-width whitespace when we have rich_text from BE -->
<span v-else>&#8203;</span>
</td>
</tr>
</template>
...@@ -6,16 +6,6 @@ import Flash from '~/flash'; ...@@ -6,16 +6,6 @@ import Flash from '~/flash';
export default { export default {
components: { SuggestionDiff }, components: { SuggestionDiff },
props: { props: {
fromLine: {
type: Number,
required: false,
default: 0,
},
fromContent: {
type: String,
required: false,
default: '',
},
lineType: { lineType: {
type: String, type: String,
required: false, required: false,
...@@ -71,41 +61,19 @@ export default { ...@@ -71,41 +61,19 @@ export default {
suggestionElements.forEach((suggestionEl, i) => { suggestionElements.forEach((suggestionEl, i) => {
const suggestionParentEl = suggestionEl.parentElement; const suggestionParentEl = suggestionEl.parentElement;
const newLines = this.extractNewLines(suggestionParentEl); const diffComponent = this.generateDiff(i);
const diffComponent = this.generateDiff(newLines, i);
diffComponent.$mount(suggestionParentEl); diffComponent.$mount(suggestionParentEl);
}); });
this.isRendered = true; this.isRendered = true;
}, },
extractNewLines(suggestionEl) { generateDiff(suggestionIndex) {
// extracts the suggested lines from the markdown
// calculates a line number for each line
const newLines = suggestionEl.querySelectorAll('.line');
const fromLine = this.suggestions.length ? this.suggestions[0].from_line : this.fromLine;
const lines = [];
newLines.forEach((line, i) => {
const content = `${line.innerText}\n`;
const lineNumber = fromLine + i;
lines.push({ content, lineNumber });
});
return lines;
},
generateDiff(newLines, suggestionIndex) {
// generates the diff <suggestion-diff /> component
// all `suggestion` markdown will be swapped out by this component
const { suggestions, disabled, helpPagePath } = this; const { suggestions, disabled, helpPagePath } = this;
const suggestion = const suggestion =
suggestions && suggestions[suggestionIndex] ? suggestions[suggestionIndex] : {}; suggestions && suggestions[suggestionIndex] ? suggestions[suggestionIndex] : {};
const fromContent = suggestion.from_content || this.fromContent;
const fromLine = suggestion.from_line || this.fromLine;
const SuggestionDiffComponent = Vue.extend(SuggestionDiff); const SuggestionDiffComponent = Vue.extend(SuggestionDiff);
const suggestionDiff = new SuggestionDiffComponent({ const suggestionDiff = new SuggestionDiffComponent({
propsData: { newLines, fromLine, fromContent, disabled, suggestion, helpPagePath }, propsData: { disabled, suggestion, helpPagePath },
}); });
suggestionDiff.$on('apply', ({ suggestionId, callback }) => { suggestionDiff.$on('apply', ({ suggestionId, callback }) => {
......
...@@ -20,7 +20,7 @@ module PreviewMarkdown ...@@ -20,7 +20,7 @@ module PreviewMarkdown
body: view_context.markdown(result[:text], markdown_params), body: view_context.markdown(result[:text], markdown_params),
references: { references: {
users: result[:users], users: result[:users],
suggestions: result[:suggestions], suggestions: SuggestionSerializer.new.represent_diff(result[:suggestions]),
commands: view_context.markdown(result[:commands]) commands: view_context.markdown(result[:commands])
} }
} }
......
...@@ -42,6 +42,6 @@ class IssueEntity < IssuableEntity ...@@ -42,6 +42,6 @@ class IssueEntity < IssuableEntity
end end
expose :preview_note_path do |issue| expose :preview_note_path do |issue|
preview_markdown_path(issue.project, quick_actions_target_type: 'Issue', quick_actions_target_id: issue.iid) preview_markdown_path(issue.project, target_type: 'Issue', target_id: issue.iid)
end end
end end
...@@ -235,7 +235,7 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -235,7 +235,7 @@ class MergeRequestWidgetEntity < IssuableEntity
end end
expose :preview_note_path do |merge_request| expose :preview_note_path do |merge_request|
preview_markdown_path(merge_request.project, quick_actions_target_type: 'MergeRequest', quick_actions_target_id: merge_request.iid) preview_markdown_path(merge_request.project, target_type: 'MergeRequest', target_id: merge_request.iid)
end end
expose :merge_commit_path do |merge_request| expose :merge_commit_path do |merge_request|
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
class SuggestionEntity < API::Entities::Suggestion class SuggestionEntity < API::Entities::Suggestion
include RequestAwareEntity include RequestAwareEntity
unexpose :from_line, :to_line, :from_content, :to_content
expose :diff_lines, using: DiffLineEntity
expose :current_user do expose :current_user do
expose :can_apply do |suggestion| expose :can_apply do |suggestion|
Ability.allowed?(current_user, :apply_suggestion, suggestion) Ability.allowed?(current_user, :apply_suggestion, suggestion)
......
# frozen_string_literal: true
class SuggestionSerializer < BaseSerializer
entity SuggestionEntity
def represent_diff(resource)
represent(resource, { only: [:diff_lines] })
end
end
...@@ -2,10 +2,17 @@ ...@@ -2,10 +2,17 @@
module Suggestible module Suggestible
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
# This translates into limiting suggestion changes to `suggestion:-100+100`. # This translates into limiting suggestion changes to `suggestion:-100+100`.
MAX_LINES_CONTEXT = 100.freeze MAX_LINES_CONTEXT = 100.freeze
def diff_lines
strong_memoize(:diff_lines) do
Gitlab::Diff::SuggestionDiff.new(self).diff_lines
end
end
def fetch_from_content def fetch_from_content
diff_file.new_blob_lines_between(from_line, to_line).join diff_file.new_blob_lines_between(from_line, to_line).join
end end
......
...@@ -17,7 +17,7 @@ class PreviewMarkdownService < BaseService ...@@ -17,7 +17,7 @@ class PreviewMarkdownService < BaseService
private private
def explain_quick_actions(text) def explain_quick_actions(text)
return text, [] unless %w(Issue MergeRequest Commit).include?(commands_target_type) return text, [] unless %w(Issue MergeRequest Commit).include?(target_type)
quick_actions_service = QuickActions::InterpretService.new(project, current_user) quick_actions_service = QuickActions::InterpretService.new(project, current_user)
quick_actions_service.explain(text, find_commands_target) quick_actions_service.explain(text, find_commands_target)
...@@ -30,22 +30,34 @@ class PreviewMarkdownService < BaseService ...@@ -30,22 +30,34 @@ class PreviewMarkdownService < BaseService
end end
def find_suggestions(text) def find_suggestions(text)
return [] unless params[:preview_suggestions] return [] unless preview_sugestions?
Banzai::SuggestionsParser.parse(text) position = Gitlab::Diff::Position.new(new_path: params[:file_path],
new_line: params[:line].to_i,
base_sha: params[:base_sha],
head_sha: params[:head_sha],
start_sha: params[:start_sha])
Gitlab::Diff::SuggestionsParser.parse(text, position: position, project: project)
end
def preview_sugestions?
params[:preview_suggestions] &&
target_type == 'MergeRequest' &&
Ability.allowed?(current_user, :download_code, project)
end end
def find_commands_target def find_commands_target
QuickActions::TargetService QuickActions::TargetService
.new(project, current_user) .new(project, current_user)
.execute(commands_target_type, commands_target_id) .execute(target_type, target_id)
end end
def commands_target_type def target_type
params[:quick_actions_target_type] params[:target_type]
end end
def commands_target_id def target_id
params[:quick_actions_target_id] params[:target_id]
end end
end end
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- supports_quick_actions = model.new_record? - supports_quick_actions = model.new_record?
- if supports_quick_actions - if supports_quick_actions
- preview_url = preview_markdown_path(project, quick_actions_target_type: model.class.name) - preview_url = preview_markdown_path(project, target_type: model.class.name)
- else - else
- preview_url = preview_markdown_path(project) - preview_url = preview_markdown_path(project)
......
- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true) - supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true)
- supports_quick_actions = note_supports_quick_actions?(@note) - supports_quick_actions = note_supports_quick_actions?(@note)
- if supports_quick_actions - if supports_quick_actions
- preview_url = preview_markdown_path(@project, quick_actions_target_type: @note.noteable_type, quick_actions_target_id: @note.noteable_id) - preview_url = preview_markdown_path(@project, target_type: @note.noteable_type, target_id: @note.noteable_id)
- else - else
- preview_url = preview_markdown_path(@project) - preview_url = preview_markdown_path(@project)
......
---
title: Support multi-line suggestions
merge_request: 25211
author:
type: added
...@@ -344,6 +344,24 @@ and push the suggested change directly into the codebase in the merge request's ...@@ -344,6 +344,24 @@ and push the suggested change directly into the codebase in the merge request's
Custom commit messages will be introduced by Custom commit messages will be introduced by
[#54404](https://gitlab.com/gitlab-org/gitlab-ce/issues/54404). [#54404](https://gitlab.com/gitlab-org/gitlab-ce/issues/54404).
### Multi-line suggestions
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53310) in GitLab 11.10.
Reviewers can also suggest changes to
multiple lines with a single suggestion within Merge Request diff discussions.
![Multi-line suggestion syntax](img/multi-line-suggestion-syntax.png)
In the example above, the suggestion covers three lines above and four lines below the commented diff line.
It'd change from 3 lines _above_ to 4 lines _below_ the commented Diff line.
![Multi-line suggestion preview](img/multi-line-suggestion-preview.png)
NOTE: **Note:**
Suggestions covering multiple lines are limited to 100 lines _above_ and 100 lines _below_
the commented diff line, allowing up to 200 changed lines per suggestion.
## Start a discussion by replying to a standard comment ## Start a discussion by replying to a standard comment
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30299) in GitLab 11.9 > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30299) in GitLab 11.9
......
# frozen_string_literal: true
# TODO: Delete when https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26107
# exchange this parser by `Gitlab::Diff::SuggestionsParser`.
module Banzai
module SuggestionsParser
# Returns the content of each suggestion code block.
#
def self.parse(text)
html = Banzai.render(text, project: nil, no_original_data: true)
doc = Nokogiri::HTML(html)
doc.search('pre.suggestion').map { |node| node.text }
end
end
end
...@@ -703,6 +703,16 @@ describe ProjectsController do ...@@ -703,6 +703,16 @@ describe ProjectsController do
expect(JSON.parse(response.body).keys).to match_array(%w(body references)) expect(JSON.parse(response.body).keys).to match_array(%w(body references))
end end
context 'when not authorized' do
let(:private_project) { create(:project, :private) }
it 'returns 404' do
post :preview_markdown, params: { namespace_id: private_project.namespace, id: private_project, text: '*Markdown* text' }
expect(response).to have_gitlab_http_status(404)
end
end
context 'state filter on references' do context 'state filter on references' do
let(:issue) { create(:issue, :closed, project: public_project) } let(:issue) { create(:issue, :closed, project: public_project) }
let(:merge_request) { create(:merge_request, :closed, target_project: public_project) } let(:merge_request) { create(:merge_request, :closed, target_project: public_project) }
......
...@@ -6,6 +6,14 @@ describe 'User comments on a diff', :js do ...@@ -6,6 +6,14 @@ describe 'User comments on a diff', :js do
include MergeRequestDiffHelpers include MergeRequestDiffHelpers
include RepoHelpers include RepoHelpers
def expect_suggestion_has_content(element, expected_changing_content, expected_suggested_content)
changing_content = element.all(:css, '.line_holder.old').map(&:text)
suggested_content = element.all(:css, '.line_holder.new').map(&:text)
expect(changing_content).to eq(expected_changing_content)
expect(suggested_content).to eq(expected_suggested_content)
end
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:merge_request) do let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test') create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
...@@ -33,8 +41,18 @@ describe 'User comments on a diff', :js do ...@@ -33,8 +41,18 @@ describe 'User comments on a diff', :js do
page.within('.diff-discussions') do page.within('.diff-discussions') do
expect(page).to have_button('Apply suggestion') expect(page).to have_button('Apply suggestion')
expect(page).to have_content('Suggested change') expect(page).to have_content('Suggested change')
expect(page).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git') end
expect(page).to have_content('# change to a comment')
page.within('.md-suggestion-diff') do
expected_changing_content = [
"6 url = https://github.com/gitlabhq/gitlab-shell.git"
]
expected_suggested_content = [
"6 # change to a comment"
]
expect_suggestion_has_content(page, expected_changing_content, expected_suggested_content)
end end
end end
...@@ -64,7 +82,7 @@ describe 'User comments on a diff', :js do ...@@ -64,7 +82,7 @@ describe 'User comments on a diff', :js do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']")) click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do page.within('.js-discussion-note-form') do
fill_in('note_note', with: "```suggestion\n# change to a comment\n```\n```suggestion\n# or that\n```") fill_in('note_note', with: "```suggestion\n# change to a comment\n```\n```suggestion:-2\n# or that\n# heh\n```")
click_button('Comment') click_button('Comment')
end end
...@@ -74,11 +92,90 @@ describe 'User comments on a diff', :js do ...@@ -74,11 +92,90 @@ describe 'User comments on a diff', :js do
suggestion_1 = page.all(:css, '.md-suggestion-diff')[0] suggestion_1 = page.all(:css, '.md-suggestion-diff')[0]
suggestion_2 = page.all(:css, '.md-suggestion-diff')[1] suggestion_2 = page.all(:css, '.md-suggestion-diff')[1]
expect(suggestion_1).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git') suggestion_1_expected_changing_content = [
expect(suggestion_1).to have_content('# change to a comment') "6 url = https://github.com/gitlabhq/gitlab-shell.git"
]
suggestion_1_expected_suggested_content = [
"6 # change to a comment"
]
suggestion_2_expected_changing_content = [
"4 [submodule \"gitlab-shell\"]",
"5 path = gitlab-shell",
"6 url = https://github.com/gitlabhq/gitlab-shell.git"
]
suggestion_2_expected_suggested_content = [
"4 # or that",
"5 # heh"
]
expect_suggestion_has_content(suggestion_1,
suggestion_1_expected_changing_content,
suggestion_1_expected_suggested_content)
expect_suggestion_has_content(suggestion_2,
suggestion_2_expected_changing_content,
suggestion_2_expected_suggested_content)
end
end
end
context 'multi-line suggestions' do
it 'suggestion is presented' do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```")
click_button('Comment')
end
expect(suggestion_2).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git') wait_for_requests
expect(suggestion_2).to have_content('# or that')
page.within('.diff-discussions') do
expect(page).to have_button('Apply suggestion')
expect(page).to have_content('Suggested change')
end
page.within('.md-suggestion-diff') do
expected_changing_content = [
"3 url = git://github.com/randx/six.git",
"4 [submodule \"gitlab-shell\"]",
"5 path = gitlab-shell",
"6 url = https://github.com/gitlabhq/gitlab-shell.git",
"7 [submodule \"gitlab-grack\"]",
"8 path = gitlab-grack",
"9 url = https://gitlab.com/gitlab-org/gitlab-grack.git"
]
expected_suggested_content = [
"3 # change to a",
"4 # comment",
"5 # with",
"6 # broken",
"7 # lines"
]
expect_suggestion_has_content(page, expected_changing_content, expected_suggested_content)
end
end
it 'suggestion is appliable' do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```")
click_button('Comment')
end
wait_for_requests
page.within('.diff-discussions') do
expect(page).not_to have_content('Applied')
click_button('Apply suggestion')
wait_for_requests
expect(page).to have_content('Applied')
end end
end end
end end
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import SuggestionDiffRow from '~/vue_shared/components/markdown/suggestion_diff_row.vue';
const oldLine = {
can_receive_suggestion: false,
line_code: null,
meta_data: null,
new_line: null,
old_line: 5,
rich_text: '-oldtext',
text: '-oldtext',
type: 'old',
};
const newLine = {
can_receive_suggestion: false,
line_code: null,
meta_data: null,
new_line: 6,
old_line: null,
rich_text: '-newtext',
text: '-newtext',
type: 'new',
};
describe(SuggestionDiffRow.name, () => {
let wrapper;
const factory = (options = {}) => {
const localVue = createLocalVue();
wrapper = shallowMount(SuggestionDiffRow, {
localVue,
...options,
});
};
const findOldLineWrapper = () => wrapper.find('.old_line');
const findNewLineWrapper = () => wrapper.find('.new_line');
afterEach(() => {
wrapper.destroy();
});
it('renders correctly', () => {
factory({
propsData: {
line: oldLine,
},
});
expect(wrapper.is('.line_holder')).toBe(true);
});
describe('when passed line has type old', () => {
beforeEach(() => {
factory({
propsData: {
line: oldLine,
},
});
});
it('has old class when line has type old', () => {
expect(wrapper.find('td').classes()).toContain('old');
});
it('has old line number rendered', () => {
expect(findOldLineWrapper().text()).toBe('5');
});
it('has no new line number rendered', () => {
expect(findNewLineWrapper().text()).toBe('');
});
});
describe('when passed line has type new', () => {
beforeEach(() => {
factory({
propsData: {
line: newLine,
},
});
});
it('has new class when line has type new', () => {
expect(wrapper.find('td').classes()).toContain('new');
});
it('has no old line number rendered', () => {
expect(findOldLineWrapper().text()).toBe('');
});
it('has no new line number rendered', () => {
expect(findNewLineWrapper().text()).toBe('6');
});
});
});
...@@ -44,8 +44,7 @@ export const noteableDataMock = { ...@@ -44,8 +44,7 @@ export const noteableDataMock = {
milestone: null, milestone: null,
milestone_id: null, milestone_id: null,
moved_to_id: null, moved_to_id: null,
preview_note_path: preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?target_id=98&target_type=Issue',
'/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
project_id: 2, project_id: 2,
state: 'opened', state: 'opened',
time_estimate: 0, time_estimate: 0,
...@@ -347,8 +346,7 @@ export const loggedOutnoteableData = { ...@@ -347,8 +346,7 @@ export const loggedOutnoteableData = {
}, },
noteable_note_url: '/group/project/merge_requests/1#note_1', noteable_note_url: '/group/project/merge_requests/1#note_1',
create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue', create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue',
preview_note_path: preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?target_id=98&target_type=Issue',
'/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
}; };
export const collapseNotesMock = [ export const collapseNotesMock = [
......
...@@ -98,7 +98,7 @@ describe('Markdown field header component', () => { ...@@ -98,7 +98,7 @@ describe('Markdown field header component', () => {
it('renders suggestion template', () => { it('renders suggestion template', () => {
vm.lineContent = 'Some content'; vm.lineContent = 'Some content';
expect(vm.mdSuggestion).toEqual('```suggestion\n{text}\n```'); expect(vm.mdSuggestion).toEqual('```suggestion:-0+0\n{text}\n```');
}); });
it('does not render suggestion button if `canSuggest` is set to false', () => { it('does not render suggestion button if `canSuggest` is set to false', () => {
......
import Vue from 'vue'; import Vue from 'vue';
import SuggestionDiffComponent from '~/vue_shared/components/markdown/suggestion_diff.vue'; import SuggestionDiffComponent from '~/vue_shared/components/markdown/suggestion_diff.vue';
import { selectDiffLines } from '~/vue_shared/components/lib/utils/diff_utils';
const MOCK_DATA = { const MOCK_DATA = {
canApply: true, canApply: true,
newLines: [
{ content: 'Line 1\n', lineNumber: 1 },
{ content: 'Line 2\n', lineNumber: 2 },
{ content: 'Line 3\n', lineNumber: 3 },
],
fromLine: 1,
fromContent: 'Old content',
suggestion: { suggestion: {
id: 1, id: 1,
diff_lines: [
{
can_receive_suggestion: false,
line_code: null,
meta_data: null,
new_line: null,
old_line: 5,
rich_text: '-test',
text: '-test',
type: 'old',
},
{
can_receive_suggestion: true,
line_code: null,
meta_data: null,
new_line: 5,
old_line: null,
rich_text: '+new test',
text: '+new test',
type: 'new',
},
{
can_receive_suggestion: true,
line_code: null,
meta_data: null,
new_line: 5,
old_line: null,
rich_text: '+new test2',
text: '+new test2',
type: 'new',
},
],
}, },
helpPagePath: 'path_to_docs', helpPagePath: 'path_to_docs',
}; };
const lines = selectDiffLines(MOCK_DATA.suggestion.diff_lines);
const newLines = lines.filter(line => line.type === 'new');
describe('Suggestion Diff component', () => { describe('Suggestion Diff component', () => {
let vm; let vm;
...@@ -39,30 +68,23 @@ describe('Suggestion Diff component', () => { ...@@ -39,30 +68,23 @@ describe('Suggestion Diff component', () => {
}); });
it('renders the oldLineNumber', () => { it('renders the oldLineNumber', () => {
const fromLine = vm.$el.querySelector('.qa-old-diff-line-number').innerHTML; const fromLine = vm.$el.querySelector('.old_line').innerHTML;
expect(parseInt(fromLine, 10)).toBe(vm.fromLine); expect(parseInt(fromLine, 10)).toBe(lines[0].old_line);
}); });
it('renders the oldLineContent', () => { it('renders the oldLineContent', () => {
const fromContent = vm.$el.querySelector('.line_content.old').innerHTML; const fromContent = vm.$el.querySelector('.line_content.old').innerHTML;
expect(fromContent.includes(vm.fromContent)).toBe(true); expect(fromContent.includes(lines[0].text)).toBe(true);
});
it('renders the contents of newLines', () => {
const newLines = vm.$el.querySelectorAll('.line_holder.new');
newLines.forEach((line, i) => {
expect(newLines[i].innerHTML.includes(vm.newLines[i].content)).toBe(true);
});
}); });
it('renders a line number for each line', () => { it('renders new lines', () => {
const newLineNumbers = vm.$el.querySelectorAll('.qa-new-diff-line-number'); const newLinesElements = vm.$el.querySelectorAll('.line_holder.new');
newLineNumbers.forEach((line, i) => { newLinesElements.forEach((line, i) => {
expect(newLineNumbers[i].innerHTML.includes(vm.newLines[i].lineNumber)).toBe(true); expect(newLinesElements[i].innerHTML.includes(newLines[i].new_line)).toBe(true);
expect(newLinesElements[i].innerHTML.includes(newLines[i].text)).toBe(true);
}); });
}); });
}); });
......
...@@ -2,46 +2,52 @@ import Vue from 'vue'; ...@@ -2,46 +2,52 @@ import Vue from 'vue';
import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue'; import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue';
const MOCK_DATA = { const MOCK_DATA = {
fromLine: 1, suggestions: [
fromContent: 'Old content', {
suggestions: [], id: 1,
appliable: true,
applied: false,
current_user: {
can_apply: true,
},
diff_lines: [
{
can_receive_suggestion: false,
line_code: null,
meta_data: null,
new_line: null,
old_line: 5,
rich_text: '-test',
text: '-test',
type: 'old',
},
{
can_receive_suggestion: true,
line_code: null,
meta_data: null,
new_line: 5,
old_line: null,
rich_text: '+new test',
text: '+new test',
type: 'new',
},
],
},
],
noteHtml: ` noteHtml: `
<div class="suggestion">
<div class="line">-oldtest</div>
</div>
<div class="suggestion"> <div class="suggestion">
<div class="line">Suggestion 1</div> <div class="line">+newtest</div>
</div> </div>
<div class="suggestion">
<div class="line">Suggestion 2</div>
</div>
`, `,
isApplied: false, isApplied: false,
helpPagePath: 'path_to_docs', helpPagePath: 'path_to_docs',
}; };
const generateLine = content => {
const line = document.createElement('div');
line.className = 'line';
line.innerHTML = content;
return line;
};
const generateMockLines = () => {
const line1 = generateLine('Line 1');
const line2 = generateLine('Line 2');
const line3 = generateLine('- Line 3');
const container = document.createElement('div');
container.appendChild(line1);
container.appendChild(line2);
container.appendChild(line3);
return container;
};
describe('Suggestion component', () => { describe('Suggestion component', () => {
let vm; let vm;
let extractedLines;
let diffTable; let diffTable;
beforeEach(done => { beforeEach(done => {
...@@ -51,8 +57,7 @@ describe('Suggestion component', () => { ...@@ -51,8 +57,7 @@ describe('Suggestion component', () => {
propsData: MOCK_DATA, propsData: MOCK_DATA,
}).$mount(); }).$mount();
extractedLines = vm.extractNewLines(generateMockLines()); diffTable = vm.generateDiff(0).$mount().$el;
diffTable = vm.generateDiff(extractedLines).$mount().$el;
spyOn(vm, 'renderSuggestions'); spyOn(vm, 'renderSuggestions');
vm.renderSuggestions(); vm.renderSuggestions();
...@@ -70,32 +75,8 @@ describe('Suggestion component', () => { ...@@ -70,32 +75,8 @@ describe('Suggestion component', () => {
it('renders suggestions', () => { it('renders suggestions', () => {
expect(vm.renderSuggestions).toHaveBeenCalled(); expect(vm.renderSuggestions).toHaveBeenCalled();
expect(vm.$el.innerHTML.includes('Suggestion 1')).toBe(true); expect(vm.$el.innerHTML.includes('oldtest')).toBe(true);
expect(vm.$el.innerHTML.includes('Suggestion 2')).toBe(true); expect(vm.$el.innerHTML.includes('newtest')).toBe(true);
});
});
describe('extractNewLines', () => {
it('extracts suggested lines', () => {
const expectedReturn = [
{ content: 'Line 1\n', lineNumber: 1 },
{ content: 'Line 2\n', lineNumber: 2 },
{ content: '- Line 3\n', lineNumber: 3 },
];
expect(vm.extractNewLines(generateMockLines())).toEqual(expectedReturn);
});
it('increments line number for each extracted line', () => {
expect(extractedLines[0].lineNumber).toEqual(1);
expect(extractedLines[1].lineNumber).toEqual(2);
expect(extractedLines[2].lineNumber).toEqual(3);
});
it('returns empty array if no lines are found', () => {
const el = document.createElement('div');
expect(vm.extractNewLines(el)).toEqual([]);
}); });
}); });
...@@ -109,17 +90,17 @@ describe('Suggestion component', () => { ...@@ -109,17 +90,17 @@ describe('Suggestion component', () => {
}); });
it('generates a diff table that contains contents the suggested lines', () => { it('generates a diff table that contains contents the suggested lines', () => {
extractedLines.forEach((line, i) => { MOCK_DATA.suggestions[0].diff_lines.forEach(line => {
expect(diffTable.innerHTML.includes(extractedLines[i].content)).toBe(true); const text = line.text.substring(1);
expect(diffTable.innerHTML.includes(text)).toBe(true);
}); });
}); });
it('generates a diff table with the correct line number for each suggested line', () => { it('generates a diff table with the correct line number for each suggested line', () => {
const lines = diffTable.getElementsByClassName('qa-new-diff-line-number'); const lines = diffTable.querySelectorAll('.old_line');
expect([...lines][0].innerHTML).toBe('1'); expect(parseInt([...lines][0].innerHTML, 10)).toBe(5);
expect([...lines][1].innerHTML).toBe('2');
expect([...lines][2].innerHTML).toBe('3');
}); });
}); });
}); });
# frozen_string_literal: true
require 'spec_helper'
describe Banzai::SuggestionsParser do
describe '.parse' do
it 'returns a list of suggestion contents' do
markdown = <<-MARKDOWN.strip_heredoc
```suggestion
foo
bar
```
```
nothing
```
```suggestion
xpto
baz
```
```thing
this is not a suggestion, it's a thing
```
MARKDOWN
expect(described_class.parse(markdown)).to eq([" foo\n bar",
" xpto\n baz"])
end
end
end
...@@ -10,6 +10,16 @@ describe Gitlab::Diff::Suggestion do ...@@ -10,6 +10,16 @@ describe Gitlab::Diff::Suggestion do
lines_above: above, lines_above: above,
lines_below: below) lines_below: below)
end end
it 'returns diff lines with correct line numbers' do
diff_lines = suggestion.diff_lines
expect(diff_lines).to all(be_a(Gitlab::Diff::Line))
expected_diff_lines.each_with_index do |expected_line, index|
expect(diff_lines[index].to_hash).to include(expected_line)
end
end
end end
let(:merge_request) { create(:merge_request) } let(:merge_request) { create(:merge_request) }
...@@ -48,6 +58,18 @@ describe Gitlab::Diff::Suggestion do ...@@ -48,6 +58,18 @@ describe Gitlab::Diff::Suggestion do
let(:expected_above) { line - 1 } let(:expected_above) { line - 1 }
let(:expected_below) { below } let(:expected_below) { below }
let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) }
let(:expected_diff_lines) do
[
{ old_pos: 1, new_pos: 1, type: 'old', text: "-require 'fileutils'" },
{ old_pos: 2, new_pos: 1, type: 'old', text: "-require 'open3'" },
{ old_pos: 3, new_pos: 1, type: 'old', text: "-" },
{ old_pos: 4, new_pos: 1, type: 'old', text: "-module Popen" },
{ old_pos: 5, new_pos: 1, type: 'old', text: "- extend self" },
{ old_pos: 6, new_pos: 1, type: 'old', text: "-" },
{ old_pos: 7, new_pos: 1, type: 'new', text: "+# parsed suggestion content" },
{ old_pos: 7, new_pos: 2, type: 'new', text: "+# with comments" }
]
end
it_behaves_like 'correct suggestion raw content' it_behaves_like 'correct suggestion raw content'
end end
...@@ -59,6 +81,47 @@ describe Gitlab::Diff::Suggestion do ...@@ -59,6 +81,47 @@ describe Gitlab::Diff::Suggestion do
let(:expected_below) { below } let(:expected_below) { below }
let(:expected_above) { above } let(:expected_above) { above }
let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) }
let(:expected_diff_lines) do
[
{ old_pos: 4, new_pos: 4, type: "match", text: "@@ -4 +4" },
{ old_pos: 4, new_pos: 4, type: "old", text: "-module Popen" },
{ old_pos: 5, new_pos: 4, type: "old", text: "- extend self" },
{ old_pos: 6, new_pos: 4, type: "old", text: "-" },
{ old_pos: 7, new_pos: 4, type: "old", text: "- def popen(cmd, path=nil)" },
{ old_pos: 8, new_pos: 4, type: "old", text: "- unless cmd.is_a?(Array)" },
{ old_pos: 9, new_pos: 4, type: "old", text: "- raise RuntimeError, \"System commands must be given as an array of strings\"" },
{ old_pos: 10, new_pos: 4, type: "old", text: "- end" },
{ old_pos: 11, new_pos: 4, type: "old", text: "-" },
{ old_pos: 12, new_pos: 4, type: "old", text: "- path ||= Dir.pwd" },
{ old_pos: 13, new_pos: 4, type: "old", text: "-" },
{ old_pos: 14, new_pos: 4, type: "old", text: "- vars = {" },
{ old_pos: 15, new_pos: 4, type: "old", text: "- \"PWD\" => path" },
{ old_pos: 16, new_pos: 4, type: "old", text: "- }" },
{ old_pos: 17, new_pos: 4, type: "old", text: "-" },
{ old_pos: 18, new_pos: 4, type: "old", text: "- options = {" },
{ old_pos: 19, new_pos: 4, type: "old", text: "- chdir: path" },
{ old_pos: 20, new_pos: 4, type: "old", text: "- }" },
{ old_pos: 21, new_pos: 4, type: "old", text: "-" },
{ old_pos: 22, new_pos: 4, type: "old", text: "- unless File.directory?(path)" },
{ old_pos: 23, new_pos: 4, type: "old", text: "- FileUtils.mkdir_p(path)" },
{ old_pos: 24, new_pos: 4, type: "old", text: "- end" },
{ old_pos: 25, new_pos: 4, type: "old", text: "-" },
{ old_pos: 26, new_pos: 4, type: "old", text: "- @cmd_output = \"\"" },
{ old_pos: 27, new_pos: 4, type: "old", text: "- @cmd_status = 0" },
{ old_pos: 28, new_pos: 4, type: "old", text: "-" },
{ old_pos: 29, new_pos: 4, type: "old", text: "- Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|" },
{ old_pos: 30, new_pos: 4, type: "old", text: "- @cmd_output << stdout.read" },
{ old_pos: 31, new_pos: 4, type: "old", text: "- @cmd_output << stderr.read" },
{ old_pos: 32, new_pos: 4, type: "old", text: "- @cmd_status = wait_thr.value.exitstatus" },
{ old_pos: 33, new_pos: 4, type: "old", text: "- end" },
{ old_pos: 34, new_pos: 4, type: "old", text: "-" },
{ old_pos: 35, new_pos: 4, type: "old", text: "- return @cmd_output, @cmd_status" },
{ old_pos: 36, new_pos: 4, type: "old", text: "- end" },
{ old_pos: 37, new_pos: 4, type: "old", text: "-end" },
{ old_pos: 38, new_pos: 4, type: "new", text: "+# parsed suggestion content" },
{ old_pos: 38, new_pos: 5, type: "new", text: "+# with comments" }
]
end
it_behaves_like 'correct suggestion raw content' it_behaves_like 'correct suggestion raw content'
end end
...@@ -70,17 +133,19 @@ describe Gitlab::Diff::Suggestion do ...@@ -70,17 +133,19 @@ describe Gitlab::Diff::Suggestion do
let(:expected_below) { below } let(:expected_below) { below }
let(:expected_above) { above } let(:expected_above) { above }
let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) }
let(:expected_diff_lines) do
it_behaves_like 'correct suggestion raw content' [
end { old_pos: 3, new_pos: 3, type: "match", text: "@@ -3 +3" },
{ old_pos: 3, new_pos: 3, type: "old", text: "-" },
context 'when no extra lines (single-line suggestion)' do { old_pos: 4, new_pos: 3, type: "old", text: "-module Popen" },
let(:line) { 5 } { old_pos: 5, new_pos: 3, type: "old", text: "- extend self" },
let(:above) { 0 } { old_pos: 6, new_pos: 3, type: "old", text: "-" },
let(:below) { 0 } { old_pos: 7, new_pos: 3, type: "old", text: "- def popen(cmd, path=nil)" },
let(:expected_below) { below } { old_pos: 8, new_pos: 3, type: "old", text: "- unless cmd.is_a?(Array)" },
let(:expected_above) { above } { old_pos: 9, new_pos: 3, type: "new", text: "+# parsed suggestion content" },
let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } { old_pos: 9, new_pos: 4, type: "new", text: "+# with comments" }
]
end
it_behaves_like 'correct suggestion raw content' it_behaves_like 'correct suggestion raw content'
end end
......
...@@ -69,5 +69,66 @@ describe Gitlab::Diff::SuggestionsParser do ...@@ -69,5 +69,66 @@ describe Gitlab::Diff::SuggestionsParser do
lines_below: 0) lines_below: 0)
end end
end end
context 'multi-line suggestions' do
let(:markdown) do
<<-MARKDOWN.strip_heredoc
```suggestion:-2+1
# above and below
```
```
nothing
```
```suggestion:-3
# only above
```
```suggestion:+3
# only below
```
```thing
this is not a suggestion, it's a thing
```
MARKDOWN
end
it 'returns a list of Gitlab::Diff::Suggestion' do
expect(subject).to all(be_a(Gitlab::Diff::Suggestion))
expect(subject.size).to eq(3)
end
it 'suggestion with above and below param has correct data' do
from_line = position.new_line - 2
to_line = position.new_line + 1
expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line),
to_content: " # above and below\n",
lines_above: 2,
lines_below: 1)
end
it 'suggestion with above param has correct data' do
from_line = position.new_line - 3
to_line = position.new_line
expect(subject.second.to_hash).to eq(from_content: blob_lines_data(from_line, to_line),
to_content: " # only above\n",
lines_above: 3,
lines_below: 0)
end
it 'suggestion with below param has correct data' do
from_line = position.new_line
to_line = position.new_line + 3
expect(subject.third.to_hash).to eq(from_content: blob_lines_data(from_line, to_line),
to_content: " # only below\n",
lines_above: 0,
lines_below: 3)
end
end
end end
end end
...@@ -21,6 +21,22 @@ describe Suggestion do ...@@ -21,6 +21,22 @@ describe Suggestion do
end end
end end
describe '#diff_lines' do
let(:suggestion) { create(:suggestion, :content_from_repo) }
it 'returns parsed diff lines' do
expected_diff_lines = Gitlab::Diff::SuggestionDiff.new(suggestion).diff_lines
diff_lines = suggestion.diff_lines
expect(diff_lines.size).to eq(expected_diff_lines.size)
expect(diff_lines).to all(be_a(Gitlab::Diff::Line))
expected_diff_lines.each_with_index do |expected_line, index|
expect(diff_lines[index].to_hash).to eq(expected_line.to_hash)
end
end
end
describe '#appliable?' do describe '#appliable?' do
context 'when note does not support suggestions' do context 'when note does not support suggestions' do
it 'returns false' do it 'returns false' do
......
...@@ -13,8 +13,7 @@ describe SuggestionEntity do ...@@ -13,8 +13,7 @@ describe SuggestionEntity do
subject { entity.as_json } subject { entity.as_json }
it 'exposes correct attributes' do it 'exposes correct attributes' do
expect(subject).to include(:id, :from_line, :to_line, :appliable, expect(subject.keys).to match_array([:id, :appliable, :applied, :diff_lines, :current_user])
:applied, :from_content, :to_content)
end end
it 'exposes current user abilities' do it 'exposes current user abilities' do
......
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe PreviewMarkdownService do describe PreviewMarkdownService do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project, :repository) }
before do before do
project.add_developer(user) project.add_developer(user)
...@@ -20,23 +20,72 @@ describe PreviewMarkdownService do ...@@ -20,23 +20,72 @@ describe PreviewMarkdownService do
end end
describe 'suggestions' do describe 'suggestions' do
let(:params) { { text: "```suggestion\nfoo\n```", preview_suggestions: preview_suggestions } } let(:merge_request) do
create(:merge_request, target_project: project, source_project: project)
end
let(:text) { "```suggestion\nfoo\n```" }
let(:params) do
suggestion_params.merge(text: text,
target_type: 'MergeRequest',
target_id: merge_request.iid)
end
let(:service) { described_class.new(project, user, params) } let(:service) { described_class.new(project, user, params) }
context 'when preview markdown param is present' do context 'when preview markdown param is present' do
let(:preview_suggestions) { true } let(:path) { "files/ruby/popen.rb" }
let(:line) { 10 }
let(:diff_refs) { merge_request.diff_refs }
let(:suggestion_params) do
{
preview_suggestions: true,
file_path: path,
line: line,
base_sha: diff_refs.base_sha,
start_sha: diff_refs.start_sha,
head_sha: diff_refs.head_sha
}
end
it 'returns suggestions referenced in text' do
position = Gitlab::Diff::Position.new(new_path: path,
new_line: line,
diff_refs: diff_refs)
expect(Gitlab::Diff::SuggestionsParser)
.to receive(:parse)
.with(text, position: position, project: merge_request.project)
.and_call_original
it 'returns users referenced in text' do
result = service.execute result = service.execute
expect(result[:suggestions]).to eq(['foo']) expect(result[:suggestions]).to all(be_a(Gitlab::Diff::Suggestion))
end
context 'when user is not authorized' do
let(:another_user) { create(:user) }
let(:service) { described_class.new(project, another_user, params) }
before do
project.add_guest(another_user)
end
it 'returns no suggestions' do
result = service.execute
expect(result[:suggestions]).to be_empty
end
end end
end end
context 'when preview markdown param is not present' do context 'when preview markdown param is not present' do
let(:preview_suggestions) { false } let(:suggestion_params) do
{
preview_suggestions: false
}
end
it 'returns users referenced in text' do it 'returns suggestions referenced in text' do
result = service.execute result = service.execute
expect(result[:suggestions]).to eq([]) expect(result[:suggestions]).to eq([])
...@@ -49,8 +98,8 @@ describe PreviewMarkdownService do ...@@ -49,8 +98,8 @@ describe PreviewMarkdownService do
let(:params) do let(:params) do
{ {
text: "Please do it\n/assign #{user.to_reference}", text: "Please do it\n/assign #{user.to_reference}",
quick_actions_target_type: 'Issue', target_type: 'Issue',
quick_actions_target_id: issue.id target_id: issue.id
} }
end end
let(:service) { described_class.new(project, user, params) } let(:service) { described_class.new(project, user, params) }
...@@ -72,7 +121,7 @@ describe PreviewMarkdownService do ...@@ -72,7 +121,7 @@ describe PreviewMarkdownService do
let(:params) do let(:params) do
{ {
text: "My work\n/estimate 2y", text: "My work\n/estimate 2y",
quick_actions_target_type: 'MergeRequest' target_type: 'MergeRequest'
} }
end end
let(:service) { described_class.new(project, user, params) } let(:service) { described_class.new(project, user, params) }
...@@ -96,8 +145,8 @@ describe PreviewMarkdownService do ...@@ -96,8 +145,8 @@ describe PreviewMarkdownService do
let(:params) do let(:params) do
{ {
text: "My work\n/tag v1.2.3 Stable release", text: "My work\n/tag v1.2.3 Stable release",
quick_actions_target_type: 'Commit', target_type: 'Commit',
quick_actions_target_id: commit.id target_id: commit.id
} }
end end
let(:service) { described_class.new(project, user, params) } let(:service) { described_class.new(project, user, params) }
......
...@@ -51,6 +51,10 @@ describe Suggestions::ApplyService do ...@@ -51,6 +51,10 @@ describe Suggestions::ApplyService do
diff_refs: merge_request.diff_refs) diff_refs: merge_request.diff_refs)
end end
let(:diff_note) do
create(:diff_note_on_merge_request, noteable: merge_request, position: position, project: project)
end
let(:suggestion) do let(:suggestion) do
create(:suggestion, :content_from_repo, note: diff_note, create(:suggestion, :content_from_repo, note: diff_note,
to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n") to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n")
...@@ -108,12 +112,6 @@ describe Suggestions::ApplyService do ...@@ -108,12 +112,6 @@ describe Suggestions::ApplyService do
target_project: project) target_project: project)
end end
let!(:diff_note) do
create(:diff_note_on_merge_request, noteable: merge_request,
position: position,
project: project)
end
before do before do
project.add_maintainer(user) project.add_maintainer(user)
end end
...@@ -192,11 +190,6 @@ describe Suggestions::ApplyService do ...@@ -192,11 +190,6 @@ describe Suggestions::ApplyService do
CONTENT CONTENT
end end
let(:merge_request) do
create(:merge_request, source_project: project,
target_project: project)
end
def create_suggestion(diff, old_line: nil, new_line: nil, from_content:, to_content:, path:) def create_suggestion(diff, old_line: nil, new_line: nil, from_content:, to_content:, path:)
position = Gitlab::Diff::Position.new(old_path: path, position = Gitlab::Diff::Position.new(old_path: path,
new_path: path, new_path: path,
...@@ -291,6 +284,55 @@ describe Suggestions::ApplyService do ...@@ -291,6 +284,55 @@ describe Suggestions::ApplyService do
expect(suggestion_2_diff.strip).to eq(expected_suggestion_2_diff.strip) expect(suggestion_2_diff.strip).to eq(expected_suggestion_2_diff.strip)
end end
end end
context 'multi-line suggestion' do
let(:expected_content) do
<<~CONTENT
require 'fileutils'
require 'open3'
module Popen
extend self
# multi
# line
vars = {
"PWD" => path
}
options = {
chdir: path
}
unless File.directory?(path)
FileUtils.mkdir_p(path)
end
@cmd_output = ""
@cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
@cmd_output << stdout.read
@cmd_output << stderr.read
@cmd_status = wait_thr.value.exitstatus
end
return @cmd_output, @cmd_status
end
end
CONTENT
end
let(:suggestion) do
create(:suggestion, :content_from_repo, note: diff_note,
lines_above: 2,
lines_below: 3,
to_content: "# multi\n# line\n")
end
it_behaves_like 'successfully creates commit and updates suggestion'
end
end end
context 'fork-project' do context 'fork-project' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment