summaryrefslogtreecommitdiff
path: root/app/services/snippets/update_service.rb
blob: d83b21271c01c408cf4c8ff7a1b3c3ecfda78aa9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# frozen_string_literal: true

module Snippets
  class UpdateService < Snippets::BaseService
    COMMITTABLE_ATTRIBUTES = %w(file_name content).freeze

    UpdateError = Class.new(StandardError)

    # NOTE: For Snippets::UpdateService, we default the spam_params to nil, because spam_checking is not
    # necessary in many cases, and we don't want every caller to have to explicitly pass it as nil
    # to disable spam checking.
    def initialize(project:, current_user: nil, params: {}, spam_params: nil)
      super(project: project, current_user: current_user, params: params)
      @spam_params = spam_params
    end

    def execute(snippet)
      return invalid_params_error(snippet) unless valid_params?

      if visibility_changed?(snippet) && !visibility_allowed?(visibility_level)
        return forbidden_visibility_error(snippet)
      end

      update_snippet_attributes(snippet)

      Spam::SpamActionService.new(
        spammable: snippet,
        spam_params: spam_params,
        user: current_user,
        action: :update
      ).execute

      if save_and_commit(snippet)
        Gitlab::UsageDataCounters::SnippetCounter.count(:update)

        ServiceResponse.success(payload: { snippet: snippet })
      else
        snippet_error_response(snippet, 400)
      end
    end

    private

    attr_reader :spam_params

    def visibility_changed?(snippet)
      visibility_level && visibility_level.to_i != snippet.visibility_level
    end

    def update_snippet_attributes(snippet)
      # We can remove the following condition once
      # https://gitlab.com/gitlab-org/gitlab/-/issues/217801
      # is implemented.
      # Once we can perform different operations through this service
      # we won't need to keep track of the `content` and `file_name` fields
      #
      # If the repository does not exist we don't need to update `params`
      # because we need to commit the information from the database
      if snippet_actions.any? && snippet.repository_exists?
        params[:content] = snippet_actions[0].content if snippet_actions[0].content
        params[:file_name] = snippet_actions[0].file_path
      end

      snippet.assign_attributes(params)
    end

    def save_and_commit(snippet)
      return false unless snippet.save

      # If the updated attributes does not need to update
      # the repository we can just return
      return true unless committable_attributes?

      unless snippet.repository_exists?
        create_repository_for(snippet)
        create_first_commit_using_db_data(snippet)
      end

      create_commit(snippet)

      true
    rescue StandardError => e
      # Restore old attributes but re-assign changes so they're not lost
      unless snippet.previous_changes.empty?
        snippet.previous_changes.each { |attr, value| snippet[attr] = value[0] }
        snippet.save

        snippet.assign_attributes(params)
      end

      add_snippet_repository_error(snippet: snippet, error: e)

      Gitlab::ErrorTracking.log_exception(e, service: 'Snippets::UpdateService')

      # If the commit action failed we remove it because
      # we don't want to leave empty repositories
      # around, to allow cloning them.
      delete_repository(snippet) if repository_empty?(snippet)

      false
    end

    def create_repository_for(snippet)
      snippet.create_repository

      raise CreateRepositoryError, 'Repository could not be created' unless snippet.repository_exists?
    end

    # If the user provides `snippet_actions` and the repository
    # does not exist, we need to commit first the snippet info stored
    # in the database.  Mostly because the content inside `snippet_actions`
    # would assume that the file is already in the repository.
    def create_first_commit_using_db_data(snippet)
      return if snippet_actions.empty?

      attrs = commit_attrs(snippet, INITIAL_COMMIT_MSG)
      actions = [{ file_path: snippet.file_name, content: snippet.content }]

      snippet.snippet_repository.multi_files_action(current_user, actions, **attrs)
    end

    def create_commit(snippet)
      raise UpdateError unless snippet.snippet_repository

      attrs = commit_attrs(snippet, UPDATE_COMMIT_MSG)

      snippet.snippet_repository.multi_files_action(current_user, files_to_commit(snippet), **attrs)
    end

    # Because we are removing repositories we don't want to remove
    # any existing repository with data. Therefore, we cannot
    # rely on cached methods for that check in order to avoid losing
    # data.
    def repository_empty?(snippet)
      snippet.repository._uncached_exists? && !snippet.repository._uncached_has_visible_content?
    end

    def committable_attributes?
      (params.stringify_keys.keys & COMMITTABLE_ATTRIBUTES).present? || snippet_actions.any?
    end

    def build_actions_from_params(snippet)
      file_name_on_repo = snippet.file_name_on_repo

      [{ previous_path: file_name_on_repo,
         file_path: params[:file_name] || file_name_on_repo,
         content: params[:content] }]
    end
  end
end