archsight 0.3.2 → 0.4.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.
- checksums.yaml +4 -4
- data/README.md +7 -1
- data/docs/archimate.md +21 -1
- data/docs/configuration.md +1 -0
- data/docs/diagram.md +15 -3
- data/docs/docker.md +4 -1
- data/docs/icons.md +32 -0
- data/docs/import.md +178 -55
- data/docs/index.md.erb +31 -8
- data/docs/modeling.md +772 -24
- data/docs/pages.md +44 -13
- data/docs/search.md +21 -1
- data/docs/togaf.md +5 -1
- data/lib/archsight/analysis/result.rb +8 -2
- data/lib/archsight/analysis/sandbox.rb +8 -0
- data/lib/archsight/analysis/typst_renderer.rb +246 -0
- data/lib/archsight/analysis.rb +1 -0
- data/lib/archsight/annotations/annotation.rb +30 -3
- data/lib/archsight/annotations/architecture_annotations.rb +47 -0
- data/lib/archsight/annotations/asset_annotations.rb +34 -0
- data/lib/archsight/annotations/link_annotations.rb +15 -0
- data/lib/archsight/annotations/resource_reference.rb +12 -0
- data/lib/archsight/annotations/risk_annotations.rb +21 -0
- data/lib/archsight/cli.rb +124 -23
- data/lib/archsight/database.rb +40 -0
- data/lib/archsight/diagram/cli.rb +50 -2
- data/lib/archsight/diagram/formatter.rb +112 -0
- data/lib/archsight/diagram/routing/edge_router/orthogonal_path.rb +38 -10
- data/lib/archsight/diagram/routing/edge_router.rb +29 -4
- data/lib/archsight/diagram/routing/edge_routing.rb +61 -5
- data/lib/archsight/diagram/style/theme.rb +7 -2
- data/lib/archsight/documentation.rb +3 -2
- data/lib/archsight/editor/page_source.rb +13 -1
- data/lib/archsight/export/confluence/analysis_output.rb +101 -0
- data/lib/archsight/export/confluence/client.rb +13 -0
- data/lib/archsight/export/confluence/diagram_links.rb +1 -1
- data/lib/archsight/export/confluence/exporter.rb +40 -13
- data/lib/archsight/export/confluence/labels.rb +25 -0
- data/lib/archsight/export/confluence/page_properties.rb +4 -6
- data/lib/archsight/export/confluence/storage.rb +38 -2
- data/lib/archsight/helpers/embeds.rb +18 -7
- data/lib/archsight/helpers/resource_resolver.rb +25 -0
- data/lib/archsight/import/executor.rb +139 -16
- data/lib/archsight/import/handler.rb +55 -6
- data/lib/archsight/import/handler_loader.rb +64 -0
- data/lib/archsight/import/handlers/cache_pruner.rb +30 -7
- data/lib/archsight/import/handlers/command.rb +120 -0
- data/lib/archsight/import/handlers/github.rb +4 -1
- data/lib/archsight/import/handlers/gitlab.rb +4 -1
- data/lib/archsight/import/handlers/go_dep_resolver.rb +10 -2
- data/lib/archsight/import/handlers/go_grapher.rb +102 -5
- data/lib/archsight/import/handlers/go_module_parser.rb +88 -0
- data/lib/archsight/import/handlers/grapher.rb +40 -1
- data/lib/archsight/import/handlers/repository.rb +3 -0
- data/lib/archsight/import/handlers/rest_api_index.rb +3 -0
- data/lib/archsight/import/registry.rb +22 -1
- data/lib/archsight/import.rb +64 -1
- data/lib/archsight/linter/structure.rb +239 -0
- data/lib/archsight/linter.rb +217 -6
- data/lib/archsight/page_loader.rb +12 -1
- data/lib/archsight/query/evaluator.rb +13 -6
- data/lib/archsight/query/parser.rb +73 -6
- data/lib/archsight/query.rb +1 -1
- data/lib/archsight/references.rb +3 -3
- data/lib/archsight/renderer.rb +80 -23
- data/lib/archsight/resources/analysis.rb +3 -2
- data/lib/archsight/resources/application_component.rb +83 -8
- data/lib/archsight/resources/application_event.rb +79 -0
- data/lib/archsight/resources/application_service.rb +18 -2
- data/lib/archsight/resources/base.rb +58 -5
- data/lib/archsight/resources/business_actor.rb +14 -1
- data/lib/archsight/resources/business_contract.rb +72 -0
- data/lib/archsight/resources/business_control.rb +10 -2
- data/lib/archsight/resources/business_event.rb +79 -0
- data/lib/archsight/resources/business_process.rb +8 -1
- data/lib/archsight/resources/business_product.rb +12 -0
- data/lib/archsight/resources/business_role.rb +70 -0
- data/lib/archsight/resources/compliance_evidence.rb +8 -2
- data/lib/archsight/resources/data_object.rb +6 -1
- data/lib/archsight/resources/implementation_deliverable.rb +63 -0
- data/lib/archsight/resources/implementation_event.rb +52 -0
- data/lib/archsight/resources/implementation_gap.rb +49 -0
- data/lib/archsight/resources/implementation_plateau.rb +67 -0
- data/lib/archsight/resources/implementation_work_package.rb +84 -0
- data/lib/archsight/resources/import.rb +37 -11
- data/lib/archsight/resources/location.rb +56 -0
- data/lib/archsight/resources/motivation_assessment.rb +121 -0
- data/lib/archsight/resources/motivation_constraint.rb +7 -1
- data/lib/archsight/resources/motivation_driver.rb +50 -0
- data/lib/archsight/resources/motivation_goal.rb +13 -1
- data/lib/archsight/resources/motivation_principle.rb +75 -0
- data/lib/archsight/resources/motivation_requirement.rb +18 -2
- data/lib/archsight/resources/motivation_stakeholder.rb +21 -0
- data/lib/archsight/resources/page.rb +9 -7
- data/lib/archsight/resources/strategy_capability.rb +9 -0
- data/lib/archsight/resources/technology_artifact.rb +11 -4
- data/lib/archsight/resources/technology_communication_network.rb +89 -0
- data/lib/archsight/resources/technology_device.rb +88 -0
- data/lib/archsight/resources/technology_equipment.rb +75 -0
- data/lib/archsight/resources/technology_event.rb +80 -0
- data/lib/archsight/resources/technology_facility.rb +70 -0
- data/lib/archsight/resources/technology_node.rb +57 -1
- data/lib/archsight/resources/technology_service.rb +33 -0
- data/lib/archsight/resources/technology_system_software.rb +28 -0
- data/lib/archsight/resources.rb +12 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/analysis_helpers.rb +55 -0
- data/lib/archsight/web/api/json_helpers.rb +3 -37
- data/lib/archsight/web/api/openapi/spec.yaml +10 -8
- data/lib/archsight/web/api/page_helpers.rb +1 -8
- data/lib/archsight/web/api/routes.rb +7 -2
- data/lib/archsight/web/application.rb +38 -2
- data/lib/archsight/web/editor/routes.rb +2 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-COp4onwn.js +1 -0
- data/lib/archsight/web/public/vue/DocPage-D52OwaKc.js +1 -0
- data/lib/archsight/web/public/vue/EditorPage-3aqL7yVf.js +35 -0
- data/lib/archsight/web/public/vue/ErrorPage--b9XeSYa.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-qh_EtRrQ.js +1 -0
- data/lib/archsight/web/public/vue/HomePage-C2TNh2U-.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-ZaCcjlIX.css +1 -0
- data/lib/archsight/web/public/vue/InstanceRouter-uzb_OMG0.js +1 -0
- data/lib/archsight/web/public/vue/KindList-CyJuP_uq.js +1 -0
- data/lib/archsight/web/public/vue/KindList-yYcRT2BV.css +1 -0
- data/lib/archsight/web/public/vue/PageView-B93woeN3.css +1 -0
- data/lib/archsight/web/public/vue/PageView-FyygrS-b.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-B4Ex_csZ.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-BY6Z1Kcb.js +2 -0
- data/lib/archsight/web/public/vue/ResourceList-D75O87zG.css +1 -0
- data/lib/archsight/web/public/vue/{SearchResults-CyPUZZSC.css → SearchResults-BV6UHriJ.css} +1 -1
- data/lib/archsight/web/public/vue/SearchResults-CkFpHF8m.js +1 -0
- data/lib/archsight/web/public/vue/WikiPage-BoHNewCX.js +3 -0
- data/lib/archsight/web/public/vue/WikiPage-TpmM1VAF.css +1 -0
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BvzKZE4e.js +1 -0
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-D-xs_Hxh.js +1 -0
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-nbjxG01S.js +1 -0
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-CW6mw2ab.js +1 -0
- data/lib/archsight/web/public/vue/index-BGlYCbi7.js +3 -0
- data/lib/archsight/web/public/vue/index-eUHmJ7K2.css +1 -0
- data/lib/archsight/web/public/vue/info-A6RAGUB7-C12sjlHe.js +1 -0
- data/lib/archsight/web/public/vue/{mermaid-BjEi5URd.js → mermaid-DjsXQ_XN.js} +35 -35
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-wMwKwHmA.js +1 -0
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-w1-sAU_K.js +1 -0
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-DpwqIFoy.js +1 -0
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-Ce5XC24d.js +1 -0
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-BSQO2pds.js +1 -0
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-Dq1c2YtD.js +1 -0
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DKj6dli0.js +1 -0
- data/lib/archsight/web/public/vue/rolldown-runtime-hePW80VL.js +1 -0
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-BmHNTWxU.js +1 -0
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DsEI7sSD.js +1 -0
- data/lib/archsight/web/public/vue/{useGraphviz-DweKV7Kg.js → useGraphviz-4evusK9i.js} +13 -1
- data/lib/archsight/web/public/vue/{useHighlight-D5Gf8DLl.js → useHighlight-DPguaxfm.js} +8 -8
- data/lib/archsight/web/public/vue/usePanZoom-seQE1TPk.js +12 -0
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-CSCW83-M.js +1 -0
- data/lib/archsight/web/public/vue.html +5 -5
- metadata +72 -38
- data/lib/archsight/web/public/vue/ApiDocsPage-DoOxKjG0.js +0 -1
- data/lib/archsight/web/public/vue/DocPage-CfsC3CeQ.js +0 -1
- data/lib/archsight/web/public/vue/EditorPage-CsJA0q8n.js +0 -35
- data/lib/archsight/web/public/vue/ErrorPage-OJpJz9df.js +0 -2
- data/lib/archsight/web/public/vue/GraphView-CBq6oFRV.js +0 -1
- data/lib/archsight/web/public/vue/HomePage-BwgMLgVC.js +0 -2
- data/lib/archsight/web/public/vue/InstanceRouter-DQz-SsQq.js +0 -1
- data/lib/archsight/web/public/vue/InstanceRouter-jeavM6PW.css +0 -1
- data/lib/archsight/web/public/vue/KindList-4q0K9Cll.js +0 -1
- data/lib/archsight/web/public/vue/PageView-DzeGnvoS.js +0 -1
- data/lib/archsight/web/public/vue/QueryError-kg6Yf-pW.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +0 -1
- data/lib/archsight/web/public/vue/ResourceList-Df_0e0qt.js +0 -2
- data/lib/archsight/web/public/vue/SearchResults-CKgY6_wH.js +0 -1
- data/lib/archsight/web/public/vue/WikiPage-DoFJ0dbW.css +0 -1
- data/lib/archsight/web/public/vue/WikiPage-DrvCHwJ7.js +0 -13
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +0 -1
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +0 -1
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +0 -1
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +0 -1
- data/lib/archsight/web/public/vue/index-D0Q5GZRs.js +0 -3
- data/lib/archsight/web/public/vue/index-oF-iyZvk.css +0 -1
- data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +0 -1
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +0 -1
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +0 -1
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +0 -1
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +0 -1
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +0 -1
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +0 -1
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +0 -1
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +0 -1
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +0 -1
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +0 -1
|
@@ -4,26 +4,37 @@ require "erb"
|
|
|
4
4
|
|
|
5
5
|
module Archsight
|
|
6
6
|
module Helpers
|
|
7
|
-
# Replaces `![[
|
|
8
|
-
#
|
|
9
|
-
# loads and runs
|
|
7
|
+
# Replaces `![[Kind/Name]]` in rendered HTML by a placeholder for the content of that resource: the result list of
|
|
8
|
+
# a View, the result of an Analysis, the description of any other resource (a goal, a capability, ...). The
|
|
9
|
+
# frontend turns each placeholder into that content (it loads and runs it itself, so rendering a page never
|
|
10
|
+
# executes a query or a script):
|
|
10
11
|
#
|
|
11
12
|
# <div class="kind-embed" data-kind="View" data-name="View:Services"><a href="/kinds/View/instances/...">...</a></div>
|
|
12
13
|
#
|
|
13
14
|
# The link inside is what API and MCP consumers (and a browser without script) get. A reference that does not
|
|
14
|
-
# name an existing
|
|
15
|
+
# name an existing resource (or names a page) renders as a `broken-link` marker. Code (`<pre>`, `<code>`) is
|
|
16
|
+
# left alone.
|
|
15
17
|
class Embeds
|
|
16
|
-
|
|
18
|
+
# Embeds with live content; every other kind shows its description
|
|
19
|
+
LIVE_KINDS = %w[View Analysis].freeze
|
|
20
|
+
# Pages and menus are not resources with a description
|
|
21
|
+
NOT_EMBEDDABLE = %w[Page PageMenu].freeze
|
|
17
22
|
PATTERN = /!\[\[([^\]|]+)\]\]/
|
|
18
23
|
PARAGRAPH = %r{<p>\s*(#{PATTERN.source})\s*</p>}
|
|
19
24
|
CODE = %r{(<pre\b.*?</pre>|<code\b.*?</code>)}m
|
|
20
25
|
|
|
21
26
|
REASONS = {
|
|
22
|
-
kind: "
|
|
27
|
+
kind: "embed a resource as Kind/Name (a View, an Analysis or the description of any other resource); " \
|
|
28
|
+
"pages cannot be embedded",
|
|
23
29
|
missing: "no such resource",
|
|
24
30
|
ambiguous: "ambiguous reference"
|
|
25
31
|
}.freeze
|
|
26
32
|
|
|
33
|
+
# Whether `![[kind/...]]` can be embedded: a known kind that is not a page
|
|
34
|
+
def self.embeddable_kind?(kind)
|
|
35
|
+
!Archsight::Resources[kind].nil? && !NOT_EMBEDDABLE.include?(Archsight::Resources.canonical(kind))
|
|
36
|
+
end
|
|
37
|
+
|
|
27
38
|
def initialize(database, resolver: ResourceResolver.new(database))
|
|
28
39
|
@database = database
|
|
29
40
|
@resolver = resolver
|
|
@@ -66,7 +77,7 @@ module Archsight
|
|
|
66
77
|
# @return [Array(Array(String, String), Symbol)] kind and name, and the problem (nil if it is embeddable)
|
|
67
78
|
def check(reference)
|
|
68
79
|
kind, name = reference.split("/", 2)
|
|
69
|
-
return [[kind, name], :kind] unless name &&
|
|
80
|
+
return [[kind, name], :kind] unless name && self.class.embeddable_kind?(kind)
|
|
70
81
|
|
|
71
82
|
problem = @resolver.call(reference)
|
|
72
83
|
[[kind, name], problem.is_a?(Symbol) ? problem : nil]
|
|
@@ -32,6 +32,31 @@ module Archsight
|
|
|
32
32
|
matches.first
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# What an annotation that names another resource (`lifecycle/replacedBy`) refers to. A bare `Name` is a
|
|
36
|
+
# resource of the same kind as the annotated one, another kind needs `Kind/Name`.
|
|
37
|
+
# @param value [String] the annotation value
|
|
38
|
+
# @param kind [String] the kind of the annotated resource
|
|
39
|
+
# @return [Array(String, String), Symbol] `[kind, name]`, or `:missing`
|
|
40
|
+
def find_annotated(value, kind)
|
|
41
|
+
reference = value.to_s.strip
|
|
42
|
+
found = find(reference.include?("/") ? reference : "#{kind}/#{reference}")
|
|
43
|
+
found.is_a?(Array) ? found : :missing
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Where the annotations of a resource that name another resource point to:
|
|
47
|
+
# `{ key => page path }`, `nil` for a reference that does not resolve.
|
|
48
|
+
# @param kind [String] the kind of the annotated resource, which knows the annotation formats
|
|
49
|
+
# @param annotations [Hash]
|
|
50
|
+
def annotation_links(kind, annotations)
|
|
51
|
+
klass = Archsight::Resources[kind]
|
|
52
|
+
annotations.each_with_object({}) do |(key, value), links|
|
|
53
|
+
next unless klass&.annotation_format(key) == :resource
|
|
54
|
+
|
|
55
|
+
found = find_annotated(value, kind)
|
|
56
|
+
links[key] = found.is_a?(Array) ? path(*found) : nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
35
60
|
private
|
|
36
61
|
|
|
37
62
|
def in_kind(kind, name)
|
|
@@ -38,7 +38,9 @@ class Archsight::Import::Executor
|
|
|
38
38
|
# @param output [IO] Output stream for progress (default: $stdout)
|
|
39
39
|
# @param filter [String, nil] Regex pattern to match import names
|
|
40
40
|
# @param force [Boolean] Whether to bypass cache and re-run all imports
|
|
41
|
-
|
|
41
|
+
# @param deps [Boolean] With a filter, also run the imports the matched ones depend on (false: only the matched ones)
|
|
42
|
+
def initialize(database:, resources_dir:, verbose: false, max_concurrent: MAX_CONCURRENT, output: $stdout, filter: nil, force: false,
|
|
43
|
+
deps: true)
|
|
42
44
|
@database = database
|
|
43
45
|
@resources_dir = resources_dir
|
|
44
46
|
@verbose = verbose
|
|
@@ -47,7 +49,10 @@ class Archsight::Import::Executor
|
|
|
47
49
|
@filter = filter
|
|
48
50
|
@filter_regex = Regexp.new(filter, Regexp::IGNORECASE) if filter
|
|
49
51
|
@force = force
|
|
52
|
+
@deps = deps
|
|
50
53
|
@executed_this_run = Set.new
|
|
54
|
+
@ran_this_run = Set.new
|
|
55
|
+
@handler_problems = {}
|
|
51
56
|
@iteration = 0
|
|
52
57
|
@mutex = Mutex.new
|
|
53
58
|
end
|
|
@@ -154,18 +159,24 @@ class Archsight::Import::Executor
|
|
|
154
159
|
sorted = topological_sort(filtered_imports)
|
|
155
160
|
|
|
156
161
|
@output.puts "Filter: #{@filter}" if @filter
|
|
162
|
+
@output.puts "Dependencies: not run (--no-deps)" if only_matches?
|
|
157
163
|
|
|
158
164
|
sorted.each_with_index do |imp, idx|
|
|
159
165
|
enabled = import_enabled?(imp)
|
|
160
166
|
deps = import_dependency_names(imp)
|
|
161
167
|
deps_str = deps.empty? ? "(no dependencies)" : "depends on: #{deps.join(", ")}"
|
|
162
168
|
enabled_str = enabled ? "" : " [DISABLED]"
|
|
163
|
-
@output.puts " #{idx + 1}. #{imp.name}#{enabled_str} #{deps_str}"
|
|
169
|
+
@output.puts " #{idx + 1}. #{imp.name}#{enabled_str} #{deps_str}#{unmatched_patterns_note(imp)}"
|
|
164
170
|
end
|
|
165
171
|
|
|
166
172
|
sorted
|
|
167
173
|
end
|
|
168
174
|
|
|
175
|
+
# Names of the imports that executed in this run (not served from the cache)
|
|
176
|
+
def ran_imports
|
|
177
|
+
@mutex.synchronize { @ran_this_run.to_a.sort }
|
|
178
|
+
end
|
|
179
|
+
|
|
169
180
|
private
|
|
170
181
|
|
|
171
182
|
# Set up signal handlers for graceful shutdown
|
|
@@ -222,7 +233,13 @@ class Archsight::Import::Executor
|
|
|
222
233
|
@imports_to_run.include?(import.name)
|
|
223
234
|
end
|
|
224
235
|
|
|
225
|
-
#
|
|
236
|
+
# Whether only the imports matching the filter run (`-f` with `--no-deps`)
|
|
237
|
+
def only_matches?
|
|
238
|
+
!@deps && !@filter_regex.nil?
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Compute the full set of imports to run: filtered imports + all their dependencies (only the filtered ones with
|
|
242
|
+
# `--no-deps`)
|
|
226
243
|
def compute_imports_to_run
|
|
227
244
|
all_imports = database.instances_by_kind("Import")&.values || []
|
|
228
245
|
imports_by_name = all_imports.to_h { |imp| [imp.name, imp] }
|
|
@@ -230,6 +247,7 @@ class Archsight::Import::Executor
|
|
|
230
247
|
# Start with imports matching the filter
|
|
231
248
|
matching = all_imports.select { |imp| @filter_regex.match?(imp.name) }.map(&:name)
|
|
232
249
|
to_run = Set.new(matching)
|
|
250
|
+
return to_run unless @deps
|
|
233
251
|
|
|
234
252
|
# Recursively add dependencies
|
|
235
253
|
queue = matching.dup
|
|
@@ -249,9 +267,12 @@ class Archsight::Import::Executor
|
|
|
249
267
|
to_run
|
|
250
268
|
end
|
|
251
269
|
|
|
252
|
-
# Check if all dependencies of an import are satisfied (executed successfully this run)
|
|
270
|
+
# Check if all dependencies of an import are satisfied (executed successfully this run).
|
|
271
|
+
# A parent that can never run (no handler or disabled, e.g. the repository was
|
|
272
|
+
# deleted upstream and only its generated `generates` entry is left) is not
|
|
273
|
+
# waited for, otherwise its children would deadlock the run.
|
|
253
274
|
def dependencies_satisfied?(import)
|
|
254
|
-
dep_names =
|
|
275
|
+
dep_names = runnable_dependency_names(import)
|
|
255
276
|
return true if dep_names.empty?
|
|
256
277
|
|
|
257
278
|
@mutex.synchronize do
|
|
@@ -261,30 +282,81 @@ class Archsight::Import::Executor
|
|
|
261
282
|
end
|
|
262
283
|
end
|
|
263
284
|
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
|
|
285
|
+
# With `--no-deps` a dependency outside the run is not waited for: it does not run, so the import uses the model
|
|
286
|
+
# as it is
|
|
287
|
+
def runnable_dependency_names(import)
|
|
288
|
+
imports_by_name = (database.instances_by_kind("Import")&.values || []).to_h { |imp| [imp.name, imp] }
|
|
289
|
+
import_dependency_names(import).select do |dep_name|
|
|
290
|
+
parent = imports_by_name[dep_name]
|
|
291
|
+
parent && import_enabled?(parent) && (!only_matches? || import_matches_filter?(parent))
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Get dependency names for an import: the parents that generate it (the inverse of `generates` - if Import A
|
|
296
|
+
# generates Import B, then B depends on A) and the imports it declares in `import/dependsOn`
|
|
267
297
|
def import_dependency_names(import)
|
|
268
|
-
# Find all imports that have this import in their generates.imports
|
|
269
298
|
all_imports = database.instances_by_kind("Import")&.values || []
|
|
270
299
|
parent_imports = all_imports.select do |parent|
|
|
271
|
-
|
|
272
|
-
|
|
300
|
+
generated_import_names(parent).include?(import.name)
|
|
301
|
+
end
|
|
302
|
+
(parent_imports.map(&:name) + declared_dependency_names(import, all_imports)).uniq
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# The imports `import/dependsOn` names or matches. For a pattern this also includes the imports that may still
|
|
306
|
+
# generate a matching import in this run: one whose last run generated a match, and one that never ran but whose
|
|
307
|
+
# handler generates imports. So `Import:Repo:*` waits for the GitHub import and then for the repositories it found.
|
|
308
|
+
def declared_dependency_names(import, all_imports = database.instances_by_kind("Import")&.values || [])
|
|
309
|
+
entries = Archsight::Import.declared_dependencies(import)
|
|
310
|
+
return [] if entries.empty?
|
|
311
|
+
|
|
312
|
+
others = all_imports.reject { |imp| imp.name == import.name }
|
|
313
|
+
entries.flat_map do |entry|
|
|
314
|
+
matches = others.select { |imp| Archsight::Import.dependency_match?(entry, imp.name) }
|
|
315
|
+
matches += others.select { |imp| may_generate_match?(imp, entry) } if Archsight::Import.pattern?(entry)
|
|
316
|
+
matches.map(&:name)
|
|
317
|
+
end.uniq
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Whether an import generated a match of the pattern in its last run, or may do so on its first one
|
|
321
|
+
def may_generate_match?(import, pattern)
|
|
322
|
+
generated = generated_import_names(import)
|
|
323
|
+
return generated.any? { |name| Archsight::Import.dependency_match?(pattern, name) } if generated.any?
|
|
324
|
+
|
|
325
|
+
handler = Archsight::Import::Registry[import.annotations["import/handler"]]
|
|
326
|
+
!import.annotations["generated/at"] && handler.respond_to?(:generates_imports?) && handler.generates_imports?
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def generated_import_names(import)
|
|
330
|
+
(import.relations(:generates, :imports) || []).map { |gen| gen.is_a?(String) ? gen : gen.name }
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# For the plan: patterns of `import/dependsOn` that match no import yet
|
|
334
|
+
def unmatched_patterns_note(import)
|
|
335
|
+
all_names = (database.instances_by_kind("Import")&.keys || []) - [import.name]
|
|
336
|
+
unmatched = Archsight::Import.declared_dependencies(import).select do |entry|
|
|
337
|
+
Archsight::Import.pattern?(entry) && all_names.none? { |name| Archsight::Import.dependency_match?(entry, name) }
|
|
273
338
|
end
|
|
274
|
-
|
|
339
|
+
unmatched.empty? ? "" : " (#{unmatched.join(", ")} matches no import yet)"
|
|
275
340
|
end
|
|
276
341
|
|
|
277
|
-
# Reload database without verbose output
|
|
342
|
+
# Reload database without verbose output, and load the handlers the imports declare (`import/handlerPath`)
|
|
278
343
|
def reload_database_quietly!
|
|
279
344
|
original_verbose = database.verbose
|
|
280
345
|
database.verbose = false
|
|
281
346
|
database.reload!
|
|
347
|
+
load_local_handlers
|
|
282
348
|
# Reset filter cache since new imports may have been discovered
|
|
283
349
|
@imports_to_run = nil if @filter_regex
|
|
284
350
|
ensure
|
|
285
351
|
database.verbose = original_verbose
|
|
286
352
|
end
|
|
287
353
|
|
|
354
|
+
# A handler file that does not load fails the imports that use it, not the whole run
|
|
355
|
+
def load_local_handlers
|
|
356
|
+
problems = Archsight::Import::HandlerLoader.load(database, resources_dir: resources_dir)
|
|
357
|
+
@handler_problems = problems.to_h { |problem| [problem.import.name, problem.message] }
|
|
358
|
+
end
|
|
359
|
+
|
|
288
360
|
# Reload database and update progress total for newly discovered imports
|
|
289
361
|
def reload_and_update_total!
|
|
290
362
|
reload_database_quietly!
|
|
@@ -315,6 +387,8 @@ class Archsight::Import::Executor
|
|
|
315
387
|
result = execute_single_import(imp, slot_progress)
|
|
316
388
|
if result == :cached
|
|
317
389
|
slot_progress.complete("Cached")
|
|
390
|
+
elsif result.is_a?(String)
|
|
391
|
+
slot_progress.complete(result)
|
|
318
392
|
else
|
|
319
393
|
@mutex.synchronize { @total_executed += 1 }
|
|
320
394
|
slot_progress.complete("Done")
|
|
@@ -339,7 +413,7 @@ class Archsight::Import::Executor
|
|
|
339
413
|
end
|
|
340
414
|
|
|
341
415
|
# Execute a single import (called from thread)
|
|
342
|
-
# @return [Symbol] :executed, :cached, or raises on error
|
|
416
|
+
# @return [Symbol, String] :executed, :cached, a message for a skipped import, or raises on error
|
|
343
417
|
def execute_single_import(import, import_progress)
|
|
344
418
|
# Check if import is still fresh based on generated/at and import/cacheTime
|
|
345
419
|
if import_fresh?(import)
|
|
@@ -347,6 +421,18 @@ class Archsight::Import::Executor
|
|
|
347
421
|
return :cached
|
|
348
422
|
end
|
|
349
423
|
|
|
424
|
+
problem = @handler_problems[import.name]
|
|
425
|
+
raise Archsight::Import::UnknownHandlerError, problem if problem
|
|
426
|
+
|
|
427
|
+
missing = missing_environment(import)
|
|
428
|
+
if missing.any?
|
|
429
|
+
message = "missing environment variable#{"s" if missing.length > 1} #{missing.join(", ")} (import/config/requiredEnv)"
|
|
430
|
+
raise Archsight::Import::ImportError, message unless import.annotations["import/config/optional"] == "true"
|
|
431
|
+
|
|
432
|
+
import_progress.warn("Skipped: #{message}")
|
|
433
|
+
return "Skipped"
|
|
434
|
+
end
|
|
435
|
+
|
|
350
436
|
# Execute the import
|
|
351
437
|
handler_class = Archsight::Import::Registry.handler_for(import)
|
|
352
438
|
handler = handler_class.new(
|
|
@@ -357,12 +443,30 @@ class Archsight::Import::Executor
|
|
|
357
443
|
shared_writer: @shared_writer
|
|
358
444
|
)
|
|
359
445
|
handler.execute
|
|
446
|
+
remove_stale_outputs(import, handler.owned_outputs)
|
|
447
|
+
@mutex.synchronize { @ran_this_run.add(import.name) }
|
|
360
448
|
:executed
|
|
361
449
|
end
|
|
362
450
|
|
|
451
|
+
# Variables of `import/config/requiredEnv` that are not set (or blank)
|
|
452
|
+
def missing_environment(import)
|
|
453
|
+
names = import.annotations["import/config/requiredEnv"].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
454
|
+
names.select { |name| ENV[name].to_s.strip.empty? }
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Delete the files an earlier run of the import wrote (`generated/outputs`) that this run did not write again
|
|
458
|
+
def remove_stale_outputs(import, written)
|
|
459
|
+
previous = import.annotations["generated/outputs"].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
460
|
+
(previous - written).each do |rel_path|
|
|
461
|
+
path = Archsight::Import.resources_path(resources_dir, rel_path)
|
|
462
|
+
FileUtils.rm_f(path) if path && File.file?(path)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
363
466
|
# Check if import is still fresh (cached)
|
|
364
|
-
# Uses generated/at annotation and import/cacheTime to determine freshness
|
|
365
|
-
#
|
|
467
|
+
# Uses generated/at annotation and import/cacheTime to determine freshness. The cache is also invalid when the
|
|
468
|
+
# output file or a recorded output (`generated/outputs`) is missing, the configuration or the files of
|
|
469
|
+
# `import/config/inputs` changed, or an import it declares in `import/dependsOn` executed in this run.
|
|
366
470
|
def import_fresh?(import)
|
|
367
471
|
# Honor force flag - always re-run when forced
|
|
368
472
|
return false if @force
|
|
@@ -377,6 +481,11 @@ class Archsight::Import::Executor
|
|
|
377
481
|
return false unless File.exist?(full_path)
|
|
378
482
|
end
|
|
379
483
|
|
|
484
|
+
return false unless recorded_outputs_exist?(import)
|
|
485
|
+
return false if dependency_ran?(import)
|
|
486
|
+
return false if import.annotations["import/config/inputs"] &&
|
|
487
|
+
import.annotations["generated/inputsHash"] != Archsight::Import.inputs_hash(import, resources_dir)
|
|
488
|
+
|
|
380
489
|
generated_at = import.annotations["generated/at"]
|
|
381
490
|
return false if generated_at.nil? || generated_at.empty?
|
|
382
491
|
|
|
@@ -399,6 +508,20 @@ class Archsight::Import::Executor
|
|
|
399
508
|
false
|
|
400
509
|
end
|
|
401
510
|
|
|
511
|
+
def recorded_outputs_exist?(import)
|
|
512
|
+
import.annotations["generated/outputs"].to_s.split(",").map(&:strip).reject(&:empty?).all? do |rel_path|
|
|
513
|
+
path = Archsight::Import.resources_path(resources_dir, rel_path)
|
|
514
|
+
path && File.exist?(path)
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Whether an import declared in `import/dependsOn` executed (not just came from the cache) in this run. The
|
|
519
|
+
# generated imports of a parent are not affected: they keep their own cache time when the parent runs.
|
|
520
|
+
def dependency_ran?(import)
|
|
521
|
+
names = declared_dependency_names(import)
|
|
522
|
+
@mutex.synchronize { names.any? { |name| @ran_this_run.include?(name) } }
|
|
523
|
+
end
|
|
524
|
+
|
|
402
525
|
def parse_duration(str)
|
|
403
526
|
return nil if str.nil?
|
|
404
527
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "json"
|
|
6
|
+
require "pathname"
|
|
6
7
|
require "yaml"
|
|
7
8
|
require "archsight/import"
|
|
8
9
|
require_relative "progress"
|
|
@@ -12,7 +13,14 @@ require_relative "progress"
|
|
|
12
13
|
# Subclasses must implement the #execute method to perform the actual import.
|
|
13
14
|
# Use the helper methods to read configuration, validate environment, and write output.
|
|
14
15
|
class Archsight::Import::Handler
|
|
15
|
-
attr_reader :import_resource, :database, :resources_dir, :progress, :shared_writer
|
|
16
|
+
attr_reader :import_resource, :database, :resources_dir, :progress, :shared_writer, :owned_outputs
|
|
17
|
+
|
|
18
|
+
# Whether the handler generates child imports (`import_yaml`). An import that depends on a pattern
|
|
19
|
+
# (`import/dependsOn: Import:Repo:*`) waits for such a handler on its first run, before it has a `generates` record
|
|
20
|
+
# that tells which imports it creates. Override in handlers that generate imports.
|
|
21
|
+
def self.generates_imports?
|
|
22
|
+
false
|
|
23
|
+
end
|
|
16
24
|
|
|
17
25
|
# @param import_resource [Archsight::Resources::Import] The import resource to execute
|
|
18
26
|
# @param database [Archsight::Database] The database instance
|
|
@@ -26,6 +34,15 @@ class Archsight::Import::Handler
|
|
|
26
34
|
@progress = progress || Archsight::Import::Progress.new
|
|
27
35
|
@shared_writer = shared_writer
|
|
28
36
|
@tracked_resources = []
|
|
37
|
+
@owned_outputs = []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The directory the git-based handlers clone repositories into (`import/config/cacheRoot`, else `import.cache_root`
|
|
41
|
+
# of the user configuration or ARCHSIGHT_IMPORT_CACHE_ROOT, else ~/.cache/archsight/git), so an importer of the
|
|
42
|
+
# resources repository finds the same clones
|
|
43
|
+
# @return [String] absolute path
|
|
44
|
+
def cache_root
|
|
45
|
+
File.expand_path(Archsight::Import.cache_root(config("cacheRoot")))
|
|
29
46
|
end
|
|
30
47
|
|
|
31
48
|
# Execute the import. Must be implemented by subclasses.
|
|
@@ -74,23 +91,55 @@ class Archsight::Import::Handler
|
|
|
74
91
|
end
|
|
75
92
|
|
|
76
93
|
# Generate a marker Import for this handler with generated/at timestamp and config hash
|
|
77
|
-
# Used for caching - call at end of execute() to persist the execution timestamp
|
|
94
|
+
# Used for caching - call at end of execute() to persist the execution timestamp. It also records the hash of the
|
|
95
|
+
# files in `import/config/inputs` and the files written with #write_file, so call it after writing them.
|
|
78
96
|
# @return [Hash] Import resource hash ready for YAML serialization
|
|
79
97
|
def self_marker
|
|
98
|
+
annotations = {
|
|
99
|
+
"generated/at" => Time.now.utc.iso8601,
|
|
100
|
+
"generated/configHash" => compute_config_hash
|
|
101
|
+
}
|
|
102
|
+
inputs_hash = Archsight::Import.inputs_hash(import_resource, resources_dir)
|
|
103
|
+
annotations["generated/inputsHash"] = inputs_hash if inputs_hash
|
|
104
|
+
annotations["generated/outputs"] = owned_outputs.uniq.sort.join(",") if owned_outputs.any?
|
|
80
105
|
{
|
|
81
106
|
"apiVersion" => "architecture/v1alpha1",
|
|
82
107
|
"kind" => "Import",
|
|
83
108
|
"metadata" => {
|
|
84
109
|
"name" => import_resource.name,
|
|
85
|
-
"annotations" =>
|
|
86
|
-
"generated/at" => Time.now.utc.iso8601,
|
|
87
|
-
"generated/configHash" => compute_config_hash
|
|
88
|
-
}
|
|
110
|
+
"annotations" => annotations
|
|
89
111
|
},
|
|
90
112
|
"spec" => {}
|
|
91
113
|
}
|
|
92
114
|
end
|
|
93
115
|
|
|
116
|
+
# Write a whole file that belongs to this import alone, such as a page (Markdown) next to the generated YAML.
|
|
117
|
+
# The file is recorded in the self marker (`generated/outputs`): the cache is invalid while it is missing, and a
|
|
118
|
+
# file of an earlier run that is not written again is deleted.
|
|
119
|
+
# @param rel_path [String] path relative to the resources directory
|
|
120
|
+
# @param content [String]
|
|
121
|
+
# @return [String] the absolute path
|
|
122
|
+
# @raise [ArgumentError] for a path outside the resources directory
|
|
123
|
+
def write_file(rel_path, content)
|
|
124
|
+
full_path = Archsight::Import.resources_path(resources_dir, rel_path)
|
|
125
|
+
raise ArgumentError, "#{rel_path} is outside the resources directory" unless full_path
|
|
126
|
+
|
|
127
|
+
FileUtils.mkdir_p(File.dirname(full_path))
|
|
128
|
+
File.write(full_path, content)
|
|
129
|
+
record_output(rel_path)
|
|
130
|
+
full_path
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Record a file this import wrote by other means (a command, a library) as its own output (see #write_file)
|
|
134
|
+
# @param rel_path [String] path relative to the resources directory
|
|
135
|
+
# @raise [ArgumentError] for a path outside the resources directory
|
|
136
|
+
def record_output(rel_path)
|
|
137
|
+
full_path = Archsight::Import.resources_path(resources_dir, rel_path)
|
|
138
|
+
raise ArgumentError, "#{rel_path} is outside the resources directory" unless full_path
|
|
139
|
+
|
|
140
|
+
@owned_outputs << Pathname.new(full_path).relative_path_from(File.expand_path(resources_dir)).to_s
|
|
141
|
+
end
|
|
142
|
+
|
|
94
143
|
# Write YAML content to the output path
|
|
95
144
|
# @param content [String] YAML content to write
|
|
96
145
|
# @param filename [String, nil] Output filename (overrides import/outputPath filename)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "archsight/import"
|
|
4
|
+
|
|
5
|
+
# Loads import handlers: the built-in ones shipped with the gem, and the handlers of the resources repository that an
|
|
6
|
+
# Import declares with `import/handlerPath` (a Ruby file relative to the resources directory, like
|
|
7
|
+
# `import/outputPath`). Used by `archsight import`, `lint` and `web`, so every command knows the same handler names.
|
|
8
|
+
module Archsight::Import::HandlerLoader
|
|
9
|
+
BUILTIN_DIR = File.expand_path("handlers", __dir__)
|
|
10
|
+
|
|
11
|
+
# A problem with a local handler file, reported by the linter and by `archsight import`
|
|
12
|
+
Problem = Struct.new(:import, :message) do
|
|
13
|
+
def to_s
|
|
14
|
+
"#{import.path_ref}: Import '#{import.name}' #{message}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Require the built-in handlers. Idempotent; registers them again when a test cleared the registry.
|
|
20
|
+
def load_builtin!
|
|
21
|
+
Dir.glob(File.join(BUILTIN_DIR, "*.rb")).each { |file| require file }
|
|
22
|
+
Archsight::Import::Registry.restore_builtin!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Load the built-in handlers and every handler file the Imports of the database declare
|
|
26
|
+
# @param database [Archsight::Database]
|
|
27
|
+
# @param resources_dir [String] directory `import/handlerPath` is relative to
|
|
28
|
+
# @return [Array<Problem>] files that are missing, outside the resources directory, fail to load or do not register
|
|
29
|
+
# the handler named in `import/handler`
|
|
30
|
+
def load(database, resources_dir: database.path)
|
|
31
|
+
load_builtin!
|
|
32
|
+
imports = database.instances_by_kind("Import")&.values || []
|
|
33
|
+
imports.select { |imp| imp.annotations["import/handlerPath"] }.flat_map { |imp| load_for(imp, resources_dir) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Absolute path of the handler file of an Import, nil when it points outside the resources directory
|
|
37
|
+
def handler_file(import, resources_dir)
|
|
38
|
+
Archsight::Import.resources_path(resources_dir, import.annotations["import/handlerPath"])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def load_for(import, resources_dir)
|
|
44
|
+
file = handler_file(import, resources_dir)
|
|
45
|
+
return [Problem.new(import, "has import/handlerPath '#{import.annotations["import/handlerPath"]}' outside the resources directory")] unless file
|
|
46
|
+
return [Problem.new(import, "has import/handlerPath '#{import.annotations["import/handlerPath"]}' that does not exist")] unless File.file?(file)
|
|
47
|
+
|
|
48
|
+
problem = require_file(import, file)
|
|
49
|
+
return [problem] if problem
|
|
50
|
+
|
|
51
|
+
name = import.annotations["import/handler"]
|
|
52
|
+
return [] if name.nil? || Archsight::Import::Registry[name]
|
|
53
|
+
|
|
54
|
+
[Problem.new(import, "loads #{import.annotations["import/handlerPath"]}, which does not register the handler '#{name}'")]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def require_file(import, file)
|
|
58
|
+
require file
|
|
59
|
+
nil
|
|
60
|
+
rescue ScriptError, StandardError => e
|
|
61
|
+
Problem.new(import, "cannot load #{import.annotations["import/handlerPath"]}: #{e.class}: #{e.message.lines.first&.strip}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -6,18 +6,41 @@
|
|
|
6
6
|
# the current API listing, so a renamed, moved, or deleted repo doesn't
|
|
7
7
|
# leave a stale clone behind that a later importer could still resolve
|
|
8
8
|
# against under its old name.
|
|
9
|
+
#
|
|
10
|
+
# A listing can be incomplete (throttled or empty API pages) and clones may be
|
|
11
|
+
# in progress, so pruning is conservative: entries touched within MIN_AGE are
|
|
12
|
+
# kept, and nothing is removed when it would wipe out most of the cache.
|
|
9
13
|
module Archsight::Import::Handlers::CachePruner
|
|
14
|
+
MIN_AGE = 3600 # seconds; younger directories may be a clone in progress
|
|
15
|
+
MAX_STALE_ABSOLUTE = 10 # always allowed, whatever the cache size
|
|
16
|
+
MAX_STALE_FRACTION = 0.2 # larger share of the cache is treated as a bad listing
|
|
17
|
+
|
|
10
18
|
def prune_stale_cache_entries(target_dir, current_names)
|
|
11
19
|
return unless File.directory?(target_dir)
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
next unless File.directory?(stale_path)
|
|
21
|
+
known = current_names.to_set
|
|
22
|
+
dirs = Dir.children(target_dir).select { |entry| File.directory?(File.join(target_dir, entry)) }
|
|
23
|
+
stale = dirs.reject { |entry| known.include?(entry) }
|
|
24
|
+
return if stale.empty?
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
limit = [MAX_STALE_ABSOLUTE, (dirs.size * MAX_STALE_FRACTION).floor].max
|
|
27
|
+
if stale.size > limit
|
|
28
|
+
progress.warn("Not pruning #{stale.size} of #{dirs.size} cache entries in #{target_dir}: " \
|
|
29
|
+
"the project listing looks incomplete")
|
|
30
|
+
return
|
|
21
31
|
end
|
|
32
|
+
|
|
33
|
+
stale.each { |entry| prune_cache_entry(File.join(target_dir, entry), entry) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def prune_cache_entry(path, name)
|
|
39
|
+
return if Time.now - File.mtime(path) < MIN_AGE
|
|
40
|
+
|
|
41
|
+
progress.warn("Pruning stale cache entry: #{name} (repo renamed, moved, or removed)")
|
|
42
|
+
FileUtils.rm_rf(path)
|
|
43
|
+
rescue Errno::ENOENT
|
|
44
|
+
nil
|
|
22
45
|
end
|
|
23
46
|
end
|