mendix-ruby-bridge 0.1.1

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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.mxcli-version +2 -0
  3. data/LICENSE +21 -0
  4. data/README.md +77 -0
  5. data/bin/git-mendix +4 -0
  6. data/bin/mendix-apply +145 -0
  7. data/bin/mendix-desktop +26 -0
  8. data/bin/mendix-git +121 -0
  9. data/bin/mendix-ruby +580 -0
  10. data/bin/mxcli +20 -0
  11. data/bin/setup-tools +25 -0
  12. data/lib/mendix_bridge/backend_server.rb +1404 -0
  13. data/lib/mendix_bridge/change_planner.rb +594 -0
  14. data/lib/mendix_bridge/config.rb +89 -0
  15. data/lib/mendix_bridge/dependency_index.rb +188 -0
  16. data/lib/mendix_bridge/desktop_app.rb +121 -0
  17. data/lib/mendix_bridge/document_parser.rb +120 -0
  18. data/lib/mendix_bridge/domain_parser.rb +103 -0
  19. data/lib/mendix_bridge/dsl.rb +540 -0
  20. data/lib/mendix_bridge/enumeration_parser.rb +32 -0
  21. data/lib/mendix_bridge/git_workflow.rb +321 -0
  22. data/lib/mendix_bridge/html_viewer.rb +672 -0
  23. data/lib/mendix_bridge/importer.rb +415 -0
  24. data/lib/mendix_bridge/inventory.rb +93 -0
  25. data/lib/mendix_bridge/mdl_generator.rb +343 -0
  26. data/lib/mendix_bridge/microflow_parser.rb +131 -0
  27. data/lib/mendix_bridge/migration.rb +290 -0
  28. data/lib/mendix_bridge/migration_executor.rb +234 -0
  29. data/lib/mendix_bridge/model.rb +204 -0
  30. data/lib/mendix_bridge/page_parser.rb +95 -0
  31. data/lib/mendix_bridge/presenter.rb +35 -0
  32. data/lib/mendix_bridge/project_creator.rb +181 -0
  33. data/lib/mendix_bridge/ruby_inventory_generator.rb +63 -0
  34. data/lib/mendix_bridge/security_parser.rb +72 -0
  35. data/lib/mendix_bridge/snapshot_diff.rb +58 -0
  36. data/lib/mendix_bridge/validator.rb +46 -0
  37. data/lib/mendix_bridge/version.rb +5 -0
  38. data/lib/mendix_bridge/visual_entity_plan.rb +176 -0
  39. data/lib/mendix_bridge.rb +127 -0
  40. data/share/applications/mendix-ruby-bridge.desktop +12 -0
  41. data/share/icons/hicolor/128x128/apps/mendix-ruby-bridge.png +0 -0
  42. data/share/icons/hicolor/256x256/apps/mendix-ruby-bridge.png +0 -0
  43. data/share/icons/hicolor/32x32/apps/mendix-ruby-bridge.png +0 -0
  44. data/share/icons/hicolor/48x48/apps/mendix-ruby-bridge.png +0 -0
  45. data/share/icons/hicolor/512x512/apps/mendix-ruby-bridge.png +0 -0
  46. data/share/icons/hicolor/64x64/apps/mendix-ruby-bridge.png +0 -0
  47. data/web/dist/apple-touch-icon.png +0 -0
  48. data/web/dist/assets/index-BO6iPT1P.css +1 -0
  49. data/web/dist/assets/index-JGODw81W.js +27 -0
  50. data/web/dist/brand/mendix-ruby-bridge.png +0 -0
  51. data/web/dist/favicon.png +0 -0
  52. data/web/dist/icons.svg +24 -0
  53. data/web/dist/index.html +17 -0
  54. metadata +179 -0
@@ -0,0 +1,321 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "pathname"
5
+ require "shellwords"
6
+
7
+ module MendixBridge
8
+ class GitWorkflowError < StandardError; end
9
+
10
+ class GitWorkflow
11
+ IN_PROGRESS_MARKERS = %w[
12
+ MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD REBASE_HEAD
13
+ rebase-merge rebase-apply BISECT_LOG
14
+ ].freeze
15
+
16
+ def initialize(project_file, inventory_dir: nil, mxcli: nil, mx: nil)
17
+ @project_file = File.expand_path(project_file)
18
+ @project_dir = File.dirname(@project_file)
19
+ @inventory_dir = inventory_dir && File.expand_path(inventory_dir)
20
+ @root = git!("rev-parse", "--show-toplevel").strip
21
+ @git_dir = git!("rev-parse", "--absolute-git-dir").strip
22
+ @mxcli = mxcli
23
+ @mx = mx || resolve_mx
24
+ verify_project_tracking!
25
+ end
26
+
27
+ attr_reader :root
28
+
29
+ def status
30
+ {
31
+ "branch" => current_branch,
32
+ "clean" => clean?,
33
+ "operation_in_progress" => operation_in_progress,
34
+ "project" => @project_file,
35
+ "project_tracked" => tracked?(@project_file),
36
+ "mprcontents_tracked" => mprcontents_tracked?,
37
+ "ready_to_switch" => clean? && operation_in_progress.nil?
38
+ }
39
+ end
40
+
41
+ def branches
42
+ git!("for-each-ref", "--format=%(refname:short)", "refs/heads", "refs/remotes")
43
+ .lines.map(&:strip).reject { |name| name.end_with?("/HEAD") }
44
+ end
45
+
46
+ def fetch
47
+ git!("fetch", "--prune", "origin")
48
+ end
49
+
50
+ def switch(branch, studio_closed:)
51
+ ensure_switch_ready!(branch, studio_closed:)
52
+ previous = current_branch
53
+ switched = false
54
+
55
+ if local_branch?(branch)
56
+ git!("switch", branch)
57
+ elsif remote_branch?(branch)
58
+ git!("switch", "--track", "-c", branch, "origin/#{branch}")
59
+ else
60
+ raise GitWorkflowError, "branch does not exist locally or at origin: #{branch}"
61
+ end
62
+ switched = true
63
+ validate_and_refresh!
64
+ status
65
+ rescue StandardError => error
66
+ rollback(previous) if switched
67
+ raise error
68
+ end
69
+
70
+ def create(branch, studio_closed:)
71
+ ensure_switch_ready!(branch, studio_closed:)
72
+ raise GitWorkflowError, "branch already exists: #{branch}" if local_branch?(branch) || remote_branch?(branch)
73
+
74
+ previous = current_branch
75
+ switched = false
76
+ git!("switch", "-c", branch)
77
+ switched = true
78
+ validate_and_refresh!
79
+ status
80
+ rescue StandardError => error
81
+ rollback(previous) if switched
82
+ raise error
83
+ end
84
+
85
+ def stash_push(studio_closed:, message: nil, include_untracked: false)
86
+ ensure_studio_closed!(studio_closed)
87
+ ensure_no_operation!
88
+
89
+ arguments = ["stash", "push"]
90
+ arguments << "--include-untracked" if include_untracked
91
+ arguments.concat(["-m", message]) if message
92
+ output = git!(*arguments)
93
+ { "output" => output.strip, **status }
94
+ end
95
+
96
+ def stash_list
97
+ git!("stash", "list")
98
+ end
99
+
100
+ def stash_show(reference = "stash@{0}")
101
+ git!("stash", "show", "--stat", reference)
102
+ end
103
+
104
+ def stash_apply(reference = "stash@{0}", studio_closed:, drop: false)
105
+ ensure_switch_ready!(current_branch, studio_closed:)
106
+ output, error, result = Open3.capture3(
107
+ "git", "-C", @root, "stash", "apply", "--index", reference
108
+ )
109
+ unless result.success?
110
+ raise GitWorkflowError,
111
+ "stash apply has conflicts; the stash was preserved:\n#{error}#{output}"
112
+ end
113
+
114
+ begin
115
+ validate_project!
116
+ refresh_inventory! if @inventory_dir
117
+ rescue StandardError => validation_error
118
+ raise GitWorkflowError,
119
+ "#{validation_error.message}\nthe stash was preserved and its changes remain in the working tree"
120
+ end
121
+
122
+ git!("stash", "drop", reference) if drop
123
+ status
124
+ end
125
+
126
+ def stash_drop(reference = "stash@{0}")
127
+ git!("stash", "drop", reference).strip
128
+ end
129
+
130
+ def merge(branch, studio_closed:)
131
+ ensure_switch_ready!(branch, studio_closed:)
132
+ output, error, result = Open3.capture3(
133
+ "git", "-C", @root, "merge", "--no-ff", "--no-commit", branch
134
+ )
135
+ unless result.success?
136
+ raise GitWorkflowError,
137
+ "merge has conflicts; resolve them or run git merge --abort:\n#{error}#{output}"
138
+ end
139
+
140
+ begin
141
+ validate_project!
142
+ rescue StandardError => validation_error
143
+ raise GitWorkflowError,
144
+ "#{validation_error.message}\nmerge was not committed; run git merge --abort"
145
+ end
146
+
147
+ git!("commit", "--no-edit")
148
+ refresh_inventory! if @inventory_dir
149
+ status
150
+ end
151
+
152
+ def rebase(branch, studio_closed:)
153
+ ensure_switch_ready!(branch, studio_closed:)
154
+ validation = [
155
+ Shellwords.escape(@mx),
156
+ "check",
157
+ Shellwords.escape(@project_file)
158
+ ].join(" ")
159
+ output, error, result = Open3.capture3(
160
+ "git", "-C", @root, "rebase", "--exec", validation, branch
161
+ )
162
+ unless result.success?
163
+ raise GitWorkflowError,
164
+ "rebase stopped; resolve the issue and continue, or run git rebase --abort:\n#{error}#{output}"
165
+ end
166
+
167
+ refresh_inventory! if @inventory_dir
168
+ status
169
+ end
170
+
171
+ # Stages the Mendix project directory and commits it. Deliberately skips the
172
+ # `mx check` consistency validation so work-in-progress (even inconsistent)
173
+ # state can be committed; branch switches still guard against a dirty tree.
174
+ def commit(message, studio_closed:)
175
+ ensure_studio_closed!(studio_closed)
176
+ ensure_no_operation!
177
+ message = message.to_s.strip
178
+ raise GitWorkflowError, "commit message cannot be empty" if message.empty?
179
+ verify_project_tracking!
180
+
181
+ git!("add", "--", project_pathspec)
182
+ raise GitWorkflowError, "nothing to commit; the project has no staged changes" if
183
+ git!("diff", "--cached", "--name-only").strip.empty?
184
+
185
+ git!("commit", "-m", message)
186
+ status
187
+ end
188
+
189
+ private
190
+
191
+ def project_pathspec
192
+ relative = Pathname.new(@project_dir).relative_path_from(Pathname.new(@root)).to_s
193
+ relative == "." ? "." : relative
194
+ end
195
+
196
+ def ensure_switch_ready!(branch, studio_closed:)
197
+ ensure_studio_closed!(studio_closed)
198
+ raise GitWorkflowError, "working tree is dirty; commit or stash changes first" unless clean?
199
+ ensure_no_operation!
200
+
201
+ _output, error, status = Open3.capture3("git", "check-ref-format", "--branch", branch)
202
+ raise GitWorkflowError, "invalid branch name: #{branch} (#{error.strip})" unless status.success?
203
+ end
204
+
205
+ def validate_and_refresh!
206
+ validate_project!
207
+ refresh_inventory! if @inventory_dir
208
+ end
209
+
210
+ def validate_project!
211
+ raise GitWorkflowError, "project file is absent on branch #{current_branch}" unless File.file?(@project_file)
212
+ verify_project_tracking!
213
+
214
+ output, error, status = Open3.capture3(@mx, "check", @project_file, chdir: @project_dir)
215
+ unless status.success?
216
+ raise GitWorkflowError, "Mendix consistency check failed:\n#{error}#{output}"
217
+ end
218
+ end
219
+
220
+ def refresh_inventory!
221
+ raise GitWorkflowError, "mxcli is required to refresh the inventory" unless @mxcli
222
+
223
+ metadata_file = File.join(@inventory_dir, "mendix-project.json")
224
+ raise GitWorkflowError, "not an imported inventory: #{@inventory_dir}" unless File.file?(metadata_file)
225
+
226
+ metadata = JSON.parse(File.read(metadata_file))
227
+ unless File.realpath(metadata.fetch("source_project")) == File.realpath(@project_file)
228
+ raise GitWorkflowError, "inventory belongs to a different Mendix project"
229
+ end
230
+
231
+ _inventory, warnings = Importer.new(mxcli: @mxcli).import(@project_file, @inventory_dir)
232
+ return if warnings.empty?
233
+
234
+ raise GitWorkflowError, "inventory refresh had #{warnings.length} description errors"
235
+ end
236
+
237
+ def rollback(previous)
238
+ git!("switch", previous) if clean? && current_branch != previous
239
+ rescue GitWorkflowError
240
+ nil
241
+ end
242
+
243
+ def ensure_studio_closed!(studio_closed)
244
+ raise GitWorkflowError, "confirm Studio Pro is closed with --studio-closed" unless studio_closed
245
+ end
246
+
247
+ def ensure_no_operation!
248
+ return unless operation_in_progress
249
+
250
+ raise GitWorkflowError, "Git operation in progress: #{operation_in_progress}"
251
+ end
252
+
253
+ def resolve_mx
254
+ version_file = File.join(@project_dir, ".mendix-version")
255
+ raise GitWorkflowError, "missing #{version_file}" unless File.file?(version_file)
256
+
257
+ version = File.read(version_file).strip
258
+ executable = File.join(Dir.home, ".mxcli", "mxbuild", version, "modeler", "mx")
259
+ raise GitWorkflowError, "Mendix mx #{version} is not installed: #{executable}" unless File.executable?(executable)
260
+
261
+ executable
262
+ end
263
+
264
+ def verify_project_tracking!
265
+ raise GitWorkflowError, "Mendix project does not exist: #{@project_file}" unless File.file?(@project_file)
266
+ raise GitWorkflowError, "project is outside its Git repository" unless inside_root?(@project_file)
267
+ raise GitWorkflowError, "the .mpr is not tracked by Git" unless tracked?(@project_file)
268
+ raise GitWorkflowError, "mprcontents has no tracked files" unless mprcontents_tracked?
269
+ end
270
+
271
+ def inside_root?(path)
272
+ path == @root || path.start_with?("#{@root}/")
273
+ end
274
+
275
+ def tracked?(path)
276
+ relative = path.delete_prefix("#{@root}/")
277
+ _output, _error, status = Open3.capture3("git", "-C", @root, "ls-files", "--error-unmatch", relative)
278
+ status.success?
279
+ end
280
+
281
+ def mprcontents_tracked?
282
+ return true unless Dir.exist?(File.join(@project_dir, "mprcontents"))
283
+
284
+ relative_dir = Pathname.new(@project_dir).relative_path_from(Pathname.new(@root)).to_s
285
+ pathspec = relative_dir == "." ? "mprcontents" : File.join(relative_dir, "mprcontents")
286
+ !git!("ls-files", pathspec).strip.empty?
287
+ end
288
+
289
+ def clean?
290
+ git!("status", "--porcelain").empty?
291
+ end
292
+
293
+ def current_branch
294
+ git!("branch", "--show-current").strip
295
+ end
296
+
297
+ def operation_in_progress
298
+ IN_PROGRESS_MARKERS.find { |marker| File.exist?(File.join(@git_dir, marker)) }
299
+ end
300
+
301
+ def local_branch?(branch)
302
+ ref_exists?("refs/heads/#{branch}")
303
+ end
304
+
305
+ def remote_branch?(branch)
306
+ ref_exists?("refs/remotes/origin/#{branch}")
307
+ end
308
+
309
+ def ref_exists?(ref)
310
+ _output, _error, status = Open3.capture3("git", "-C", @root, "show-ref", "--verify", "--quiet", ref)
311
+ status.success?
312
+ end
313
+
314
+ def git!(*arguments)
315
+ output, error, status = Open3.capture3("git", "-C", @root || @project_dir, *arguments)
316
+ return output if status.success?
317
+
318
+ raise GitWorkflowError, "git #{arguments.join(' ')} failed: #{error.strip}"
319
+ end
320
+ end
321
+ end