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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE +21 -0
- data/README.ja.md +467 -0
- data/README.md +124 -0
- data/docs/design.md +358 -0
- data/docs/moonraker-api.md +108 -0
- data/exe/bosun +7 -0
- data/lib/moonraker/bosun/cli.rb +64 -0
- data/lib/moonraker/bosun/client.rb +147 -0
- data/lib/moonraker/bosun/commands/add.rb +38 -0
- data/lib/moonraker/bosun/commands/audit.rb +48 -0
- data/lib/moonraker/bosun/commands/backup.rb +135 -0
- data/lib/moonraker/bosun/commands/base.rb +61 -0
- data/lib/moonraker/bosun/commands/diff.rb +29 -0
- data/lib/moonraker/bosun/commands/export.rb +103 -0
- data/lib/moonraker/bosun/commands/fetching.rb +76 -0
- data/lib/moonraker/bosun/commands/forget.rb +26 -0
- data/lib/moonraker/bosun/commands/get.rb +57 -0
- data/lib/moonraker/bosun/commands/ls.rb +43 -0
- data/lib/moonraker/bosun/commands/pull.rb +52 -0
- data/lib/moonraker/bosun/commands/push.rb +136 -0
- data/lib/moonraker/bosun/commands/removing.rb +38 -0
- data/lib/moonraker/bosun/commands/rm.rb +105 -0
- data/lib/moonraker/bosun/commands/roots.rb +31 -0
- data/lib/moonraker/bosun/commands/status.rb +22 -0
- data/lib/moonraker/bosun/commands/status_report.rb +75 -0
- data/lib/moonraker/bosun/config.rb +31 -0
- data/lib/moonraker/bosun/context.rb +26 -0
- data/lib/moonraker/bosun/error.rb +8 -0
- data/lib/moonraker/bosun/file_rules.rb +37 -0
- data/lib/moonraker/bosun/git.rb +139 -0
- data/lib/moonraker/bosun/host_tracking.rb +214 -0
- data/lib/moonraker/bosun/messages/catalog.rb +837 -0
- data/lib/moonraker/bosun/messages.rb +42 -0
- data/lib/moonraker/bosun/push_plan.rb +89 -0
- data/lib/moonraker/bosun/sync_status.rb +130 -0
- data/lib/moonraker/bosun/version.rb +7 -0
- data/lib/moonraker/bosun.rb +31 -0
- 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,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
|