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.
- checksums.yaml +7 -0
- data/.mxcli-version +2 -0
- data/LICENSE +21 -0
- data/README.md +77 -0
- data/bin/git-mendix +4 -0
- data/bin/mendix-apply +145 -0
- data/bin/mendix-desktop +26 -0
- data/bin/mendix-git +121 -0
- data/bin/mendix-ruby +580 -0
- data/bin/mxcli +20 -0
- data/bin/setup-tools +25 -0
- data/lib/mendix_bridge/backend_server.rb +1404 -0
- data/lib/mendix_bridge/change_planner.rb +594 -0
- data/lib/mendix_bridge/config.rb +89 -0
- data/lib/mendix_bridge/dependency_index.rb +188 -0
- data/lib/mendix_bridge/desktop_app.rb +121 -0
- data/lib/mendix_bridge/document_parser.rb +120 -0
- data/lib/mendix_bridge/domain_parser.rb +103 -0
- data/lib/mendix_bridge/dsl.rb +540 -0
- data/lib/mendix_bridge/enumeration_parser.rb +32 -0
- data/lib/mendix_bridge/git_workflow.rb +321 -0
- data/lib/mendix_bridge/html_viewer.rb +672 -0
- data/lib/mendix_bridge/importer.rb +415 -0
- data/lib/mendix_bridge/inventory.rb +93 -0
- data/lib/mendix_bridge/mdl_generator.rb +343 -0
- data/lib/mendix_bridge/microflow_parser.rb +131 -0
- data/lib/mendix_bridge/migration.rb +290 -0
- data/lib/mendix_bridge/migration_executor.rb +234 -0
- data/lib/mendix_bridge/model.rb +204 -0
- data/lib/mendix_bridge/page_parser.rb +95 -0
- data/lib/mendix_bridge/presenter.rb +35 -0
- data/lib/mendix_bridge/project_creator.rb +181 -0
- data/lib/mendix_bridge/ruby_inventory_generator.rb +63 -0
- data/lib/mendix_bridge/security_parser.rb +72 -0
- data/lib/mendix_bridge/snapshot_diff.rb +58 -0
- data/lib/mendix_bridge/validator.rb +46 -0
- data/lib/mendix_bridge/version.rb +5 -0
- data/lib/mendix_bridge/visual_entity_plan.rb +176 -0
- data/lib/mendix_bridge.rb +127 -0
- data/share/applications/mendix-ruby-bridge.desktop +12 -0
- data/share/icons/hicolor/128x128/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/256x256/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/32x32/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/48x48/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/512x512/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/64x64/apps/mendix-ruby-bridge.png +0 -0
- data/web/dist/apple-touch-icon.png +0 -0
- data/web/dist/assets/index-BO6iPT1P.css +1 -0
- data/web/dist/assets/index-JGODw81W.js +27 -0
- data/web/dist/brand/mendix-ruby-bridge.png +0 -0
- data/web/dist/favicon.png +0 -0
- data/web/dist/icons.svg +24 -0
- data/web/dist/index.html +17 -0
- 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
|