makit 0.0.112 → 0.0.128
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 +4 -4
- data/README.md +41 -41
- data/exe/makit +5 -5
- data/lib/makit/apache.rb +28 -28
- data/lib/makit/cli/build_commands.rb +500 -500
- data/lib/makit/cli/generators/base_generator.rb +74 -74
- data/lib/makit/cli/generators/dotnet_generator.rb +50 -50
- data/lib/makit/cli/generators/generator_factory.rb +49 -49
- data/lib/makit/cli/generators/node_generator.rb +50 -50
- data/lib/makit/cli/generators/ruby_generator.rb +77 -77
- data/lib/makit/cli/generators/rust_generator.rb +50 -50
- data/lib/makit/cli/generators/templates/dotnet_templates.rb +167 -167
- data/lib/makit/cli/generators/templates/node_templates.rb +161 -161
- data/lib/makit/cli/generators/templates/ruby/gemfile.rb +26 -26
- data/lib/makit/cli/generators/templates/ruby/gemspec.rb +40 -40
- data/lib/makit/cli/generators/templates/ruby/main_lib.rb +33 -33
- data/lib/makit/cli/generators/templates/ruby/rakefile.rb +35 -35
- data/lib/makit/cli/generators/templates/ruby/readme.rb +63 -63
- data/lib/makit/cli/generators/templates/ruby/test.rb +39 -39
- data/lib/makit/cli/generators/templates/ruby/test_helper.rb +29 -29
- data/lib/makit/cli/generators/templates/ruby/version.rb +29 -29
- data/lib/makit/cli/generators/templates/rust_templates.rb +128 -128
- data/lib/makit/cli/main.rb +62 -62
- data/lib/makit/cli/project_commands.rb +868 -868
- data/lib/makit/cli/repository_commands.rb +661 -661
- data/lib/makit/cli/utility_commands.rb +521 -521
- data/lib/makit/commands/factory.rb +359 -359
- data/lib/makit/commands/middleware/base.rb +73 -73
- data/lib/makit/commands/middleware/cache.rb +248 -248
- data/lib/makit/commands/middleware/command_logger.rb +311 -320
- data/lib/makit/commands/middleware/validator.rb +269 -269
- data/lib/makit/commands/request.rb +316 -254
- data/lib/makit/commands/result.rb +323 -323
- data/lib/makit/commands/runner.rb +368 -337
- data/lib/makit/commands/strategies/base.rb +171 -160
- data/lib/makit/commands/strategies/synchronous.rb +139 -134
- data/lib/makit/commands.rb +50 -51
- data/lib/makit/configuration/gitlab_helper.rb +58 -60
- data/lib/makit/configuration/project.rb +167 -127
- data/lib/makit/configuration/rakefile_helper.rb +43 -43
- data/lib/makit/configuration/step.rb +34 -34
- data/lib/makit/configuration.rb +14 -14
- data/lib/makit/content/default_gitignore.rb +7 -7
- data/lib/makit/content/default_gitignore.txt +226 -0
- data/lib/makit/content/default_rakefile.rb +13 -13
- data/lib/makit/content/gem_rakefile.rb +16 -16
- data/lib/makit/context.rb +1 -1
- data/lib/makit/data.rb +49 -49
- data/lib/makit/directories.rb +140 -141
- data/lib/makit/directory.rb +262 -262
- data/lib/makit/docs/files.rb +89 -89
- data/lib/makit/docs/rake.rb +102 -102
- data/lib/makit/dotnet/cli.rb +69 -65
- data/lib/makit/dotnet/project.rb +217 -153
- data/lib/makit/dotnet/solution.rb +38 -38
- data/lib/makit/dotnet/solution_classlib.rb +239 -239
- data/lib/makit/dotnet/solution_console.rb +264 -264
- data/lib/makit/dotnet/solution_maui.rb +354 -354
- data/lib/makit/dotnet/solution_wasm.rb +275 -275
- data/lib/makit/dotnet/solution_wpf.rb +304 -304
- data/lib/makit/dotnet.rb +102 -102
- data/lib/makit/email.rb +90 -90
- data/lib/makit/environment.rb +142 -142
- data/lib/makit/examples/runner.rb +370 -370
- data/lib/makit/exceptions.rb +45 -45
- data/lib/makit/fileinfo.rb +24 -24
- data/lib/makit/files.rb +43 -43
- data/lib/makit/gems.rb +40 -40
- data/lib/makit/git/cli.rb +54 -54
- data/lib/makit/git/repository.rb +90 -90
- data/lib/makit/git.rb +98 -98
- data/lib/makit/gitlab_runner.rb +59 -59
- data/lib/makit/humanize.rb +137 -137
- data/lib/makit/indexer.rb +47 -47
- data/lib/makit/logging/configuration.rb +308 -305
- data/lib/makit/logging/format_registry.rb +84 -84
- data/lib/makit/logging/formatters/base.rb +39 -39
- data/lib/makit/logging/formatters/console_formatter.rb +140 -140
- data/lib/makit/logging/formatters/json_formatter.rb +65 -65
- data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -71
- data/lib/makit/logging/formatters/text_formatter.rb +64 -64
- data/lib/makit/logging/log_request.rb +119 -115
- data/lib/makit/logging/logger.rb +199 -163
- data/lib/makit/logging/sinks/base.rb +91 -91
- data/lib/makit/logging/sinks/console.rb +72 -72
- data/lib/makit/logging/sinks/file_sink.rb +92 -92
- data/lib/makit/logging/sinks/structured.rb +123 -129
- data/lib/makit/logging/sinks/unified_file_sink.rb +296 -303
- data/lib/makit/logging.rb +565 -530
- data/lib/makit/markdown.rb +75 -75
- data/lib/makit/mp/basic_object_mp.rb +17 -17
- data/lib/makit/mp/command_mp.rb +13 -13
- data/lib/makit/mp/command_request.mp.rb +17 -17
- data/lib/makit/mp/project_mp.rb +199 -199
- data/lib/makit/mp/string_mp.rb +191 -193
- data/lib/makit/nuget.rb +74 -74
- data/lib/makit/port.rb +32 -32
- data/lib/makit/process.rb +163 -163
- data/lib/makit/protoc.rb +107 -107
- data/lib/makit/rake/cli.rb +196 -196
- data/lib/makit/rake.rb +25 -25
- data/lib/makit/ruby/cli.rb +185 -185
- data/lib/makit/ruby.rb +25 -25
- data/lib/makit/secrets.rb +51 -51
- data/lib/makit/serializer.rb +130 -130
- data/lib/makit/services/builder.rb +186 -186
- data/lib/makit/services/error_handler.rb +226 -226
- data/lib/makit/services/repository_manager.rb +231 -229
- data/lib/makit/services/validator.rb +112 -112
- data/lib/makit/setup/classlib.rb +94 -53
- data/lib/makit/setup/gem.rb +268 -263
- data/lib/makit/setup/razorclasslib.rb +91 -0
- data/lib/makit/setup/runner.rb +54 -45
- data/lib/makit/setup.rb +5 -5
- data/lib/makit/show.rb +110 -110
- data/lib/makit/storage.rb +126 -126
- data/lib/makit/symbols.rb +170 -170
- data/lib/makit/task_info.rb +128 -128
- data/lib/makit/tasks/at_exit.rb +15 -13
- data/lib/makit/tasks/build.rb +22 -19
- data/lib/makit/tasks/clean.rb +13 -11
- data/lib/makit/tasks/configure.rb +10 -0
- data/lib/makit/tasks/format.rb +10 -0
- data/lib/makit/tasks/hook_manager.rb +391 -393
- data/lib/makit/tasks/init.rb +49 -47
- data/lib/makit/tasks/integrate.rb +29 -17
- data/lib/makit/tasks/pull_incoming.rb +13 -11
- data/lib/makit/tasks/setup.rb +13 -6
- data/lib/makit/tasks/sync.rb +17 -12
- data/lib/makit/tasks/tag.rb +16 -15
- data/lib/makit/tasks/task_monkey_patch.rb +81 -79
- data/lib/makit/tasks/test.rb +22 -0
- data/lib/makit/tasks/update.rb +18 -0
- data/lib/makit/tasks.rb +20 -15
- data/lib/makit/test_cache.rb +239 -239
- data/lib/makit/tree.rb +37 -37
- data/lib/makit/v1/makit.v1_pb.rb +35 -34
- data/lib/makit/v1/makit.v1_services_pb.rb +27 -27
- data/lib/makit/version.rb +5 -5
- data/lib/makit/version_util.rb +21 -21
- data/lib/makit/wix.rb +95 -95
- data/lib/makit/yaml.rb +29 -29
- data/lib/makit/zip.rb +17 -17
- data/lib/makit copy.rb +44 -44
- data/lib/makit.rb +39 -40
- metadata +69 -7
- data/lib/makit/commands/middleware/unified_logger.rb +0 -243
@@ -1,661 +1,661 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "clamp"
|
4
|
-
require "json"
|
5
|
-
require "uri"
|
6
|
-
require "time"
|
7
|
-
require "find"
|
8
|
-
|
9
|
-
module Makit
|
10
|
-
module Cli
|
11
|
-
# Consolidated repository management commands
|
12
|
-
# Combines: add, clone, pull, list, remove, import functionality
|
13
|
-
class RepositoryCommand < Clamp::Command
|
14
|
-
self.description = <<~DESC
|
15
|
-
Manage git repositories tracked by Makit.
|
16
|
-
|
17
|
-
Available operations:
|
18
|
-
add - Add repository URL to tracking list
|
19
|
-
clone - Clone repository to local directory#{" "}
|
20
|
-
pull - Pull latest changes from repository
|
21
|
-
list - List tracked repositories
|
22
|
-
remove - Remove tracked repositories by pattern
|
23
|
-
import - Import repositories from filesystem
|
24
|
-
DESC
|
25
|
-
end
|
26
|
-
|
27
|
-
# Add repository to tracking list
|
28
|
-
class AddRepositoryCommand < Clamp::Command
|
29
|
-
self.description = <<~DESC
|
30
|
-
Add git repository URLs to the tracked list.
|
31
|
-
|
32
|
-
Examples:
|
33
|
-
makit repository add https://github.com/user/repo.git
|
34
|
-
makit repository add git@gitlab.com:group/project.git
|
35
|
-
DESC
|
36
|
-
|
37
|
-
parameter "GIT_URL", "The git repository URL to track", attribute_name: :git_url, required: true
|
38
|
-
|
39
|
-
def execute
|
40
|
-
validate_git_url(git_url)
|
41
|
-
|
42
|
-
if add_to_tracked_list(git_url)
|
43
|
-
puts "✅ Successfully added repository to tracked list: #{git_url}"
|
44
|
-
else
|
45
|
-
puts "ℹ️ Repository already tracked: #{git_url}"
|
46
|
-
end
|
47
|
-
rescue ArgumentError => e
|
48
|
-
warn "❌ Invalid repository URL: #{e.message}"
|
49
|
-
exit 1
|
50
|
-
rescue StandardError => e
|
51
|
-
warn "❌ Failed to add repository: #{e.message}"
|
52
|
-
exit 1
|
53
|
-
end
|
54
|
-
|
55
|
-
private
|
56
|
-
|
57
|
-
def validate_git_url(url)
|
58
|
-
raise ArgumentError, "URL cannot be empty" if url.nil? || url.strip.empty?
|
59
|
-
|
60
|
-
url = url.strip
|
61
|
-
valid_patterns = [
|
62
|
-
%r{^https?://\w+[\w.-]*\w+/[\w.-]+/[\w.-]+(?:\.git)?/?$}, # HTTPS
|
63
|
-
%r{^git@[\w.-]+:[\w.-]+/[\w.-]+(?:\.git)?$}, # SSH
|
64
|
-
%r{^ssh://git@[\w.-]+/[\w.-]+/[\w.-]+(?:\.git)?$}, # SSH with ssh://
|
65
|
-
%r{^[\w.-]+@[\w.-]+:[\w.-]+/[\w.-]+(?:\.git)?$},
|
66
|
-
]
|
67
|
-
|
68
|
-
unless valid_patterns.any? { |pattern| url.match?(pattern) }
|
69
|
-
raise ArgumentError, "URL does not match expected git repository format"
|
70
|
-
end
|
71
|
-
|
72
|
-
return unless url.include?("github.com") || url.include?("gitlab.com") || url.include?("bitbucket.org")
|
73
|
-
|
74
|
-
validate_hosted_repository_url(url)
|
75
|
-
end
|
76
|
-
|
77
|
-
def validate_hosted_repository_url(url)
|
78
|
-
if url.start_with?("http")
|
79
|
-
path = URI.parse(url).path.sub(%r{^/}, "").sub(/\.git$/, "").sub(%r{/$}, "")
|
80
|
-
elsif url.include?("@")
|
81
|
-
path = url.split(":").last.sub(/\.git$/, "")
|
82
|
-
end
|
83
|
-
|
84
|
-
parts = path.split("/")
|
85
|
-
raise ArgumentError, "Repository path should be in format 'user/repository'" if parts.length < 2
|
86
|
-
|
87
|
-
parts.each do |part|
|
88
|
-
if part.empty? || part.match?(/[<>:"|?*]/)
|
89
|
-
raise ArgumentError, "Invalid characters in repository path: #{part}"
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def add_to_tracked_list(url)
|
95
|
-
tracked_repos = load_tracked_repositories
|
96
|
-
return false if tracked_repos.any? { |repo| repo["url"] == url }
|
97
|
-
|
98
|
-
new_repo = {
|
99
|
-
"url" => url,
|
100
|
-
"added_at" => Time.now.iso8601,
|
101
|
-
"status" => "tracked",
|
102
|
-
}
|
103
|
-
|
104
|
-
tracked_repos << new_repo
|
105
|
-
save_tracked_repositories(tracked_repos)
|
106
|
-
true
|
107
|
-
end
|
108
|
-
|
109
|
-
def load_tracked_repositories
|
110
|
-
tracked_file = tracked_repositories_file
|
111
|
-
return [] unless File.exist?(tracked_file)
|
112
|
-
|
113
|
-
begin
|
114
|
-
JSON.parse(File.read(tracked_file))
|
115
|
-
rescue JSON::ParserError => e
|
116
|
-
warn "Warning: Could not parse tracked repositories file: #{e.message}"
|
117
|
-
[]
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
def save_tracked_repositories(repositories)
|
122
|
-
tracked_file = tracked_repositories_file
|
123
|
-
FileUtils.mkdir_p(File.dirname(tracked_file))
|
124
|
-
sorted_repos = repositories.sort_by { |repo| repo["url"] }
|
125
|
-
File.write(tracked_file, JSON.pretty_generate(sorted_repos))
|
126
|
-
end
|
127
|
-
|
128
|
-
def tracked_repositories_file
|
129
|
-
File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
# Clone repository to local directory
|
134
|
-
class CloneRepositoryCommand < Clamp::Command
|
135
|
-
self.description = <<~DESC
|
136
|
-
Clone git repositories to local directories.
|
137
|
-
|
138
|
-
Examples:
|
139
|
-
makit repository clone https://github.com/user/repo
|
140
|
-
makit repository clone git@github.com:user/repo.git
|
141
|
-
DESC
|
142
|
-
|
143
|
-
parameter "GIT_REPOSITORY", "The git repository URL", attribute_name: :git_repository, required: true
|
144
|
-
|
145
|
-
def execute
|
146
|
-
clone_dir = Makit::Directories.get_clone_directory(git_repository)
|
147
|
-
|
148
|
-
if Dir.exist?(clone_dir)
|
149
|
-
puts "Repository already exists at: #{clone_dir}"
|
150
|
-
return
|
151
|
-
end
|
152
|
-
|
153
|
-
puts "Cloning repository: #{git_repository} to #{clone_dir}"
|
154
|
-
|
155
|
-
begin
|
156
|
-
commands = Makit.clone(git_repository)
|
157
|
-
|
158
|
-
if commands.any? && commands.last.exit_code != 0
|
159
|
-
handle_clone_error(commands.last, clone_dir)
|
160
|
-
elsif !Dir.exist?(clone_dir)
|
161
|
-
warn "Clone operation completed but directory not found: #{clone_dir}"
|
162
|
-
exit 1
|
163
|
-
else
|
164
|
-
puts "Successfully cloned repository to: #{clone_dir}"
|
165
|
-
end
|
166
|
-
rescue ArgumentError => e
|
167
|
-
warn "Invalid repository URL: #{e.message}"
|
168
|
-
exit 1
|
169
|
-
rescue StandardError => e
|
170
|
-
warn "Failed to clone repository: #{git_repository}"
|
171
|
-
puts e.message
|
172
|
-
exit 1
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
private
|
177
|
-
|
178
|
-
def handle_clone_error(clone_command, clone_dir)
|
179
|
-
summary = "Failed to clone repository: #{git_repository} to #{clone_dir}\n"
|
180
|
-
summary += "Please check the URL and your network connection.\n"
|
181
|
-
summary += "Exit code: #{clone_command.exit_code}\n"
|
182
|
-
summary += "Output: #{clone_command.output}" if clone_command.respond_to?(:output)
|
183
|
-
warn summary
|
184
|
-
exit 1
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
# Pull latest changes from repository
|
189
|
-
class PullRepositoryCommand < Clamp::Command
|
190
|
-
self.description = <<~DESC
|
191
|
-
Pull latest changes from git repositories.
|
192
|
-
|
193
|
-
Examples:
|
194
|
-
makit repository pull https://github.com/user/repo
|
195
|
-
DESC
|
196
|
-
|
197
|
-
parameter "GIT_REPOSITORY", "The git repository URL", attribute_name: :git_repository, required: true
|
198
|
-
|
199
|
-
def execute
|
200
|
-
commands = Makit.pull(git_repository)
|
201
|
-
|
202
|
-
if commands.any? && commands.last.exit_code.zero?
|
203
|
-
puts "Successfully pulled latest changes from: #{git_repository}"
|
204
|
-
else
|
205
|
-
warn "Failed to pull changes from: #{git_repository}"
|
206
|
-
exit 1
|
207
|
-
end
|
208
|
-
rescue ArgumentError => e
|
209
|
-
warn "Invalid repository URL: #{e.message}"
|
210
|
-
exit 1
|
211
|
-
rescue StandardError => e
|
212
|
-
warn "Failed to pull repository: #{git_repository}"
|
213
|
-
puts e.message
|
214
|
-
exit 1
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
# List tracked repositories
|
219
|
-
class ListRepositoryCommand < Clamp::Command
|
220
|
-
self.description = <<~DESC
|
221
|
-
List git repositories tracked by Makit.
|
222
|
-
|
223
|
-
Examples:
|
224
|
-
makit repository list
|
225
|
-
makit repository list --details
|
226
|
-
makit repository list --tracked
|
227
|
-
DESC
|
228
|
-
|
229
|
-
option ["--details"], :flag, "Show detailed information about each repository"
|
230
|
-
option ["--paths"], :flag, "Show local paths for each repository"
|
231
|
-
option ["--tracked"], :flag, "Show tracked repository URLs (not cloned)"
|
232
|
-
option ["--all"], :flag, "Show both cloned and tracked repositories"
|
233
|
-
option ["--format"], "FORMAT", "Output format (table, json, plain)", default: "table"
|
234
|
-
|
235
|
-
def execute
|
236
|
-
repositories = discover_repositories
|
237
|
-
|
238
|
-
if repositories.empty?
|
239
|
-
puts "No git repositories found."
|
240
|
-
puts "Use 'makit repository clone <url>' to clone repositories."
|
241
|
-
return
|
242
|
-
end
|
243
|
-
|
244
|
-
case format
|
245
|
-
when "json"
|
246
|
-
output_json(repositories)
|
247
|
-
when "plain"
|
248
|
-
output_plain(repositories)
|
249
|
-
else
|
250
|
-
output_table(repositories)
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
private
|
255
|
-
|
256
|
-
def discover_repositories
|
257
|
-
repositories = []
|
258
|
-
|
259
|
-
if tracked? && !all?
|
260
|
-
repositories = load_tracked_repositories
|
261
|
-
elsif all?
|
262
|
-
repositories = discover_cloned_repositories
|
263
|
-
tracked_repos = load_tracked_repositories
|
264
|
-
tracked_repos.each do |tracked_repo|
|
265
|
-
unless repositories.any? { |repo| repo[:url] == tracked_repo[:url] }
|
266
|
-
repositories << tracked_repo.merge(type: "tracked")
|
267
|
-
end
|
268
|
-
end
|
269
|
-
else
|
270
|
-
repositories = discover_cloned_repositories
|
271
|
-
end
|
272
|
-
|
273
|
-
repositories.sort_by { |repo| repo[:url] || repo[:name] }
|
274
|
-
end
|
275
|
-
|
276
|
-
def discover_cloned_repositories
|
277
|
-
repositories = []
|
278
|
-
clone_base_dir = Makit::Directories::CLONE
|
279
|
-
return repositories unless Dir.exist?(clone_base_dir)
|
280
|
-
|
281
|
-
Find.find(clone_base_dir) do |path|
|
282
|
-
next unless File.directory?(path) && File.basename(path) == ".git"
|
283
|
-
|
284
|
-
repo_dir = File.dirname(path)
|
285
|
-
repo_info = extract_repository_info(repo_dir)
|
286
|
-
if repo_info
|
287
|
-
repo_info[:type] = "cloned"
|
288
|
-
repositories << repo_info
|
289
|
-
end
|
290
|
-
Find.prune
|
291
|
-
end
|
292
|
-
|
293
|
-
repositories
|
294
|
-
end
|
295
|
-
|
296
|
-
def load_tracked_repositories
|
297
|
-
tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
298
|
-
return [] unless File.exist?(tracked_file)
|
299
|
-
|
300
|
-
begin
|
301
|
-
tracked_repos = JSON.parse(File.read(tracked_file))
|
302
|
-
tracked_repos.map do |repo|
|
303
|
-
{
|
304
|
-
name: extract_repo_name_from_url(repo["url"]),
|
305
|
-
url: repo["url"],
|
306
|
-
added_at: repo["added_at"],
|
307
|
-
status: repo["status"],
|
308
|
-
type: "tracked",
|
309
|
-
}
|
310
|
-
end
|
311
|
-
rescue JSON::ParserError => e
|
312
|
-
warn "Warning: Could not parse tracked repositories file: #{e.message}"
|
313
|
-
[]
|
314
|
-
end
|
315
|
-
end
|
316
|
-
|
317
|
-
def extract_repo_name_from_url(url)
|
318
|
-
if url.include?("/")
|
319
|
-
url.split("/").last.sub(/\.git$/, "")
|
320
|
-
else
|
321
|
-
url
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
def extract_repository_info(repo_dir)
|
326
|
-
return nil unless Dir.exist?(repo_dir)
|
327
|
-
|
328
|
-
info = {
|
329
|
-
name: File.basename(repo_dir),
|
330
|
-
path: repo_dir,
|
331
|
-
relative_path: repo_dir.sub("#{Makit::Directories::CLONE}/", ""),
|
332
|
-
}
|
333
|
-
|
334
|
-
begin
|
335
|
-
Dir.chdir(repo_dir) do
|
336
|
-
remote_output = `git remote get-url origin 2>/dev/null`.strip
|
337
|
-
info[:url] = remote_output unless remote_output.empty?
|
338
|
-
|
339
|
-
if details?
|
340
|
-
info[:branch] = `git branch --show-current 2>/dev/null`.strip
|
341
|
-
info[:last_commit] = `git log -1 --format="%H %s" 2>/dev/null`.strip
|
342
|
-
info[:last_commit_date] = `git log -1 --format="%ci" 2>/dev/null`.strip
|
343
|
-
info[:status] = `git status --porcelain 2>/dev/null`.strip
|
344
|
-
info[:clean] = info[:status].empty?
|
345
|
-
end
|
346
|
-
end
|
347
|
-
rescue StandardError => e
|
348
|
-
info[:error] = "Cannot read git info: #{e.message}"
|
349
|
-
end
|
350
|
-
|
351
|
-
info
|
352
|
-
end
|
353
|
-
|
354
|
-
def output_table(repositories)
|
355
|
-
if details?
|
356
|
-
output_detailed_table(repositories)
|
357
|
-
else
|
358
|
-
output_simple_table(repositories)
|
359
|
-
end
|
360
|
-
end
|
361
|
-
|
362
|
-
def output_simple_table(repositories)
|
363
|
-
puts "Tracked Git Repositories (#{repositories.length}):"
|
364
|
-
puts "=" * 80
|
365
|
-
|
366
|
-
repositories.each_with_index do |repo, index|
|
367
|
-
type_indicator = repo[:type] == "tracked" ? "📋" : "📁"
|
368
|
-
puts "#{(index + 1).to_s.rjust(3)}. #{type_indicator} #{repo[:url] || repo[:name]}"
|
369
|
-
puts " Path: #{repo[:path] || "Not cloned"}" if paths?
|
370
|
-
puts " Type: #{repo[:type]&.capitalize || "Unknown"}" if all? || tracked?
|
371
|
-
puts
|
372
|
-
end
|
373
|
-
end
|
374
|
-
|
375
|
-
def output_detailed_table(repositories)
|
376
|
-
puts "Tracked Git Repositories (#{repositories.length}):"
|
377
|
-
puts "=" * 120
|
378
|
-
|
379
|
-
repositories.each_with_index do |repo, index|
|
380
|
-
type_indicator = repo[:type] == "tracked" ? "📋" : "📁"
|
381
|
-
puts "#{(index + 1).to_s.rjust(3)}. #{type_indicator} #{repo[:url] || repo[:name]}"
|
382
|
-
puts " Type: #{repo[:type]&.capitalize || "Unknown"}"
|
383
|
-
puts " Path: #{repo[:path] || "Not cloned"}" if repo[:type] != "tracked" || all?
|
384
|
-
puts " Branch: #{repo[:branch]}" if repo[:branch] && !repo[:branch].empty?
|
385
|
-
puts " Status: #{repo[:clean] ? "Clean" : "Modified"}" if repo[:type] == "cloned"
|
386
|
-
|
387
|
-
if repo[:last_commit] && !repo[:last_commit].empty?
|
388
|
-
commit_parts = repo[:last_commit].split(" ", 2)
|
389
|
-
commit_hash = commit_parts[0][0..7] if commit_parts[0]
|
390
|
-
commit_msg = commit_parts[1] if commit_parts[1]
|
391
|
-
puts " Commit: #{commit_hash} - #{commit_msg}"
|
392
|
-
end
|
393
|
-
|
394
|
-
puts " Date: #{repo[:last_commit_date]}" if repo[:last_commit_date] && !repo[:last_commit_date].empty?
|
395
|
-
puts " Added: #{repo[:added_at]}" if repo[:added_at]
|
396
|
-
puts " Error: #{repo[:error]}" if repo[:error]
|
397
|
-
puts
|
398
|
-
end
|
399
|
-
end
|
400
|
-
|
401
|
-
def output_json(repositories)
|
402
|
-
puts JSON.pretty_generate({
|
403
|
-
count: repositories.length,
|
404
|
-
repositories: repositories,
|
405
|
-
})
|
406
|
-
end
|
407
|
-
|
408
|
-
def output_plain(repositories)
|
409
|
-
repositories.each do |repo|
|
410
|
-
if paths?
|
411
|
-
puts "#{repo[:url] || repo[:name]} -> #{repo[:path]}"
|
412
|
-
else
|
413
|
-
puts repo[:url] || repo[:name]
|
414
|
-
end
|
415
|
-
end
|
416
|
-
end
|
417
|
-
end
|
418
|
-
|
419
|
-
# Remove tracked repositories by pattern
|
420
|
-
class RemoveRepositoryCommand < Clamp::Command
|
421
|
-
self.description = <<~DESC
|
422
|
-
Remove tracked repositories using pattern matching.
|
423
|
-
|
424
|
-
Examples:
|
425
|
-
makit repository remove github.com/user/
|
426
|
-
makit repository remove clamp.git
|
427
|
-
makit repository remove --missing-rakefile
|
428
|
-
DESC
|
429
|
-
|
430
|
-
parameter "PATTERN", "Pattern to match repository URLs", attribute_name: :pattern, required: false
|
431
|
-
option ["--missing-rakefile"], :flag, "Remove repositories without Rakefile"
|
432
|
-
option ["--dry-run"], :flag, "Show what would be removed without actually removing"
|
433
|
-
option ["--verbose"], :flag, "Show verbose output"
|
434
|
-
|
435
|
-
def execute
|
436
|
-
if missing_rakefile?
|
437
|
-
remove_missing_rakefile_repos
|
438
|
-
elsif pattern
|
439
|
-
remove_by_pattern(pattern)
|
440
|
-
else
|
441
|
-
puts "Error: Either provide a PATTERN or use --missing-rakefile"
|
442
|
-
exit 1
|
443
|
-
end
|
444
|
-
end
|
445
|
-
|
446
|
-
private
|
447
|
-
|
448
|
-
def remove_by_pattern(pattern)
|
449
|
-
tracked_repos = load_tracked_repositories
|
450
|
-
matching_repos = tracked_repos.select { |repo| repo["url"].include?(pattern) }
|
451
|
-
|
452
|
-
if matching_repos.empty?
|
453
|
-
puts "No repositories found matching pattern: #{pattern}"
|
454
|
-
return
|
455
|
-
end
|
456
|
-
|
457
|
-
puts "Found #{matching_repos.length} repositories matching pattern: #{pattern}"
|
458
|
-
matching_repos.each { |repo| puts " - #{repo["url"]}" }
|
459
|
-
|
460
|
-
return if dry_run?
|
461
|
-
|
462
|
-
remaining_repos = tracked_repos.reject { |repo| repo["url"].include?(pattern) }
|
463
|
-
save_tracked_repositories(remaining_repos)
|
464
|
-
puts "✅ Removed #{matching_repos.length} repositories"
|
465
|
-
end
|
466
|
-
|
467
|
-
def remove_missing_rakefile_repos
|
468
|
-
tracked_repos = load_tracked_repositories
|
469
|
-
repos_to_remove = []
|
470
|
-
|
471
|
-
tracked_repos.each do |repo|
|
472
|
-
clone_dir = Makit::Directories.get_clone_directory(repo["url"])
|
473
|
-
rakefile_path = File.join(clone_dir, "Rakefile")
|
474
|
-
|
475
|
-
if Dir.exist?(clone_dir) && !File.exist?(rakefile_path)
|
476
|
-
repos_to_remove << repo
|
477
|
-
puts " - #{repo["url"]} (no Rakefile)" if verbose?
|
478
|
-
end
|
479
|
-
end
|
480
|
-
|
481
|
-
if repos_to_remove.empty?
|
482
|
-
puts "No repositories found without Rakefile"
|
483
|
-
return
|
484
|
-
end
|
485
|
-
|
486
|
-
puts "Found #{repos_to_remove.length} repositories without Rakefile"
|
487
|
-
return if dry_run?
|
488
|
-
|
489
|
-
remaining_repos = tracked_repos - repos_to_remove
|
490
|
-
save_tracked_repositories(remaining_repos)
|
491
|
-
puts "✅ Removed #{repos_to_remove.length} repositories without Rakefile"
|
492
|
-
end
|
493
|
-
|
494
|
-
def load_tracked_repositories
|
495
|
-
tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
496
|
-
return [] unless File.exist?(tracked_file)
|
497
|
-
|
498
|
-
begin
|
499
|
-
JSON.parse(File.read(tracked_file))
|
500
|
-
rescue JSON::ParserError => e
|
501
|
-
warn "Warning: Could not parse tracked repositories file: #{e.message}"
|
502
|
-
[]
|
503
|
-
end
|
504
|
-
end
|
505
|
-
|
506
|
-
def save_tracked_repositories(repositories)
|
507
|
-
tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
508
|
-
FileUtils.mkdir_p(File.dirname(tracked_file))
|
509
|
-
sorted_repos = repositories.sort_by { |repo| repo["url"] }
|
510
|
-
File.write(tracked_file, JSON.pretty_generate(sorted_repos))
|
511
|
-
end
|
512
|
-
end
|
513
|
-
|
514
|
-
# Import repositories from filesystem
|
515
|
-
class ImportRepositoryCommand < Clamp::Command
|
516
|
-
self.description = <<~DESC
|
517
|
-
Import git repositories from filesystem by discovery.
|
518
|
-
|
519
|
-
Examples:
|
520
|
-
makit repository import
|
521
|
-
makit repository import --directory ~/code
|
522
|
-
makit repository import --skip-rakefile-check
|
523
|
-
DESC
|
524
|
-
|
525
|
-
option ["--directory"], "DIR", "Directory to search for repositories", default: File.join(Dir.home, "code")
|
526
|
-
option ["--max-depth"], "DEPTH", "Maximum search depth", default: 3, &method(:Integer)
|
527
|
-
option ["--skip-rakefile-check"], :flag, "Import all repositories, not just those with Rakefile"
|
528
|
-
option ["--dry-run"], :flag, "Show what would be imported without actually importing"
|
529
|
-
option ["--verbose"], :flag, "Show verbose output"
|
530
|
-
|
531
|
-
def execute
|
532
|
-
search_dir = directory
|
533
|
-
unless Dir.exist?(search_dir)
|
534
|
-
puts "Directory does not exist: #{search_dir}"
|
535
|
-
exit 1
|
536
|
-
end
|
537
|
-
|
538
|
-
puts "Searching for git repositories in: #{search_dir}"
|
539
|
-
puts "Max depth: #{max_depth}"
|
540
|
-
puts "Rakefile required: #{!skip_rakefile_check?}"
|
541
|
-
|
542
|
-
repositories = discover_repositories(search_dir)
|
543
|
-
|
544
|
-
if repositories.empty?
|
545
|
-
puts "No repositories found"
|
546
|
-
return
|
547
|
-
end
|
548
|
-
|
549
|
-
puts "\nFound #{repositories.length} repositories:"
|
550
|
-
repositories.each { |repo| puts " - #{repo[:url] || repo[:path]}" }
|
551
|
-
|
552
|
-
return if dry_run?
|
553
|
-
|
554
|
-
import_repositories(repositories)
|
555
|
-
end
|
556
|
-
|
557
|
-
private
|
558
|
-
|
559
|
-
def discover_repositories(base_dir)
|
560
|
-
repositories = []
|
561
|
-
|
562
|
-
Find.find(base_dir) do |path|
|
563
|
-
next unless File.directory?(path) && File.basename(path) == ".git"
|
564
|
-
|
565
|
-
repo_dir = File.dirname(path)
|
566
|
-
|
567
|
-
# Check depth
|
568
|
-
relative_path = repo_dir.sub("#{base_dir}/", "")
|
569
|
-
depth = relative_path.split("/").length
|
570
|
-
|
571
|
-
if depth <= max_depth
|
572
|
-
repo_info = extract_repository_info(repo_dir)
|
573
|
-
repositories << repo_info if repo_info && should_import?(repo_dir)
|
574
|
-
end
|
575
|
-
|
576
|
-
Find.prune
|
577
|
-
end
|
578
|
-
|
579
|
-
repositories
|
580
|
-
end
|
581
|
-
|
582
|
-
def should_import?(repo_dir)
|
583
|
-
return true if skip_rakefile_check?
|
584
|
-
|
585
|
-
File.exist?(File.join(repo_dir, "Rakefile"))
|
586
|
-
end
|
587
|
-
|
588
|
-
def extract_repository_info(repo_dir)
|
589
|
-
return nil unless Dir.exist?(repo_dir)
|
590
|
-
|
591
|
-
info = { path: repo_dir }
|
592
|
-
|
593
|
-
begin
|
594
|
-
Dir.chdir(repo_dir) do
|
595
|
-
remote_output = `git remote get-url origin 2>/dev/null`.strip
|
596
|
-
info[:url] = remote_output unless remote_output.empty?
|
597
|
-
end
|
598
|
-
rescue StandardError
|
599
|
-
# If we can't get git info, skip this repo
|
600
|
-
return nil
|
601
|
-
end
|
602
|
-
|
603
|
-
info
|
604
|
-
end
|
605
|
-
|
606
|
-
def import_repositories(repositories)
|
607
|
-
tracked_repos = load_tracked_repositories
|
608
|
-
imported_count = 0
|
609
|
-
|
610
|
-
repositories.each do |repo|
|
611
|
-
next unless repo[:url]
|
612
|
-
next if tracked_repos.any? { |tracked| tracked["url"] == repo[:url] }
|
613
|
-
|
614
|
-
new_repo = {
|
615
|
-
"url" => repo[:url],
|
616
|
-
"added_at" => Time.now.iso8601,
|
617
|
-
"status" => "imported",
|
618
|
-
}
|
619
|
-
|
620
|
-
tracked_repos << new_repo
|
621
|
-
imported_count += 1
|
622
|
-
puts " ✅ Imported: #{repo[:url]}" if verbose?
|
623
|
-
end
|
624
|
-
|
625
|
-
if imported_count.positive?
|
626
|
-
save_tracked_repositories(tracked_repos)
|
627
|
-
puts "✅ Successfully imported #{imported_count} repositories"
|
628
|
-
else
|
629
|
-
puts "No new repositories to import"
|
630
|
-
end
|
631
|
-
end
|
632
|
-
|
633
|
-
def load_tracked_repositories
|
634
|
-
tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
635
|
-
return [] unless File.exist?(tracked_file)
|
636
|
-
|
637
|
-
begin
|
638
|
-
JSON.parse(File.read(tracked_file))
|
639
|
-
rescue JSON::ParserError => e
|
640
|
-
warn "Warning: Could not parse tracked repositories file: #{e.message}"
|
641
|
-
[]
|
642
|
-
end
|
643
|
-
end
|
644
|
-
|
645
|
-
def save_tracked_repositories(repositories)
|
646
|
-
tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
647
|
-
FileUtils.mkdir_p(File.dirname(tracked_file))
|
648
|
-
sorted_repos = repositories.sort_by { |repo| repo["url"] }
|
649
|
-
File.write(tracked_file, JSON.pretty_generate(sorted_repos))
|
650
|
-
end
|
651
|
-
end
|
652
|
-
|
653
|
-
# Add subcommand declarations after all classes are defined
|
654
|
-
RepositoryCommand.subcommand "add", "Add repository to tracking list", AddRepositoryCommand
|
655
|
-
RepositoryCommand.subcommand "clone", "Clone repository to local directory", CloneRepositoryCommand
|
656
|
-
RepositoryCommand.subcommand "pull", "Pull latest changes", PullRepositoryCommand
|
657
|
-
RepositoryCommand.subcommand "list", "List tracked repositories", ListRepositoryCommand
|
658
|
-
RepositoryCommand.subcommand "remove", "Remove tracked repositories", RemoveRepositoryCommand
|
659
|
-
RepositoryCommand.subcommand "import", "Import repositories from filesystem", ImportRepositoryCommand
|
660
|
-
end
|
661
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "clamp"
|
4
|
+
require "json"
|
5
|
+
require "uri"
|
6
|
+
require "time"
|
7
|
+
require "find"
|
8
|
+
|
9
|
+
module Makit
|
10
|
+
module Cli
|
11
|
+
# Consolidated repository management commands
|
12
|
+
# Combines: add, clone, pull, list, remove, import functionality
|
13
|
+
class RepositoryCommand < Clamp::Command
|
14
|
+
self.description = <<~DESC
|
15
|
+
Manage git repositories tracked by Makit.
|
16
|
+
|
17
|
+
Available operations:
|
18
|
+
add - Add repository URL to tracking list
|
19
|
+
clone - Clone repository to local directory#{" "}
|
20
|
+
pull - Pull latest changes from repository
|
21
|
+
list - List tracked repositories
|
22
|
+
remove - Remove tracked repositories by pattern
|
23
|
+
import - Import repositories from filesystem
|
24
|
+
DESC
|
25
|
+
end
|
26
|
+
|
27
|
+
# Add repository to tracking list
|
28
|
+
class AddRepositoryCommand < Clamp::Command
|
29
|
+
self.description = <<~DESC
|
30
|
+
Add git repository URLs to the tracked list.
|
31
|
+
|
32
|
+
Examples:
|
33
|
+
makit repository add https://github.com/user/repo.git
|
34
|
+
makit repository add git@gitlab.com:group/project.git
|
35
|
+
DESC
|
36
|
+
|
37
|
+
parameter "GIT_URL", "The git repository URL to track", attribute_name: :git_url, required: true
|
38
|
+
|
39
|
+
def execute
|
40
|
+
validate_git_url(git_url)
|
41
|
+
|
42
|
+
if add_to_tracked_list(git_url)
|
43
|
+
puts "✅ Successfully added repository to tracked list: #{git_url}"
|
44
|
+
else
|
45
|
+
puts "ℹ️ Repository already tracked: #{git_url}"
|
46
|
+
end
|
47
|
+
rescue ArgumentError => e
|
48
|
+
warn "❌ Invalid repository URL: #{e.message}"
|
49
|
+
exit 1
|
50
|
+
rescue StandardError => e
|
51
|
+
warn "❌ Failed to add repository: #{e.message}"
|
52
|
+
exit 1
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def validate_git_url(url)
|
58
|
+
raise ArgumentError, "URL cannot be empty" if url.nil? || url.strip.empty?
|
59
|
+
|
60
|
+
url = url.strip
|
61
|
+
valid_patterns = [
|
62
|
+
%r{^https?://\w+[\w.-]*\w+/[\w.-]+/[\w.-]+(?:\.git)?/?$}, # HTTPS
|
63
|
+
%r{^git@[\w.-]+:[\w.-]+/[\w.-]+(?:\.git)?$}, # SSH
|
64
|
+
%r{^ssh://git@[\w.-]+/[\w.-]+/[\w.-]+(?:\.git)?$}, # SSH with ssh://
|
65
|
+
%r{^[\w.-]+@[\w.-]+:[\w.-]+/[\w.-]+(?:\.git)?$}, # Generic SSH
|
66
|
+
]
|
67
|
+
|
68
|
+
unless valid_patterns.any? { |pattern| url.match?(pattern) }
|
69
|
+
raise ArgumentError, "URL does not match expected git repository format"
|
70
|
+
end
|
71
|
+
|
72
|
+
return unless url.include?("github.com") || url.include?("gitlab.com") || url.include?("bitbucket.org")
|
73
|
+
|
74
|
+
validate_hosted_repository_url(url)
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_hosted_repository_url(url)
|
78
|
+
if url.start_with?("http")
|
79
|
+
path = URI.parse(url).path.sub(%r{^/}, "").sub(/\.git$/, "").sub(%r{/$}, "")
|
80
|
+
elsif url.include?("@")
|
81
|
+
path = url.split(":").last.sub(/\.git$/, "")
|
82
|
+
end
|
83
|
+
|
84
|
+
parts = path.split("/")
|
85
|
+
raise ArgumentError, "Repository path should be in format 'user/repository'" if parts.length < 2
|
86
|
+
|
87
|
+
parts.each do |part|
|
88
|
+
if part.empty? || part.match?(/[<>:"|?*]/)
|
89
|
+
raise ArgumentError, "Invalid characters in repository path: #{part}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_to_tracked_list(url)
|
95
|
+
tracked_repos = load_tracked_repositories
|
96
|
+
return false if tracked_repos.any? { |repo| repo["url"] == url }
|
97
|
+
|
98
|
+
new_repo = {
|
99
|
+
"url" => url,
|
100
|
+
"added_at" => Time.now.iso8601,
|
101
|
+
"status" => "tracked",
|
102
|
+
}
|
103
|
+
|
104
|
+
tracked_repos << new_repo
|
105
|
+
save_tracked_repositories(tracked_repos)
|
106
|
+
true
|
107
|
+
end
|
108
|
+
|
109
|
+
def load_tracked_repositories
|
110
|
+
tracked_file = tracked_repositories_file
|
111
|
+
return [] unless File.exist?(tracked_file)
|
112
|
+
|
113
|
+
begin
|
114
|
+
JSON.parse(File.read(tracked_file))
|
115
|
+
rescue JSON::ParserError => e
|
116
|
+
warn "Warning: Could not parse tracked repositories file: #{e.message}"
|
117
|
+
[]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def save_tracked_repositories(repositories)
|
122
|
+
tracked_file = tracked_repositories_file
|
123
|
+
FileUtils.mkdir_p(File.dirname(tracked_file))
|
124
|
+
sorted_repos = repositories.sort_by { |repo| repo["url"] }
|
125
|
+
File.write(tracked_file, JSON.pretty_generate(sorted_repos))
|
126
|
+
end
|
127
|
+
|
128
|
+
def tracked_repositories_file
|
129
|
+
File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Clone repository to local directory
|
134
|
+
class CloneRepositoryCommand < Clamp::Command
|
135
|
+
self.description = <<~DESC
|
136
|
+
Clone git repositories to local directories.
|
137
|
+
|
138
|
+
Examples:
|
139
|
+
makit repository clone https://github.com/user/repo
|
140
|
+
makit repository clone git@github.com:user/repo.git
|
141
|
+
DESC
|
142
|
+
|
143
|
+
parameter "GIT_REPOSITORY", "The git repository URL", attribute_name: :git_repository, required: true
|
144
|
+
|
145
|
+
def execute
|
146
|
+
clone_dir = Makit::Directories.get_clone_directory(git_repository)
|
147
|
+
|
148
|
+
if Dir.exist?(clone_dir)
|
149
|
+
puts "Repository already exists at: #{clone_dir}"
|
150
|
+
return
|
151
|
+
end
|
152
|
+
|
153
|
+
puts "Cloning repository: #{git_repository} to #{clone_dir}"
|
154
|
+
|
155
|
+
begin
|
156
|
+
commands = Makit.clone(git_repository)
|
157
|
+
|
158
|
+
if commands.any? && commands.last.exit_code != 0
|
159
|
+
handle_clone_error(commands.last, clone_dir)
|
160
|
+
elsif !Dir.exist?(clone_dir)
|
161
|
+
warn "Clone operation completed but directory not found: #{clone_dir}"
|
162
|
+
exit 1
|
163
|
+
else
|
164
|
+
puts "Successfully cloned repository to: #{clone_dir}"
|
165
|
+
end
|
166
|
+
rescue ArgumentError => e
|
167
|
+
warn "Invalid repository URL: #{e.message}"
|
168
|
+
exit 1
|
169
|
+
rescue StandardError => e
|
170
|
+
warn "Failed to clone repository: #{git_repository}"
|
171
|
+
puts e.message
|
172
|
+
exit 1
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def handle_clone_error(clone_command, clone_dir)
|
179
|
+
summary = "Failed to clone repository: #{git_repository} to #{clone_dir}\n"
|
180
|
+
summary += "Please check the URL and your network connection.\n"
|
181
|
+
summary += "Exit code: #{clone_command.exit_code}\n"
|
182
|
+
summary += "Output: #{clone_command.output}" if clone_command.respond_to?(:output)
|
183
|
+
warn summary
|
184
|
+
exit 1
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Pull latest changes from repository
|
189
|
+
class PullRepositoryCommand < Clamp::Command
|
190
|
+
self.description = <<~DESC
|
191
|
+
Pull latest changes from git repositories.
|
192
|
+
|
193
|
+
Examples:
|
194
|
+
makit repository pull https://github.com/user/repo
|
195
|
+
DESC
|
196
|
+
|
197
|
+
parameter "GIT_REPOSITORY", "The git repository URL", attribute_name: :git_repository, required: true
|
198
|
+
|
199
|
+
def execute
|
200
|
+
commands = Makit.pull(git_repository)
|
201
|
+
|
202
|
+
if commands.any? && commands.last.exit_code.zero?
|
203
|
+
puts "Successfully pulled latest changes from: #{git_repository}"
|
204
|
+
else
|
205
|
+
warn "Failed to pull changes from: #{git_repository}"
|
206
|
+
exit 1
|
207
|
+
end
|
208
|
+
rescue ArgumentError => e
|
209
|
+
warn "Invalid repository URL: #{e.message}"
|
210
|
+
exit 1
|
211
|
+
rescue StandardError => e
|
212
|
+
warn "Failed to pull repository: #{git_repository}"
|
213
|
+
puts e.message
|
214
|
+
exit 1
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# List tracked repositories
|
219
|
+
class ListRepositoryCommand < Clamp::Command
|
220
|
+
self.description = <<~DESC
|
221
|
+
List git repositories tracked by Makit.
|
222
|
+
|
223
|
+
Examples:
|
224
|
+
makit repository list
|
225
|
+
makit repository list --details
|
226
|
+
makit repository list --tracked
|
227
|
+
DESC
|
228
|
+
|
229
|
+
option ["--details"], :flag, "Show detailed information about each repository"
|
230
|
+
option ["--paths"], :flag, "Show local paths for each repository"
|
231
|
+
option ["--tracked"], :flag, "Show tracked repository URLs (not cloned)"
|
232
|
+
option ["--all"], :flag, "Show both cloned and tracked repositories"
|
233
|
+
option ["--format"], "FORMAT", "Output format (table, json, plain)", default: "table"
|
234
|
+
|
235
|
+
def execute
|
236
|
+
repositories = discover_repositories
|
237
|
+
|
238
|
+
if repositories.empty?
|
239
|
+
puts "No git repositories found."
|
240
|
+
puts "Use 'makit repository clone <url>' to clone repositories."
|
241
|
+
return
|
242
|
+
end
|
243
|
+
|
244
|
+
case format
|
245
|
+
when "json"
|
246
|
+
output_json(repositories)
|
247
|
+
when "plain"
|
248
|
+
output_plain(repositories)
|
249
|
+
else
|
250
|
+
output_table(repositories)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
def discover_repositories
|
257
|
+
repositories = []
|
258
|
+
|
259
|
+
if tracked? && !all?
|
260
|
+
repositories = load_tracked_repositories
|
261
|
+
elsif all?
|
262
|
+
repositories = discover_cloned_repositories
|
263
|
+
tracked_repos = load_tracked_repositories
|
264
|
+
tracked_repos.each do |tracked_repo|
|
265
|
+
unless repositories.any? { |repo| repo[:url] == tracked_repo[:url] }
|
266
|
+
repositories << tracked_repo.merge(type: "tracked")
|
267
|
+
end
|
268
|
+
end
|
269
|
+
else
|
270
|
+
repositories = discover_cloned_repositories
|
271
|
+
end
|
272
|
+
|
273
|
+
repositories.sort_by { |repo| repo[:url] || repo[:name] }
|
274
|
+
end
|
275
|
+
|
276
|
+
def discover_cloned_repositories
|
277
|
+
repositories = []
|
278
|
+
clone_base_dir = Makit::Directories::CLONE
|
279
|
+
return repositories unless Dir.exist?(clone_base_dir)
|
280
|
+
|
281
|
+
Find.find(clone_base_dir) do |path|
|
282
|
+
next unless File.directory?(path) && File.basename(path) == ".git"
|
283
|
+
|
284
|
+
repo_dir = File.dirname(path)
|
285
|
+
repo_info = extract_repository_info(repo_dir)
|
286
|
+
if repo_info
|
287
|
+
repo_info[:type] = "cloned"
|
288
|
+
repositories << repo_info
|
289
|
+
end
|
290
|
+
Find.prune
|
291
|
+
end
|
292
|
+
|
293
|
+
repositories
|
294
|
+
end
|
295
|
+
|
296
|
+
def load_tracked_repositories
|
297
|
+
tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
298
|
+
return [] unless File.exist?(tracked_file)
|
299
|
+
|
300
|
+
begin
|
301
|
+
tracked_repos = JSON.parse(File.read(tracked_file))
|
302
|
+
tracked_repos.map do |repo|
|
303
|
+
{
|
304
|
+
name: extract_repo_name_from_url(repo["url"]),
|
305
|
+
url: repo["url"],
|
306
|
+
added_at: repo["added_at"],
|
307
|
+
status: repo["status"],
|
308
|
+
type: "tracked",
|
309
|
+
}
|
310
|
+
end
|
311
|
+
rescue JSON::ParserError => e
|
312
|
+
warn "Warning: Could not parse tracked repositories file: #{e.message}"
|
313
|
+
[]
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def extract_repo_name_from_url(url)
|
318
|
+
if url.include?("/")
|
319
|
+
url.split("/").last.sub(/\.git$/, "")
|
320
|
+
else
|
321
|
+
url
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def extract_repository_info(repo_dir)
|
326
|
+
return nil unless Dir.exist?(repo_dir)
|
327
|
+
|
328
|
+
info = {
|
329
|
+
name: File.basename(repo_dir),
|
330
|
+
path: repo_dir,
|
331
|
+
relative_path: repo_dir.sub("#{Makit::Directories::CLONE}/", ""),
|
332
|
+
}
|
333
|
+
|
334
|
+
begin
|
335
|
+
Dir.chdir(repo_dir) do
|
336
|
+
remote_output = `git remote get-url origin 2>/dev/null`.strip
|
337
|
+
info[:url] = remote_output unless remote_output.empty?
|
338
|
+
|
339
|
+
if details?
|
340
|
+
info[:branch] = `git branch --show-current 2>/dev/null`.strip
|
341
|
+
info[:last_commit] = `git log -1 --format="%H %s" 2>/dev/null`.strip
|
342
|
+
info[:last_commit_date] = `git log -1 --format="%ci" 2>/dev/null`.strip
|
343
|
+
info[:status] = `git status --porcelain 2>/dev/null`.strip
|
344
|
+
info[:clean] = info[:status].empty?
|
345
|
+
end
|
346
|
+
end
|
347
|
+
rescue StandardError => e
|
348
|
+
info[:error] = "Cannot read git info: #{e.message}"
|
349
|
+
end
|
350
|
+
|
351
|
+
info
|
352
|
+
end
|
353
|
+
|
354
|
+
def output_table(repositories)
|
355
|
+
if details?
|
356
|
+
output_detailed_table(repositories)
|
357
|
+
else
|
358
|
+
output_simple_table(repositories)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def output_simple_table(repositories)
|
363
|
+
puts "Tracked Git Repositories (#{repositories.length}):"
|
364
|
+
puts "=" * 80
|
365
|
+
|
366
|
+
repositories.each_with_index do |repo, index|
|
367
|
+
type_indicator = repo[:type] == "tracked" ? "📋" : "📁"
|
368
|
+
puts "#{(index + 1).to_s.rjust(3)}. #{type_indicator} #{repo[:url] || repo[:name]}"
|
369
|
+
puts " Path: #{repo[:path] || "Not cloned"}" if paths?
|
370
|
+
puts " Type: #{repo[:type]&.capitalize || "Unknown"}" if all? || tracked?
|
371
|
+
puts
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def output_detailed_table(repositories)
|
376
|
+
puts "Tracked Git Repositories (#{repositories.length}):"
|
377
|
+
puts "=" * 120
|
378
|
+
|
379
|
+
repositories.each_with_index do |repo, index|
|
380
|
+
type_indicator = repo[:type] == "tracked" ? "📋" : "📁"
|
381
|
+
puts "#{(index + 1).to_s.rjust(3)}. #{type_indicator} #{repo[:url] || repo[:name]}"
|
382
|
+
puts " Type: #{repo[:type]&.capitalize || "Unknown"}"
|
383
|
+
puts " Path: #{repo[:path] || "Not cloned"}" if repo[:type] != "tracked" || all?
|
384
|
+
puts " Branch: #{repo[:branch]}" if repo[:branch] && !repo[:branch].empty?
|
385
|
+
puts " Status: #{repo[:clean] ? "Clean" : "Modified"}" if repo[:type] == "cloned"
|
386
|
+
|
387
|
+
if repo[:last_commit] && !repo[:last_commit].empty?
|
388
|
+
commit_parts = repo[:last_commit].split(" ", 2)
|
389
|
+
commit_hash = commit_parts[0][0..7] if commit_parts[0]
|
390
|
+
commit_msg = commit_parts[1] if commit_parts[1]
|
391
|
+
puts " Commit: #{commit_hash} - #{commit_msg}"
|
392
|
+
end
|
393
|
+
|
394
|
+
puts " Date: #{repo[:last_commit_date]}" if repo[:last_commit_date] && !repo[:last_commit_date].empty?
|
395
|
+
puts " Added: #{repo[:added_at]}" if repo[:added_at]
|
396
|
+
puts " Error: #{repo[:error]}" if repo[:error]
|
397
|
+
puts
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def output_json(repositories)
|
402
|
+
puts JSON.pretty_generate({
|
403
|
+
count: repositories.length,
|
404
|
+
repositories: repositories,
|
405
|
+
})
|
406
|
+
end
|
407
|
+
|
408
|
+
def output_plain(repositories)
|
409
|
+
repositories.each do |repo|
|
410
|
+
if paths?
|
411
|
+
puts "#{repo[:url] || repo[:name]} -> #{repo[:path]}"
|
412
|
+
else
|
413
|
+
puts repo[:url] || repo[:name]
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# Remove tracked repositories by pattern
|
420
|
+
class RemoveRepositoryCommand < Clamp::Command
|
421
|
+
self.description = <<~DESC
|
422
|
+
Remove tracked repositories using pattern matching.
|
423
|
+
|
424
|
+
Examples:
|
425
|
+
makit repository remove github.com/user/
|
426
|
+
makit repository remove clamp.git
|
427
|
+
makit repository remove --missing-rakefile
|
428
|
+
DESC
|
429
|
+
|
430
|
+
parameter "PATTERN", "Pattern to match repository URLs", attribute_name: :pattern, required: false
|
431
|
+
option ["--missing-rakefile"], :flag, "Remove repositories without Rakefile"
|
432
|
+
option ["--dry-run"], :flag, "Show what would be removed without actually removing"
|
433
|
+
option ["--verbose"], :flag, "Show verbose output"
|
434
|
+
|
435
|
+
def execute
|
436
|
+
if missing_rakefile?
|
437
|
+
remove_missing_rakefile_repos
|
438
|
+
elsif pattern
|
439
|
+
remove_by_pattern(pattern)
|
440
|
+
else
|
441
|
+
puts "Error: Either provide a PATTERN or use --missing-rakefile"
|
442
|
+
exit 1
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
private
|
447
|
+
|
448
|
+
def remove_by_pattern(pattern)
|
449
|
+
tracked_repos = load_tracked_repositories
|
450
|
+
matching_repos = tracked_repos.select { |repo| repo["url"].include?(pattern) }
|
451
|
+
|
452
|
+
if matching_repos.empty?
|
453
|
+
puts "No repositories found matching pattern: #{pattern}"
|
454
|
+
return
|
455
|
+
end
|
456
|
+
|
457
|
+
puts "Found #{matching_repos.length} repositories matching pattern: #{pattern}"
|
458
|
+
matching_repos.each { |repo| puts " - #{repo["url"]}" }
|
459
|
+
|
460
|
+
return if dry_run?
|
461
|
+
|
462
|
+
remaining_repos = tracked_repos.reject { |repo| repo["url"].include?(pattern) }
|
463
|
+
save_tracked_repositories(remaining_repos)
|
464
|
+
puts "✅ Removed #{matching_repos.length} repositories"
|
465
|
+
end
|
466
|
+
|
467
|
+
def remove_missing_rakefile_repos
|
468
|
+
tracked_repos = load_tracked_repositories
|
469
|
+
repos_to_remove = []
|
470
|
+
|
471
|
+
tracked_repos.each do |repo|
|
472
|
+
clone_dir = Makit::Directories.get_clone_directory(repo["url"])
|
473
|
+
rakefile_path = File.join(clone_dir, "Rakefile")
|
474
|
+
|
475
|
+
if Dir.exist?(clone_dir) && !File.exist?(rakefile_path)
|
476
|
+
repos_to_remove << repo
|
477
|
+
puts " - #{repo["url"]} (no Rakefile)" if verbose?
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
if repos_to_remove.empty?
|
482
|
+
puts "No repositories found without Rakefile"
|
483
|
+
return
|
484
|
+
end
|
485
|
+
|
486
|
+
puts "Found #{repos_to_remove.length} repositories without Rakefile"
|
487
|
+
return if dry_run?
|
488
|
+
|
489
|
+
remaining_repos = tracked_repos - repos_to_remove
|
490
|
+
save_tracked_repositories(remaining_repos)
|
491
|
+
puts "✅ Removed #{repos_to_remove.length} repositories without Rakefile"
|
492
|
+
end
|
493
|
+
|
494
|
+
def load_tracked_repositories
|
495
|
+
tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
496
|
+
return [] unless File.exist?(tracked_file)
|
497
|
+
|
498
|
+
begin
|
499
|
+
JSON.parse(File.read(tracked_file))
|
500
|
+
rescue JSON::ParserError => e
|
501
|
+
warn "Warning: Could not parse tracked repositories file: #{e.message}"
|
502
|
+
[]
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
def save_tracked_repositories(repositories)
|
507
|
+
tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
508
|
+
FileUtils.mkdir_p(File.dirname(tracked_file))
|
509
|
+
sorted_repos = repositories.sort_by { |repo| repo["url"] }
|
510
|
+
File.write(tracked_file, JSON.pretty_generate(sorted_repos))
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
# Import repositories from filesystem
|
515
|
+
class ImportRepositoryCommand < Clamp::Command
|
516
|
+
self.description = <<~DESC
|
517
|
+
Import git repositories from filesystem by discovery.
|
518
|
+
|
519
|
+
Examples:
|
520
|
+
makit repository import
|
521
|
+
makit repository import --directory ~/code
|
522
|
+
makit repository import --skip-rakefile-check
|
523
|
+
DESC
|
524
|
+
|
525
|
+
option ["--directory"], "DIR", "Directory to search for repositories", default: File.join(Dir.home, "code")
|
526
|
+
option ["--max-depth"], "DEPTH", "Maximum search depth", default: 3, &method(:Integer)
|
527
|
+
option ["--skip-rakefile-check"], :flag, "Import all repositories, not just those with Rakefile"
|
528
|
+
option ["--dry-run"], :flag, "Show what would be imported without actually importing"
|
529
|
+
option ["--verbose"], :flag, "Show verbose output"
|
530
|
+
|
531
|
+
def execute
|
532
|
+
search_dir = directory
|
533
|
+
unless Dir.exist?(search_dir)
|
534
|
+
puts "Directory does not exist: #{search_dir}"
|
535
|
+
exit 1
|
536
|
+
end
|
537
|
+
|
538
|
+
puts "Searching for git repositories in: #{search_dir}"
|
539
|
+
puts "Max depth: #{max_depth}"
|
540
|
+
puts "Rakefile required: #{!skip_rakefile_check?}"
|
541
|
+
|
542
|
+
repositories = discover_repositories(search_dir)
|
543
|
+
|
544
|
+
if repositories.empty?
|
545
|
+
puts "No repositories found"
|
546
|
+
return
|
547
|
+
end
|
548
|
+
|
549
|
+
puts "\nFound #{repositories.length} repositories:"
|
550
|
+
repositories.each { |repo| puts " - #{repo[:url] || repo[:path]}" }
|
551
|
+
|
552
|
+
return if dry_run?
|
553
|
+
|
554
|
+
import_repositories(repositories)
|
555
|
+
end
|
556
|
+
|
557
|
+
private
|
558
|
+
|
559
|
+
def discover_repositories(base_dir)
|
560
|
+
repositories = []
|
561
|
+
|
562
|
+
Find.find(base_dir) do |path|
|
563
|
+
next unless File.directory?(path) && File.basename(path) == ".git"
|
564
|
+
|
565
|
+
repo_dir = File.dirname(path)
|
566
|
+
|
567
|
+
# Check depth
|
568
|
+
relative_path = repo_dir.sub("#{base_dir}/", "")
|
569
|
+
depth = relative_path.split("/").length
|
570
|
+
|
571
|
+
if depth <= max_depth
|
572
|
+
repo_info = extract_repository_info(repo_dir)
|
573
|
+
repositories << repo_info if repo_info && should_import?(repo_dir)
|
574
|
+
end
|
575
|
+
|
576
|
+
Find.prune
|
577
|
+
end
|
578
|
+
|
579
|
+
repositories
|
580
|
+
end
|
581
|
+
|
582
|
+
def should_import?(repo_dir)
|
583
|
+
return true if skip_rakefile_check?
|
584
|
+
|
585
|
+
File.exist?(File.join(repo_dir, "Rakefile"))
|
586
|
+
end
|
587
|
+
|
588
|
+
def extract_repository_info(repo_dir)
|
589
|
+
return nil unless Dir.exist?(repo_dir)
|
590
|
+
|
591
|
+
info = { path: repo_dir }
|
592
|
+
|
593
|
+
begin
|
594
|
+
Dir.chdir(repo_dir) do
|
595
|
+
remote_output = `git remote get-url origin 2>/dev/null`.strip
|
596
|
+
info[:url] = remote_output unless remote_output.empty?
|
597
|
+
end
|
598
|
+
rescue StandardError
|
599
|
+
# If we can't get git info, skip this repo
|
600
|
+
return nil
|
601
|
+
end
|
602
|
+
|
603
|
+
info
|
604
|
+
end
|
605
|
+
|
606
|
+
def import_repositories(repositories)
|
607
|
+
tracked_repos = load_tracked_repositories
|
608
|
+
imported_count = 0
|
609
|
+
|
610
|
+
repositories.each do |repo|
|
611
|
+
next unless repo[:url]
|
612
|
+
next if tracked_repos.any? { |tracked| tracked["url"] == repo[:url] }
|
613
|
+
|
614
|
+
new_repo = {
|
615
|
+
"url" => repo[:url],
|
616
|
+
"added_at" => Time.now.iso8601,
|
617
|
+
"status" => "imported",
|
618
|
+
}
|
619
|
+
|
620
|
+
tracked_repos << new_repo
|
621
|
+
imported_count += 1
|
622
|
+
puts " ✅ Imported: #{repo[:url]}" if verbose?
|
623
|
+
end
|
624
|
+
|
625
|
+
if imported_count.positive?
|
626
|
+
save_tracked_repositories(tracked_repos)
|
627
|
+
puts "✅ Successfully imported #{imported_count} repositories"
|
628
|
+
else
|
629
|
+
puts "No new repositories to import"
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
def load_tracked_repositories
|
634
|
+
tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
635
|
+
return [] unless File.exist?(tracked_file)
|
636
|
+
|
637
|
+
begin
|
638
|
+
JSON.parse(File.read(tracked_file))
|
639
|
+
rescue JSON::ParserError => e
|
640
|
+
warn "Warning: Could not parse tracked repositories file: #{e.message}"
|
641
|
+
[]
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
def save_tracked_repositories(repositories)
|
646
|
+
tracked_file = File.join(Makit::Directories::ROOT, "tracked_repositories.json")
|
647
|
+
FileUtils.mkdir_p(File.dirname(tracked_file))
|
648
|
+
sorted_repos = repositories.sort_by { |repo| repo["url"] }
|
649
|
+
File.write(tracked_file, JSON.pretty_generate(sorted_repos))
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
# Add subcommand declarations after all classes are defined
|
654
|
+
RepositoryCommand.subcommand "add", "Add repository to tracking list", AddRepositoryCommand
|
655
|
+
RepositoryCommand.subcommand "clone", "Clone repository to local directory", CloneRepositoryCommand
|
656
|
+
RepositoryCommand.subcommand "pull", "Pull latest changes", PullRepositoryCommand
|
657
|
+
RepositoryCommand.subcommand "list", "List tracked repositories", ListRepositoryCommand
|
658
|
+
RepositoryCommand.subcommand "remove", "Remove tracked repositories", RemoveRepositoryCommand
|
659
|
+
RepositoryCommand.subcommand "import", "Import repositories from filesystem", ImportRepositoryCommand
|
660
|
+
end
|
661
|
+
end
|