moonraker-bosun 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/LICENSE +21 -0
  4. data/README.ja.md +467 -0
  5. data/README.md +124 -0
  6. data/docs/design.md +358 -0
  7. data/docs/moonraker-api.md +108 -0
  8. data/exe/bosun +7 -0
  9. data/lib/moonraker/bosun/cli.rb +64 -0
  10. data/lib/moonraker/bosun/client.rb +147 -0
  11. data/lib/moonraker/bosun/commands/add.rb +38 -0
  12. data/lib/moonraker/bosun/commands/audit.rb +48 -0
  13. data/lib/moonraker/bosun/commands/backup.rb +135 -0
  14. data/lib/moonraker/bosun/commands/base.rb +61 -0
  15. data/lib/moonraker/bosun/commands/diff.rb +29 -0
  16. data/lib/moonraker/bosun/commands/export.rb +103 -0
  17. data/lib/moonraker/bosun/commands/fetching.rb +76 -0
  18. data/lib/moonraker/bosun/commands/forget.rb +26 -0
  19. data/lib/moonraker/bosun/commands/get.rb +57 -0
  20. data/lib/moonraker/bosun/commands/ls.rb +43 -0
  21. data/lib/moonraker/bosun/commands/pull.rb +52 -0
  22. data/lib/moonraker/bosun/commands/push.rb +136 -0
  23. data/lib/moonraker/bosun/commands/removing.rb +38 -0
  24. data/lib/moonraker/bosun/commands/rm.rb +105 -0
  25. data/lib/moonraker/bosun/commands/roots.rb +31 -0
  26. data/lib/moonraker/bosun/commands/status.rb +22 -0
  27. data/lib/moonraker/bosun/commands/status_report.rb +75 -0
  28. data/lib/moonraker/bosun/config.rb +31 -0
  29. data/lib/moonraker/bosun/context.rb +26 -0
  30. data/lib/moonraker/bosun/error.rb +8 -0
  31. data/lib/moonraker/bosun/file_rules.rb +37 -0
  32. data/lib/moonraker/bosun/git.rb +139 -0
  33. data/lib/moonraker/bosun/host_tracking.rb +214 -0
  34. data/lib/moonraker/bosun/messages/catalog.rb +837 -0
  35. data/lib/moonraker/bosun/messages.rb +42 -0
  36. data/lib/moonraker/bosun/push_plan.rb +89 -0
  37. data/lib/moonraker/bosun/sync_status.rb +130 -0
  38. data/lib/moonraker/bosun/version.rb +7 -0
  39. data/lib/moonraker/bosun.rb +31 -0
  40. metadata +86 -0
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ module Commands
6
+ class Get < Base
7
+ def run(args)
8
+ root = FileRules::CONFIG_ROOT
9
+ args = parse_options(args) do |o|
10
+ o.on("-r", "--root ROOT", t(:"common.root_option")) { |v| root = v }
11
+ end
12
+ return 0 unless args
13
+ raise Error, t(:"get.no_files") if args.empty?
14
+
15
+ targets = args.map do |path|
16
+ validate_path!(path)
17
+ [path, File.basename(path)]
18
+ end
19
+ check_destinations!(targets.map(&:last))
20
+
21
+ # 途中で失敗したときに一部のファイルだけが残らないよう、すべて取得してから書き込む
22
+ contents = targets.map { |path, name| [name, download(root, path)] }
23
+ contents.each do |name, body|
24
+ File.open(File.join(@context.cwd, name), File::WRONLY | File::CREAT | File::EXCL | File::BINARY) do |f|
25
+ f.write(body)
26
+ end
27
+ out.puts t(:"common.fetched", path: name)
28
+ end
29
+ 0
30
+ end
31
+
32
+ private
33
+
34
+ def check_destinations!(names)
35
+ duplicated = names.tally.select { |_, count| count > 1 }.keys
36
+ raise Error, t(:"get.duplicated", names: duplicated.join(", ")) unless duplicated.empty?
37
+
38
+ existing = names.select do |name|
39
+ path = File.join(@context.cwd, name)
40
+ File.exist?(path) || File.symlink?(path)
41
+ end
42
+ return if existing.empty?
43
+
44
+ raise Error, t(:"get.exists", names: existing.join(", "))
45
+ end
46
+
47
+ def download(root, path)
48
+ client.download(root, path)
49
+ rescue Client::APIError => e
50
+ raise unless e.code == 404
51
+
52
+ raise Error, t(:"common.file_not_on_host", path: "#{root}/#{path}")
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ module Commands
6
+ class Ls < Base
7
+ def run(args)
8
+ root = FileRules::CONFIG_ROOT
9
+ all = false
10
+ long = false
11
+ args = parse_options(args) do |o|
12
+ o.on("-r", "--root ROOT", t(:"common.root_option")) { |v| root = v }
13
+ o.on("-a", "--all", t(:"ls.all_option")) { all = true }
14
+ o.on("-l", "--long", t(:"ls.long_option")) { long = true }
15
+ end
16
+ return 0 unless args
17
+
18
+ check_no_arguments!(args)
19
+
20
+ entries = client.list_files(root).sort_by(&:path).filter_map do |info|
21
+ reason = FileRules.exclusion(root, info)
22
+ [info, reason] if all || reason.nil?
23
+ end
24
+ long ? print_long(entries) : entries.map(&:first).each { |info| out.puts info.path }
25
+ 0
26
+ end
27
+
28
+ private
29
+
30
+ def print_long(entries)
31
+ size_width = entries.map { |info, _| info.size.to_s.size }.max
32
+ entries.each do |info, reason|
33
+ line = format("%-2s %*d %s %s",
34
+ info.permissions, size_width, info.size,
35
+ Time.at(info.modified).strftime("%Y-%m-%d %H:%M:%S"), info.path)
36
+ line += " #{t(:"ls.excluded", reason: FileRules.reason_text(reason))}" if reason
37
+ out.puts line
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ module Commands
6
+ class Pull < Base
7
+ include Fetching
8
+
9
+ def run(args)
10
+ args = parse_options(args)
11
+ return 0 unless args
12
+
13
+ check_no_arguments!(args)
14
+
15
+ tip = tracking.tip
16
+ raise Error, t(:"common.no_tracked_files") unless tip
17
+
18
+ tracked = @context.git.ls_tree_paths(tip, HostTracking::CONFIG_DIR).map { |path| HostTracking.host_path(path) }
19
+ tracking.check_workspace!(tracked.map { |path| HostTracking.repo_path(path) })
20
+
21
+ infos = list_config_files
22
+ deleted = tracked.reject { |path| infos.key?(path) }
23
+ read_only = tracked.select { |path| infos.key?(path) && !infos[path].writable? }
24
+ targets = tracked - deleted - read_only
25
+ untracked = infos.keys.sort.select do |path|
26
+ !tracked.include?(path) && FileRules.exclusion(FileRules::CONFIG_ROOT, infos[path]).nil?
27
+ end
28
+
29
+ fetched = fetch(targets, infos)
30
+ # マージで衝突した場合でも確認できるよう、マージの前に表示する
31
+ warn_skipped(:host_deleted, deleted)
32
+ warn_skipped(:host_read_only, read_only)
33
+ warn_skipped(:untracked, untracked)
34
+ commit_and_merge("pull", fetched)
35
+ 0
36
+ end
37
+
38
+ private
39
+
40
+ def warn_skipped(state, paths)
41
+ return if paths.empty?
42
+
43
+ out.puts
44
+ out.puts t(:"common.not_fetched", label: SyncStatus.label(state))
45
+ paths.each { |path| out.puts " #{path}" }
46
+ print_guide(SyncStatus.guide(state))
47
+ out.puts
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ module Commands
6
+ class Push < Base
7
+ include Fetching
8
+
9
+ def run(args)
10
+ dry_run = false
11
+ args = parse_options(args) do |o|
12
+ o.on("-n", "--dry-run", t(:"push.dry_run_option")) { dry_run = true }
13
+ end
14
+ return 0 unless args
15
+
16
+ check_no_arguments!(args)
17
+
18
+ git = @context.git
19
+ raise Error, t(:"common.no_tracked_files") unless tracking.tip
20
+
21
+ # 反映の後にhost-trackingをマージするので、addやpullと同じ状態であることを確認する
22
+ tracking.check_workspace!([HostTracking::CONFIG_DIR])
23
+ check_not_printing!
24
+
25
+ plan = PushPlan.new(SyncStatus.new(git: git, client: client, tracking: tracking))
26
+ print_plan(plan)
27
+ unless plan.ok?
28
+ out.puts
29
+ out.puts t(:"push.aborted")
30
+ return 1
31
+ end
32
+ return 0 if plan.actions.empty?
33
+
34
+ if dry_run
35
+ out.puts
36
+ out.puts t(:"push.dry_run")
37
+ return 0
38
+ end
39
+
40
+ out.puts
41
+ execute(plan.actions)
42
+ out.puts
43
+ out.puts t(:"push.restart")
44
+ 0
45
+ end
46
+
47
+ private
48
+
49
+ def execute(actions)
50
+ check_unchanged_since_plan!(actions)
51
+
52
+ uploaded = []
53
+ begin
54
+ actions.each do |action|
55
+ client.upload(FileRules::CONFIG_ROOT, action.path, @context.git.cat_blob(action.blob))
56
+ uploaded << action
57
+ out.puts t(:"push.uploaded", path: action.path)
58
+ end
59
+ rescue StandardError, SignalException
60
+ # Ctrl-Cなどで中断した場合も、反映できたファイルを記録する
61
+ rest = actions - uploaded
62
+ @context.err.puts "bosun: #{t(:"push.upload_failed", paths: rest.map(&:path).join(', '))}"
63
+ # 反映できたファイルだけでも記録し、host-trackingが実際のホストとずれないようにする
64
+ begin
65
+ record(uploaded) unless uploaded.empty?
66
+ rescue StandardError, SignalException => e
67
+ @context.err.puts "bosun: #{t(:"push.record_failed", message: e.message)}"
68
+ end
69
+ raise
70
+ end
71
+ record(uploaded)
72
+ end
73
+
74
+ # 計画を立てた後にホストで変更されていないかを、書き込む直前の一覧で確認する
75
+ def check_unchanged_since_plan!(actions)
76
+ infos = list_config_files
77
+ changed = actions.reject do |action|
78
+ info = infos[action.path]
79
+ if action.kind == :create
80
+ info.nil?
81
+ else
82
+ info&.writable? && info.modified == action.host.modified && info.size == action.host.size
83
+ end
84
+ end
85
+ return if changed.empty?
86
+
87
+ raise Error, t(:"push.changed_since_plan", paths: changed.map(&:path).join(", "))
88
+ end
89
+
90
+ # アップロードしたファイルを取得し直して、host-trackingにコミットする
91
+ def record(actions)
92
+ infos = list_config_files
93
+ paths = actions.map(&:path)
94
+ missing = paths.reject { |path| infos.key?(path) }
95
+ raise Error, t(:"push.uploaded_missing", paths: missing.join(", ")) unless missing.empty?
96
+
97
+ fetched = fetch(paths, infos)
98
+ actions.zip(fetched).each do |action, f|
99
+ next if @context.git.hash_object(f.content, write: false) == action.blob
100
+
101
+ @context.err.puts "bosun: #{t(:"push.content_mismatch", path: action.path)}"
102
+ end
103
+ commit_and_merge("push", fetched)
104
+ end
105
+
106
+ def print_plan(plan)
107
+ if plan.actions.empty? && plan.ok?
108
+ out.puts t(:"push.nothing")
109
+ else
110
+ plan.actions.each { |action| out.puts t(:"push.#{action.kind}", path: action.path) }
111
+ end
112
+
113
+ unless plan.problems.empty?
114
+ out.puts
115
+ out.puts t(:"push.problems")
116
+ plan.problems.each do |problem|
117
+ out.puts " #{problem.path}: #{problem.reason}"
118
+ out.puts " → #{problem.guide}"
119
+ end
120
+ end
121
+
122
+ print_skipped(plan.skipped)
123
+ end
124
+
125
+ def print_skipped(skipped)
126
+ skipped.group_by(&:state).each do |state, entries|
127
+ out.puts
128
+ out.puts t(:"push.skipped", label: SyncStatus.label(state))
129
+ entries.each { |entry| out.puts " #{entry.path}" }
130
+ print_guide(SyncStatus.guide(state))
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ module Commands
6
+ # forgetとrmで共通の処理
7
+ module Removing
8
+ private
9
+
10
+ # 指定したファイルがトラッキング中で、ローカルで変更されていないことを確認する
11
+ def check_removable!(paths)
12
+ paths.each { |path| validate_path!(path) }
13
+ tracked = tracked_paths
14
+ untracked = paths.reject { |path| tracked.include?(path) }
15
+ raise Error, t(:"remove.not_tracked", paths: untracked.join(", ")) unless untracked.empty?
16
+
17
+ tracking.check_workspace!(paths.map { |path| HostTracking.repo_path(path) })
18
+ check_not_modified_locally!(paths)
19
+ end
20
+
21
+ # マージで変更と削除が衝突するので、ローカル(HEAD)で変更されているファイルは先に中止する
22
+ # (ローカルで削除したファイルは指定できる)
23
+ def check_not_modified_locally!(paths)
24
+ tracked_tree = @context.git.ls_tree(tracking.tip, HostTracking::CONFIG_DIR)
25
+ local_tree = @context.git.ls_tree("HEAD", HostTracking::CONFIG_DIR)
26
+ modified = paths.select do |path|
27
+ repo_path = HostTracking.repo_path(path)
28
+ local_tree.key?(repo_path) && local_tree[repo_path] != tracked_tree[repo_path]
29
+ end
30
+ return if modified.empty?
31
+
32
+ raise Error, t(:"remove.locally_modified", paths: modified.join(", "),
33
+ branch: HostTracking::BRANCH, dir: HostTracking::CONFIG_DIR)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ module Commands
6
+ class Rm < Base
7
+ include Fetching
8
+ include Removing
9
+
10
+ def run(args)
11
+ dry_run = false
12
+ args = parse_options(args) do |o|
13
+ o.on("-n", "--dry-run", t(:"rm.dry_run_option")) { dry_run = true }
14
+ end
15
+ return 0 unless args
16
+ raise Error, t(:"rm.no_files") if args.empty?
17
+
18
+ paths = args.uniq
19
+ check_removable!(paths)
20
+ check_not_printing!
21
+ infos = check_host!(paths)
22
+
23
+ paths.each { |path| out.puts t(:"rm.plan", path: path) }
24
+ if dry_run
25
+ out.puts
26
+ out.puts t(:"rm.dry_run")
27
+ return 0
28
+ end
29
+
30
+ out.puts
31
+ execute(paths, infos)
32
+ 0
33
+ end
34
+
35
+ private
36
+
37
+ # ホストのファイルが、最後に取得した内容と一致することを厳密に確認する
38
+ def check_host!(paths)
39
+ infos = list_config_files
40
+ tracked_tree = @context.git.ls_tree(tracking.tip, HostTracking::CONFIG_DIR)
41
+ problems = paths.filter_map do |path|
42
+ info = infos[path]
43
+ if info.nil?
44
+ t(:"rm.host_deleted", path: path)
45
+ elsif !info.writable?
46
+ t(:"rm.not_writable", path: path)
47
+ elsif host_blob(path) != tracked_tree[HostTracking.repo_path(path)]
48
+ # 最後に取得した後のホストでの変更を、確認せずに失わないようにする
49
+ t(:"rm.host_modified", path: path)
50
+ end
51
+ end
52
+ return infos if problems.empty?
53
+
54
+ details = problems.map { |problem| " #{problem}" }.join("\n")
55
+ raise Error, t(:"rm.aborted", details: details)
56
+ end
57
+
58
+ def host_blob(path)
59
+ @context.git.hash_object(client.download(FileRules::CONFIG_ROOT, path), write: false)
60
+ end
61
+
62
+ def execute(paths, infos)
63
+ check_unchanged_since_check!(paths, infos)
64
+
65
+ deleted = []
66
+ begin
67
+ paths.each do |path|
68
+ client.delete_file(FileRules::CONFIG_ROOT, path)
69
+ deleted << path
70
+ out.puts t(:"rm.deleted", path: path)
71
+ end
72
+ rescue StandardError, SignalException
73
+ # Ctrl-Cなどで中断した場合も、削除できたファイルを記録する
74
+ rest = paths - deleted
75
+ @context.err.puts "bosun: #{t(:"rm.delete_failed", paths: rest.join(', '))}"
76
+ # 削除できたファイルだけでも記録し、host-trackingが実際のホストとずれないようにする
77
+ begin
78
+ record(deleted) unless deleted.empty?
79
+ rescue StandardError, SignalException => e
80
+ @context.err.puts "bosun: #{t(:"rm.record_failed", message: e.message)}"
81
+ end
82
+ raise
83
+ end
84
+ record(deleted)
85
+ end
86
+
87
+ # 確認の後にホストで変更されていないかを、削除する直前の一覧で確認する
88
+ def check_unchanged_since_check!(paths, infos)
89
+ current = list_config_files
90
+ changed = paths.reject do |path|
91
+ now = current[path]
92
+ now&.writable? && now.modified == infos[path].modified && now.size == infos[path].size
93
+ end
94
+ return if changed.empty?
95
+
96
+ raise Error, t(:"rm.changed_since_check", paths: changed.join(", "))
97
+ end
98
+
99
+ def record(paths)
100
+ commit_and_merge("rm", [], removed: paths.map { |path| HostTracking.repo_path(path) })
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ module Commands
6
+ class Roots < Base
7
+ def run(args)
8
+ long = false
9
+ args = parse_options(args) do |o|
10
+ o.on("-l", "--long", t(:"roots.long_option")) { long = true }
11
+ end
12
+ return 0 unless args
13
+
14
+ check_no_arguments!(args)
15
+
16
+ # Moonrakerが返す順(configが先頭)のまま表示する
17
+ roots = client.roots
18
+ if long
19
+ name_width = roots.map { |root| root.name.size }.max
20
+ roots.each do |root|
21
+ out.puts format("%-2s %-*s %s", root.permissions, name_width, root.name, root.path)
22
+ end
23
+ else
24
+ roots.each { |root| out.puts root.name }
25
+ end
26
+ 0
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ module Commands
6
+ class Status < Base
7
+ include StatusReport
8
+
9
+ def run(args)
10
+ args = parse_options(args)
11
+ return 0 unless args
12
+
13
+ check_no_arguments!(args)
14
+
15
+ git = @context.git
16
+ report(git, SyncStatus.new(git: git, client: client, tracking: HostTracking.new(git)))
17
+ 0
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ module Commands
6
+ # SyncStatusの表示(statusとauditで共通)
7
+ module StatusReport
8
+ private
9
+
10
+ def report(git, status)
11
+ print_summary(git, status)
12
+ print_entries(status)
13
+ print_renames(status)
14
+ print_dirty(git)
15
+ end
16
+
17
+ def print_summary(git, status)
18
+ if status.tip
19
+ date = git.run("log", "-1", "--format=%cd", "--date=format-local:%Y-%m-%d %H:%M:%S", status.tip).strip
20
+ out.puts "#{HostTracking::BRANCH}: #{status.tip[0, 7]} (#{date})"
21
+ else
22
+ out.puts t(:"status.no_tip", branch: HostTracking::BRANCH)
23
+ end
24
+ return unless status.unmerged?
25
+
26
+ out.puts
27
+ out.puts t(:"status.unmerged", branch: HostTracking::BRANCH)
28
+ out.puts " → #{t(:"status.unmerged_guide", branch: HostTracking::BRANCH)}"
29
+ end
30
+
31
+ def print_entries(status)
32
+ SyncStatus::STATES.each do |state|
33
+ next if state == :synced
34
+
35
+ entries = status.by_state(state)
36
+ next if entries.empty?
37
+
38
+ out.puts
39
+ out.puts "#{SyncStatus.label(state)}:"
40
+ entries.each { |entry| out.puts " #{entry.path}" }
41
+ print_guide(SyncStatus.guide(state))
42
+ end
43
+
44
+ synced = status.by_state(:synced).size
45
+ out.puts
46
+ if synced == status.entries.size && synced.positive?
47
+ out.puts t(:"status.all_synced", count: synced)
48
+ else
49
+ out.puts t(:"status.synced_count", count: synced)
50
+ end
51
+ end
52
+
53
+ def print_renames(status)
54
+ return if status.renames.empty?
55
+
56
+ out.puts
57
+ out.puts t(:"status.renames")
58
+ status.renames.each do |rename|
59
+ out.puts " #{rename.from} → #{rename.to}"
60
+ out.puts " → #{t(:"status.rename_guide", to: rename.to, from: rename.from)}"
61
+ end
62
+ end
63
+
64
+ def print_dirty(git)
65
+ dirty = git.dirty_paths([HostTracking::CONFIG_DIR])
66
+ return if dirty.empty?
67
+
68
+ out.puts
69
+ out.puts t(:"status.dirty")
70
+ dirty.each { |path| out.puts " #{path}" }
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Moonraker
6
+ module Bosun
7
+ class Config
8
+ ENV_BASE_URL = "MOONRAKER_BASE_URL"
9
+
10
+ attr_reader :base_url
11
+
12
+ def self.from_env(env = ENV)
13
+ url = env[ENV_BASE_URL]
14
+ raise Error, Messages.t(:"config.not_set", name: ENV_BASE_URL) if url.nil? || url.empty?
15
+
16
+ new(base_url: url)
17
+ end
18
+
19
+ def initialize(base_url:)
20
+ uri = URI.parse(base_url)
21
+ unless uri.is_a?(URI::HTTP) && uri.host
22
+ raise Error, Messages.t(:"config.not_http", name: ENV_BASE_URL, url: base_url)
23
+ end
24
+
25
+ @base_url = uri
26
+ rescue URI::InvalidURIError
27
+ raise Error, Messages.t(:"config.invalid_url", name: ENV_BASE_URL, url: base_url)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ # コマンドの実行に必要な入出力や接続先
6
+ class Context
7
+ attr_reader :out, :err, :env, :cwd
8
+
9
+ def initialize(out:, err:, env:, cwd: nil, client: nil)
10
+ @out = out
11
+ @err = err
12
+ @env = env
13
+ @cwd = cwd || Dir.pwd
14
+ @client = client
15
+ end
16
+
17
+ def client
18
+ @client ||= Client.new(Config.from_env(env).base_url)
19
+ end
20
+
21
+ def git
22
+ @git ||= Git.open(cwd)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ # ユーザに表示して終了するエラー
6
+ class Error < StandardError; end
7
+ end
8
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ # ホストのファイルを一覧やトラッキングの対象にするかどうかの判定
6
+ #
7
+ # docs/design.md の「対象のファイル」を参照。
8
+ module FileRules
9
+ CONFIG_ROOT = "config"
10
+
11
+ # Klipperの SAVE_CONFIG によるもの: <名前>-YYYYMMDD_HHMMSS.cfg
12
+ KLIPPER_BACKUP = /-\d{8}_\d{6}(?:\.cfg)?\z/
13
+ # Moonrakerによるもの: .<名前>.bkp
14
+ MOONRAKER_BACKUP = /\A\..+\.bkp\z/
15
+
16
+ REASONS = %i[read_only backup].freeze
17
+
18
+ module_function
19
+
20
+ def reason_text(reason) = Messages.t(:"exclusion.#{reason}")
21
+
22
+ def host_backup?(path)
23
+ name = File.basename(path)
24
+ KLIPPER_BACKUP.match?(name) || MOONRAKER_BACKUP.match?(name)
25
+ end
26
+
27
+ # 除外する理由(REASONSのキー)を返す。除外しない場合はnil
28
+ def exclusion(root, info)
29
+ return nil unless root == CONFIG_ROOT
30
+ return :read_only unless info.writable?
31
+ return :backup if host_backup?(info.path)
32
+
33
+ nil
34
+ end
35
+ end
36
+ end
37
+ end