sashimi_tanpopo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SashimiTanpopo
4
+ module Provider
5
+ # Apply recipe files and create Pull Request
6
+ class GitLab < Base
7
+ DEFAULT_API_ENDPOINT = "https://gitlab.com/api/v4"
8
+
9
+ MAX_RETRY_COUNT = 5
10
+
11
+ # @param recipe_paths [Array<String>]
12
+ # @param target_dir [String,nil]
13
+ # @param params [Hash<Symbol, String>]
14
+ # @param dry_run [Boolean]
15
+ # @param is_colored [Boolean] Whether show color diff
16
+ # @param git_username [String,nil]
17
+ # @param git_email [String,nil]
18
+ # @param commit_message [String]
19
+ # @param repository [String]
20
+ # @param access_token [String]
21
+ # @param api_endpoint [String]
22
+ # @param mr_title [String]
23
+ # @param mr_body [String]
24
+ # @param mr_source_branch [String] Merge Request source branch
25
+ # @param mr_target_branch [String] Merge Request target branch
26
+ # @param mr_assignees [Array<String>]
27
+ # @param mr_reviewers [Array<String>]
28
+ # @param mr_labels [Array<String>]
29
+ # @param is_draft_mr [Boolean] Whether create draft Pull Request
30
+ # @param is_auto_merge [Boolean] Whether enable auto-merge
31
+ def initialize(recipe_paths:, target_dir:, params:, dry_run:, is_colored:,
32
+ git_username:, git_email:, commit_message:,
33
+ repository:, access_token:, api_endpoint: DEFAULT_API_ENDPOINT,
34
+ mr_title:, mr_body:, mr_source_branch:, mr_target_branch:,
35
+ mr_assignees: [], mr_reviewers: [], mr_labels: [], is_draft_mr:, is_auto_merge:)
36
+ super(
37
+ recipe_paths: recipe_paths,
38
+ target_dir: target_dir,
39
+ params: params,
40
+ dry_run: dry_run,
41
+ is_colored: is_colored,
42
+ is_update_local: false,
43
+ )
44
+
45
+ @commit_message = commit_message
46
+ @repository = repository
47
+ @mr_title = mr_title
48
+ @mr_body = mr_body
49
+ @mr_source_branch = mr_source_branch
50
+ @mr_target_branch = mr_target_branch
51
+ @mr_assignees = mr_assignees
52
+ @mr_reviewers = mr_reviewers
53
+ @mr_labels = mr_labels
54
+ @is_draft_mr = is_draft_mr
55
+ @is_auto_merge = is_auto_merge
56
+
57
+ @gitlab = Gitlab.client(endpoint: api_endpoint, private_token: access_token)
58
+
59
+ @git_username =
60
+ if git_username
61
+ git_username
62
+ else
63
+ current_user_name
64
+ end
65
+
66
+ @git_email =
67
+ if git_email
68
+ git_email
69
+ else
70
+ "#{@git_username}@noreply.#{self.class.gitlab_host(api_endpoint)}"
71
+ end
72
+ end
73
+
74
+ # Apply recipe files
75
+ #
76
+ # @return [String] Created Merge Request URL
77
+ # @return [nil] Merge Request isn't created
78
+ def perform
79
+ changed_files = apply_recipe_files
80
+
81
+ return nil if changed_files.empty? || @dry_run
82
+
83
+ if exists_branch?(@mr_source_branch)
84
+ SashimiTanpopo.logger.info "Skipped because branch #{@pr_source_branch} already exists on #{@repository}"
85
+ return nil
86
+ end
87
+
88
+ create_branch_and_push_changes(changed_files)
89
+
90
+ mr = create_merge_request
91
+ SashimiTanpopo.logger.info "Merge Request is created: #{mr[:web_url]}"
92
+
93
+ if @is_auto_merge
94
+ set_auto_merge(mr[:iid])
95
+ SashimiTanpopo.logger.info "Set auto-merge to #{mr[:web_url]}"
96
+ end
97
+
98
+ mr[:web_url]
99
+ end
100
+
101
+ # @param username [String]
102
+ #
103
+ # @return [Integer]
104
+ # @return [nil] user is not found
105
+ #
106
+ # @see https://docs.gitlab.com/api/users/#as-a-regular-user
107
+ def get_user_id_from_user_name(username)
108
+ user = with_retry do
109
+ @gitlab.users(username: username).first
110
+ end
111
+ return nil unless user
112
+
113
+ user["id"].to_i
114
+ end
115
+
116
+ # @param username [String]
117
+ #
118
+ # @return [Integer]
119
+ #
120
+ # @raise [SashimiTanpopo::NotFoundUserError]
121
+ #
122
+ # @see https://docs.gitlab.com/api/users/#as-a-regular-user
123
+ def get_user_id_from_user_name!(username)
124
+ user_id = get_user_id_from_user_name(username)
125
+ raise NotFoundUserError, "#{username} isn't found" unless user_id
126
+
127
+ user_id
128
+ end
129
+
130
+ # @param usernames [Array<String>]
131
+ #
132
+ # @return [Array<Integer>]
133
+ #
134
+ # @raise [SashimiTanpopo::NotFoundUserError]
135
+ #
136
+ # @see https://docs.gitlab.com/api/users/#as-a-regular-user
137
+ def get_user_ids_from_user_names!(usernames)
138
+ Parallel.map(usernames, in_threads: 2) do |username|
139
+ get_user_id_from_user_name!(username)
140
+ end
141
+ end
142
+
143
+ # @param mode [String] e.g. `100644`, `100755`
144
+ #
145
+ # @return [Boolean]
146
+ def self.executable_mode?(mode)
147
+ (mode.to_i(8) & 1) != 0
148
+ end
149
+
150
+ # Get GitLab host from api_endpoint
151
+ #
152
+ # @param api_endpoint [String]
153
+ #
154
+ # @return [String]
155
+ def self.gitlab_host(api_endpoint)
156
+ matched = %r{^https?://(.+)/api}.match(api_endpoint)
157
+ return matched[1] if matched # steep:ignore
158
+
159
+ "example.com"
160
+ end
161
+
162
+ private
163
+
164
+ def with_retry
165
+ retry_count ||= 0 # steep:ignore
166
+
167
+ yield
168
+ rescue Gitlab::Error::MethodNotAllowed, Gitlab::Error::NotAcceptable, Gitlab::Error::Unprocessable => error
169
+ retry_count += 1 # steep:ignore
170
+
171
+ raise error if retry_count > MAX_RETRY_COUNT
172
+
173
+ SashimiTanpopo.logger.warn "Error is occurred and auto retry (#{retry_count}/#{MAX_RETRY_COUNT}): #{error}"
174
+
175
+ # 1, 2, 4, 8, 16 ...
176
+ sleep_time = 2 ** (retry_count - 1)
177
+
178
+ sleep sleep_time # steep:ignore
179
+
180
+ retry
181
+ end
182
+
183
+ # @return [String]
184
+ def current_user_name
185
+ user = with_retry do
186
+ @gitlab.user
187
+ end
188
+
189
+ user["username"]
190
+ end
191
+
192
+ # Whether exists branch on repository
193
+ #
194
+ # @param branch [String]
195
+ #
196
+ # @return [Boolean]
197
+ #
198
+ # @see https://docs.gitlab.com/api/branches/#get-single-repository-branch
199
+ def exists_branch?(branch)
200
+ with_retry do
201
+ @gitlab.branch(@repository, branch)
202
+ end
203
+ true
204
+ rescue Gitlab::Error::NotFound
205
+ false
206
+ end
207
+
208
+ # Create branch on repository and push changes
209
+ #
210
+ # @param changed_files [Hash<String, { before_content: String, after_content: String, mode: String }>] key: file path, value: Hash
211
+ #
212
+ # @see https://docs.gitlab.com/api/commits/#create-a-commit-with-multiple-files-and-actions
213
+ def create_branch_and_push_changes(changed_files)
214
+ actions = changed_files.map do |file_path, changed_file|
215
+ {
216
+ action: "update",
217
+ file_path: file_path,
218
+ execute_filemode: self.class.executable_mode?(changed_file[:mode]),
219
+ content: changed_file[:after_content],
220
+ }
221
+ end
222
+
223
+ with_retry do
224
+ @gitlab.create_commit(
225
+ @repository,
226
+ @mr_source_branch,
227
+ @commit_message,
228
+ actions,
229
+ start_branch: @mr_target_branch,
230
+ author_email: @git_email,
231
+ author_name: @git_username,
232
+ )
233
+ end
234
+ end
235
+
236
+ # @return [Hash{iid: Integer, web_url: String}] Created Merge Request info
237
+ #
238
+ # @see https://docs.gitlab.com/api/merge_requests/#create-mr
239
+ def create_merge_request
240
+ params = {
241
+ source_branch: @mr_source_branch,
242
+ target_branch: @mr_target_branch,
243
+ remove_source_branch: true,
244
+ description: @mr_body,
245
+ }
246
+
247
+ params[:labels] = @mr_labels.join(",") unless @mr_labels.empty?
248
+
249
+ unless @mr_assignees.empty?
250
+ params[:assignee_ids] = get_user_ids_from_user_names!(@mr_assignees) # steep:ignore
251
+ end
252
+
253
+ unless @mr_reviewers.empty?
254
+ params[:reviewer_ids] = get_user_ids_from_user_names!(@mr_reviewers) # steep:ignore
255
+ end
256
+
257
+ mr_title =
258
+ if @is_draft_mr
259
+ "Draft: " + @mr_title
260
+ else
261
+ @mr_title
262
+ end
263
+
264
+ mr = with_retry do
265
+ @gitlab.create_merge_request(
266
+ @repository,
267
+ mr_title,
268
+ params,
269
+ )
270
+ end
271
+
272
+ {
273
+ iid: mr["iid"],
274
+ web_url: mr["web_url"],
275
+ }
276
+ end
277
+
278
+ # @param mr_iid [Integer]
279
+ #
280
+ # @see https://docs.gitlab.com/api/merge_requests/#merge-a-merge-request
281
+ def set_auto_merge(mr_iid)
282
+ with_retry do
283
+ @gitlab.accept_merge_request(@repository, mr_iid, auto_merge: true, should_remove_source_branch: true)
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SashimiTanpopo
4
+ module Provider
5
+ # Apply recipe files to local
6
+ class Local < Base
7
+ # @param recipe_paths [Array<String>]
8
+ # @param target_dir [String,nil]
9
+ # @param params [Hash<Symbol, String>]
10
+ # @param dry_run [Boolean]
11
+ # @param is_colored [Boolean] Whether show color diff
12
+ def initialize(recipe_paths:, target_dir:, params:, dry_run:, is_colored:)
13
+ super(
14
+ recipe_paths: recipe_paths,
15
+ target_dir: target_dir,
16
+ params: params,
17
+ dry_run: dry_run,
18
+ is_colored: is_colored,
19
+ is_update_local: true
20
+ )
21
+ end
22
+
23
+ def perform
24
+ apply_recipe_files
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "provider/base"
4
+ require_relative "provider/local"
5
+ require_relative "provider/github"
6
+ require_relative "provider/gitlab"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SashimiTanpopo
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "diffy"
5
+ require "octokit"
6
+ require "gitlab"
7
+ require "parallel"
8
+
9
+ require_relative "sashimi_tanpopo/version"
10
+ require_relative "sashimi_tanpopo/dsl"
11
+ require_relative "sashimi_tanpopo/logger"
12
+ require_relative "sashimi_tanpopo/provider"
13
+
14
+ module SashimiTanpopo
15
+ class Error < StandardError; end
16
+
17
+ class NotFoundUserError < Error; end
18
+ end
@@ -0,0 +1,196 @@
1
+ ---
2
+ path: ".gem_rbs_collection"
3
+ gems:
4
+ - name: addressable
5
+ version: '2.8'
6
+ source:
7
+ type: git
8
+ name: ruby/gem_rbs_collection
9
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
10
+ remote: https://github.com/ruby/gem_rbs_collection.git
11
+ repo_dir: gems
12
+ - name: base64
13
+ version: '0.1'
14
+ source:
15
+ type: git
16
+ name: ruby/gem_rbs_collection
17
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
18
+ remote: https://github.com/ruby/gem_rbs_collection.git
19
+ repo_dir: gems
20
+ - name: bigdecimal
21
+ version: '3.1'
22
+ source:
23
+ type: git
24
+ name: ruby/gem_rbs_collection
25
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
26
+ remote: https://github.com/ruby/gem_rbs_collection.git
27
+ repo_dir: gems
28
+ - name: csv
29
+ version: '3.3'
30
+ source:
31
+ type: git
32
+ name: ruby/gem_rbs_collection
33
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
34
+ remote: https://github.com/ruby/gem_rbs_collection.git
35
+ repo_dir: gems
36
+ - name: date
37
+ version: '0'
38
+ source:
39
+ type: stdlib
40
+ - name: dbm
41
+ version: '0'
42
+ source:
43
+ type: stdlib
44
+ - name: diff-lcs
45
+ version: '1.5'
46
+ source:
47
+ type: git
48
+ name: ruby/gem_rbs_collection
49
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
50
+ remote: https://github.com/ruby/gem_rbs_collection.git
51
+ repo_dir: gems
52
+ - name: erb
53
+ version: '0'
54
+ source:
55
+ type: stdlib
56
+ - name: faraday
57
+ version: '2.7'
58
+ source:
59
+ type: git
60
+ name: ruby/gem_rbs_collection
61
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
62
+ remote: https://github.com/ruby/gem_rbs_collection.git
63
+ repo_dir: gems
64
+ - name: fileutils
65
+ version: '0'
66
+ source:
67
+ type: stdlib
68
+ - name: forwardable
69
+ version: '0'
70
+ source:
71
+ type: stdlib
72
+ - name: hashdiff
73
+ version: '1.1'
74
+ source:
75
+ type: git
76
+ name: ruby/gem_rbs_collection
77
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
78
+ remote: https://github.com/ruby/gem_rbs_collection.git
79
+ repo_dir: gems
80
+ - name: httparty
81
+ version: '0.18'
82
+ source:
83
+ type: git
84
+ name: ruby/gem_rbs_collection
85
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
86
+ remote: https://github.com/ruby/gem_rbs_collection.git
87
+ repo_dir: gems
88
+ - name: io-console
89
+ version: '0'
90
+ source:
91
+ type: stdlib
92
+ - name: json
93
+ version: '0'
94
+ source:
95
+ type: stdlib
96
+ - name: logger
97
+ version: '0'
98
+ source:
99
+ type: stdlib
100
+ - name: mini_mime
101
+ version: '0.1'
102
+ source:
103
+ type: git
104
+ name: ruby/gem_rbs_collection
105
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
106
+ remote: https://github.com/ruby/gem_rbs_collection.git
107
+ repo_dir: gems
108
+ - name: monitor
109
+ version: '0'
110
+ source:
111
+ type: stdlib
112
+ - name: net-http
113
+ version: '0'
114
+ source:
115
+ type: stdlib
116
+ - name: net-protocol
117
+ version: '0'
118
+ source:
119
+ type: stdlib
120
+ - name: octokit
121
+ version: '8.0'
122
+ source:
123
+ type: git
124
+ name: ruby/gem_rbs_collection
125
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
126
+ remote: https://github.com/ruby/gem_rbs_collection.git
127
+ repo_dir: gems
128
+ - name: parallel
129
+ version: '1.20'
130
+ source:
131
+ type: git
132
+ name: ruby/gem_rbs_collection
133
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
134
+ remote: https://github.com/ruby/gem_rbs_collection.git
135
+ repo_dir: gems
136
+ - name: pp
137
+ version: '0'
138
+ source:
139
+ type: stdlib
140
+ - name: prettyprint
141
+ version: '0'
142
+ source:
143
+ type: stdlib
144
+ - name: pstore
145
+ version: '0'
146
+ source:
147
+ type: stdlib
148
+ - name: psych
149
+ version: '0'
150
+ source:
151
+ type: stdlib
152
+ - name: rake
153
+ version: '13.0'
154
+ source:
155
+ type: git
156
+ name: ruby/gem_rbs_collection
157
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
158
+ remote: https://github.com/ruby/gem_rbs_collection.git
159
+ repo_dir: gems
160
+ - name: rdoc
161
+ version: '0'
162
+ source:
163
+ type: stdlib
164
+ - name: stringio
165
+ version: '0'
166
+ source:
167
+ type: stdlib
168
+ - name: thor
169
+ version: '1.2'
170
+ source:
171
+ type: git
172
+ name: ruby/gem_rbs_collection
173
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
174
+ remote: https://github.com/ruby/gem_rbs_collection.git
175
+ repo_dir: gems
176
+ - name: timeout
177
+ version: '0'
178
+ source:
179
+ type: stdlib
180
+ - name: tsort
181
+ version: '0'
182
+ source:
183
+ type: stdlib
184
+ - name: uri
185
+ version: '0'
186
+ source:
187
+ type: stdlib
188
+ - name: webmock
189
+ version: '3.19'
190
+ source:
191
+ type: git
192
+ name: ruby/gem_rbs_collection
193
+ revision: 10d80b1f45dd4241d64d40b49e622cd5d3cfa903
194
+ remote: https://github.com/ruby/gem_rbs_collection.git
195
+ repo_dir: gems
196
+ gemfile_lock_path: Gemfile.lock
@@ -0,0 +1,23 @@
1
+ # Download sources
2
+ sources:
3
+ - type: git
4
+ name: ruby/gem_rbs_collection
5
+ remote: https://github.com/ruby/gem_rbs_collection.git
6
+ revision: main
7
+ repo_dir: gems
8
+
9
+ # You can specify local directories as sources also.
10
+ # - type: local
11
+ # path: path/to/your/local/repository
12
+
13
+ # A directory to install the downloaded RBSs
14
+ path: .gem_rbs_collection
15
+
16
+ gems:
17
+ - name: logger
18
+ - name: rbs
19
+ ignore: true
20
+ - name: steep
21
+ ignore: true
22
+ - name: yard
23
+ ignore: true
@@ -0,0 +1,28 @@
1
+ module SashimiTanpopo
2
+ class CLI < Thor
3
+ def self.exit_on_failure?: () -> bool
4
+
5
+ def self.define_exec_common_options: () -> void
6
+
7
+ def self.normalize_params: (Hash[String, String] params) -> Hash[Symbol, String]
8
+
9
+ def version: () -> void
10
+
11
+ def local: (*String recipe_files) -> void
12
+
13
+ def github: (*String recipe_files) -> void
14
+
15
+ def gitlab: (*String recipe_files) -> void
16
+
17
+ def option_or_env: (
18
+ option_name: String | Symbol,
19
+ env_name: String | Array[String],
20
+ ?default: String?,
21
+ ) -> String?
22
+
23
+ def option_or_env!: (
24
+ option_name: String | Symbol,
25
+ env_name: String | Array[String],
26
+ ) -> String
27
+ end
28
+ end
@@ -0,0 +1,68 @@
1
+ module SashimiTanpopo
2
+ class DSL
3
+ def perform: (
4
+ recipe_path: String,
5
+ target_dir: String,
6
+ params: Hash[Symbol, String],
7
+ dry_run: bool,
8
+ is_colored: bool,
9
+ is_update_local: bool,
10
+ ) -> changed_files
11
+
12
+ def evaluate: (
13
+ recipe_body: String,
14
+ recipe_path: String,
15
+ target_dir: String,
16
+ params: Hash[Symbol, String],
17
+ dry_run: bool,
18
+ is_colored: bool,
19
+ is_update_local: bool,
20
+ ) -> changed_files
21
+
22
+ class EvalContext
23
+ @__params__: Hash[Symbol, String]
24
+ @__dry_run__: bool
25
+ @__target_dir__: String
26
+ @__is_update_local__: bool
27
+ @__diffy_format__: Symbol
28
+ @__changed_files__: changed_files
29
+
30
+ def initialize: (
31
+ params: Hash[Symbol, String],
32
+ dry_run: bool,
33
+ is_colored: bool,
34
+ target_dir: String,
35
+ is_update_local: bool,
36
+ ) -> void
37
+
38
+ def params: () -> Hash[Symbol, String]
39
+
40
+ def changed_files: () -> changed_files
41
+
42
+ def dry_run?: () -> bool
43
+
44
+ def update_file: (String pattern) { (String) -> void } -> void
45
+
46
+ private
47
+
48
+ def update_single_file: (String path) { (String) -> void } -> String?
49
+
50
+ def show_diff: (String str1, String str2) -> void
51
+ end
52
+
53
+ class InstanceEval
54
+ @code: String
55
+ @target_dir: String
56
+ @context: EvalContext
57
+
58
+ def initialize: (
59
+ recipe_body: String,
60
+ recipe_path: String,
61
+ target_dir: String,
62
+ context: EvalContext
63
+ ) -> void
64
+
65
+ def call: () -> void
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,11 @@
1
+ module SashimiTanpopo
2
+ module Logger
3
+ class Formatter
4
+ def call: (String, Time, String?, String) -> String
5
+ end
6
+ end
7
+
8
+ def self.logger: () -> ::Logger
9
+
10
+ def self.logger=: (::Logger) -> ::Logger
11
+ end
@@ -0,0 +1,25 @@
1
+ module SashimiTanpopo
2
+ module Provider
3
+ class Base
4
+ @recipe_paths: Array[String]
5
+ @target_dir: String
6
+ @params: Hash[Symbol, String]
7
+ @dry_run: bool
8
+ @is_colored: bool
9
+ @is_update_local: bool
10
+
11
+ def initialize: (
12
+ recipe_paths: Array[String],
13
+ target_dir: String?,
14
+ params: Hash[Symbol, String],
15
+ dry_run: bool,
16
+ is_colored: bool,
17
+ is_update_local: bool,
18
+ ) -> void
19
+
20
+ def apply_recipe_files: () -> changed_files
21
+
22
+ def cleanup_target_dir: () -> void
23
+ end
24
+ end
25
+ end