Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 14:18:50 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 14:18:50 +0300
commit8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 (patch)
treea77e7fe7a93de11213032ed4ab1f33a3db51b738 /lib/gitlab/suggestions
parent00b35af3db1abfe813a778f643dad221aad51fca (diff)
Add latest changes from gitlab-org/gitlab@13-1-stable-ee
Diffstat (limited to 'lib/gitlab/suggestions')
-rw-r--r--lib/gitlab/suggestions/commit_message.rb54
-rw-r--r--lib/gitlab/suggestions/file_suggestion.rb107
-rw-r--r--lib/gitlab/suggestions/suggestion_set.rb120
3 files changed, 281 insertions, 0 deletions
diff --git a/lib/gitlab/suggestions/commit_message.rb b/lib/gitlab/suggestions/commit_message.rb
new file mode 100644
index 00000000000..d59a8fc3730
--- /dev/null
+++ b/lib/gitlab/suggestions/commit_message.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Suggestions
+ class CommitMessage
+ DEFAULT_SUGGESTION_COMMIT_MESSAGE =
+ 'Apply %{suggestions_count} suggestion(s) to %{files_count} file(s)'
+
+ def initialize(user, suggestion_set)
+ @user = user
+ @suggestion_set = suggestion_set
+ end
+
+ def message
+ project = suggestion_set.project
+ user_defined_message = project.suggestion_commit_message.presence
+ message = user_defined_message || DEFAULT_SUGGESTION_COMMIT_MESSAGE
+
+ Gitlab::StringPlaceholderReplacer
+ .replace_string_placeholders(message, PLACEHOLDERS_REGEX) do |key|
+ PLACEHOLDERS[key].call(user, suggestion_set)
+ end
+ end
+
+ def self.format_paths(paths)
+ paths.sort.join(', ')
+ end
+
+ private_class_method :format_paths
+
+ private
+
+ attr_reader :user, :suggestion_set
+
+ PLACEHOLDERS = {
+ 'branch_name' => ->(user, suggestion_set) { suggestion_set.branch },
+ 'files_count' => ->(user, suggestion_set) { suggestion_set.file_paths.length },
+ 'file_paths' => ->(user, suggestion_set) { format_paths(suggestion_set.file_paths) },
+ 'project_name' => ->(user, suggestion_set) { suggestion_set.project.name },
+ 'project_path' => ->(user, suggestion_set) { suggestion_set.project.path },
+ 'user_full_name' => ->(user, suggestion_set) { user.name },
+ 'username' => ->(user, suggestion_set) { user.username },
+ 'suggestions_count' => ->(user, suggestion_set) { suggestion_set.suggestions.size }
+ }.freeze
+
+ # This regex is built dynamically using the keys from the PLACEHOLDER struct.
+ # So, we can easily add new placeholder just by modifying the PLACEHOLDER hash.
+ # This regex will build the new PLACEHOLDER_REGEX with the new information
+ PLACEHOLDERS_REGEX = Regexp.union(PLACEHOLDERS.keys.map do |key|
+ Regexp.new(Regexp.escape(key))
+ end).freeze
+ end
+ end
+end
diff --git a/lib/gitlab/suggestions/file_suggestion.rb b/lib/gitlab/suggestions/file_suggestion.rb
new file mode 100644
index 00000000000..73b9800f0b8
--- /dev/null
+++ b/lib/gitlab/suggestions/file_suggestion.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Suggestions
+ class FileSuggestion
+ include Gitlab::Utils::StrongMemoize
+
+ SuggestionForDifferentFileError = Class.new(StandardError)
+
+ def initialize
+ @suggestions = []
+ end
+
+ def add_suggestion(new_suggestion)
+ if for_different_file?(new_suggestion)
+ raise SuggestionForDifferentFileError,
+ 'Only add suggestions for the same file.'
+ end
+
+ suggestions << new_suggestion
+ end
+
+ def line_conflict?
+ strong_memoize(:line_conflict) do
+ _line_conflict?
+ end
+ end
+
+ def new_content
+ @new_content ||= _new_content
+ end
+
+ def file_path
+ @file_path ||= _file_path
+ end
+
+ private
+
+ attr_accessor :suggestions
+
+ def blob
+ first_suggestion&.diff_file&.new_blob
+ end
+
+ def blob_data_lines
+ blob.load_all_data!
+ blob.data.lines
+ end
+
+ def current_content
+ @current_content ||= blob.nil? ? [''] : blob_data_lines
+ end
+
+ def _new_content
+ current_content.tap do |content|
+ suggestions.each do |suggestion|
+ range = line_range(suggestion)
+ content[range] = suggestion.to_content
+ end
+ end.join
+ end
+
+ def line_range(suggestion)
+ suggestion.from_line_index..suggestion.to_line_index
+ end
+
+ def for_different_file?(suggestion)
+ file_path && file_path != suggestion_file_path(suggestion)
+ end
+
+ def suggestion_file_path(suggestion)
+ suggestion&.diff_file&.file_path
+ end
+
+ def first_suggestion
+ suggestions.first
+ end
+
+ def _file_path
+ suggestion_file_path(first_suggestion)
+ end
+
+ def _line_conflict?
+ has_conflict = false
+
+ suggestions.each_with_object([]) do |suggestion, ranges|
+ range_in_test = line_range(suggestion)
+
+ if has_range_conflict?(range_in_test, ranges)
+ has_conflict = true
+ break
+ end
+
+ ranges << range_in_test
+ end
+
+ has_conflict
+ end
+
+ def has_range_conflict?(range_in_test, ranges)
+ ranges.any? do |range|
+ range.overlaps?(range_in_test)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/suggestions/suggestion_set.rb b/lib/gitlab/suggestions/suggestion_set.rb
new file mode 100644
index 00000000000..22abef98bf0
--- /dev/null
+++ b/lib/gitlab/suggestions/suggestion_set.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Suggestions
+ class SuggestionSet
+ attr_reader :suggestions
+
+ def initialize(suggestions)
+ @suggestions = suggestions
+ end
+
+ def project
+ first_suggestion.project
+ end
+
+ def branch
+ first_suggestion.branch
+ end
+
+ def valid?
+ error_message.nil?
+ end
+
+ def error_message
+ @error_message ||= _error_message
+ end
+
+ def actions
+ @actions ||= suggestions_per_file.map do |file_path, file_suggestion|
+ {
+ action: 'update',
+ file_path: file_path,
+ content: file_suggestion.new_content
+ }
+ end
+ end
+
+ def file_paths
+ @file_paths ||= suggestions.map(&:file_path).uniq
+ end
+
+ private
+
+ def first_suggestion
+ suggestions.first
+ end
+
+ def suggestions_per_file
+ @suggestions_per_file ||= _suggestions_per_file
+ end
+
+ def _suggestions_per_file
+ suggestions.each_with_object({}) do |suggestion, result|
+ file_path = suggestion.diff_file.file_path
+ file_suggestion = result[file_path] ||= FileSuggestion.new
+ file_suggestion.add_suggestion(suggestion)
+ end
+ end
+
+ def file_suggestions
+ suggestions_per_file.values
+ end
+
+ def first_file_suggestion
+ file_suggestions.first
+ end
+
+ def _error_message
+ suggestions.each do |suggestion|
+ message = error_for_suggestion(suggestion)
+
+ return message if message
+ end
+
+ has_line_conflict = file_suggestions.any? do |file_suggestion|
+ file_suggestion.line_conflict?
+ end
+
+ if has_line_conflict
+ return _('Suggestions are not applicable as their lines cannot overlap.')
+ end
+
+ nil
+ end
+
+ def error_for_suggestion(suggestion)
+ unless suggestion.diff_file
+ return _('A file was not found.')
+ end
+
+ unless on_same_branch?(suggestion)
+ return _('Suggestions must all be on the same branch.')
+ end
+
+ unless suggestion.appliable?(cached: false)
+ return _('A suggestion is not applicable.')
+ end
+
+ unless latest_source_head?(suggestion)
+ return _('A file has been changed.')
+ end
+
+ nil
+ end
+
+ def on_same_branch?(suggestion)
+ branch == suggestion.branch
+ end
+
+ # Checks whether the latest source branch HEAD matches with
+ # the position HEAD we're using to update the file content. Since
+ # the persisted HEAD is updated async (for MergeRequest),
+ # it's more consistent to fetch this data directly from the
+ # repository.
+ def latest_source_head?(suggestion)
+ suggestion.position.head_sha == suggestion.noteable.source_branch_sha
+ end
+ end
+ end
+end