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,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moonraker
|
|
4
|
+
module Bosun
|
|
5
|
+
# 表示するメッセージ(日本語と英語)
|
|
6
|
+
#
|
|
7
|
+
# 言語は、CLIの実行中だけスレッドごとに設定する(テストを並列に実行するため)。
|
|
8
|
+
# 設定していないときは英語になる。
|
|
9
|
+
module Messages
|
|
10
|
+
DEFAULT_LOCALE = :en
|
|
11
|
+
# POSIXと同じく、この順に見て最初に設定されているものを使う
|
|
12
|
+
LOCALE_VARIABLES = %w[LC_ALL LC_MESSAGES LANG].freeze
|
|
13
|
+
THREAD_KEY = :moonraker_bosun_locale
|
|
14
|
+
PLACEHOLDER = /%\{(\w+)\}/
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def locale_from_env(env)
|
|
19
|
+
value = LOCALE_VARIABLES.map { |name| env[name] }.find { |v| v && !v.empty? }
|
|
20
|
+
value&.start_with?("ja") ? :ja : DEFAULT_LOCALE
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Thread#[]はファイバーごとの値になるので、スレッドの中で共有するthread_variableを使う
|
|
24
|
+
def locale = Thread.current.thread_variable_get(THREAD_KEY) || DEFAULT_LOCALE
|
|
25
|
+
|
|
26
|
+
def with_locale(locale)
|
|
27
|
+
previous = Thread.current.thread_variable_get(THREAD_KEY)
|
|
28
|
+
Thread.current.thread_variable_set(THREAD_KEY, locale)
|
|
29
|
+
yield
|
|
30
|
+
ensure
|
|
31
|
+
Thread.current.thread_variable_set(THREAD_KEY, previous)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# %{name}をparamsの値で置き換える
|
|
35
|
+
def t(key, **params)
|
|
36
|
+
CATALOG.fetch(key).fetch(locale).gsub(PLACEHOLDER) { params.fetch(Regexp.last_match(1).to_sym).to_s }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
require_relative "messages/catalog"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moonraker
|
|
4
|
+
module Bosun
|
|
5
|
+
# pushでホストに反映する内容と、反映できない理由を決める
|
|
6
|
+
#
|
|
7
|
+
# docs/design.md の「push」を参照。
|
|
8
|
+
class PushPlan
|
|
9
|
+
include Messages
|
|
10
|
+
|
|
11
|
+
# kindは:createか:update。pathはRoot configからのパス
|
|
12
|
+
# hostは計画を立てたときのホストのファイルの情報(作成の場合はnil)
|
|
13
|
+
Action = Data.define(:path, :kind, :blob, :host)
|
|
14
|
+
Problem = Data.define(:path, :reason, :guide)
|
|
15
|
+
# 反映しないファイル(stateはSyncStatus::STATESのキー)
|
|
16
|
+
Skipped = Data.define(:path, :state)
|
|
17
|
+
|
|
18
|
+
# ホストには通常のファイルしか作れないので、これ以外のモード(シンボリックリンク、実行可能ファイル、
|
|
19
|
+
# submoduleなど)は反映しない。host-trackingにはホストの内容を通常のファイルとして記録するため、
|
|
20
|
+
# 反映するとマージでモードが衝突する
|
|
21
|
+
REGULAR_FILE_MODE = "100644"
|
|
22
|
+
|
|
23
|
+
# 反映の対象にしないが、知らせる状態
|
|
24
|
+
NOTICE_STATES = %i[host_modified local_deleted host_modified_local_deleted
|
|
25
|
+
host_deleted both_deleted host_read_only].freeze
|
|
26
|
+
|
|
27
|
+
attr_reader :actions, :problems, :skipped
|
|
28
|
+
|
|
29
|
+
def initialize(status)
|
|
30
|
+
@status = status
|
|
31
|
+
raise Error, unmerged_message if status.unmerged?
|
|
32
|
+
|
|
33
|
+
@actions = []
|
|
34
|
+
@problems = []
|
|
35
|
+
@skipped = []
|
|
36
|
+
status.entries.each { |entry| plan(entry) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ok? = @problems.empty?
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def plan(entry)
|
|
44
|
+
tracked = @status.tracked_blob(entry.path)
|
|
45
|
+
local = @status.local_blob(entry.path)
|
|
46
|
+
# ローカルにあり、最後に取得した内容と違うファイルが対象
|
|
47
|
+
if local && local != tracked
|
|
48
|
+
mode = @status.local_mode(entry.path)
|
|
49
|
+
return problem(entry.path, :not_regular_file, mode: mode) unless mode == REGULAR_FILE_MODE
|
|
50
|
+
|
|
51
|
+
tracked ? plan_update(entry.path, local, tracked) : plan_create(entry.path, local)
|
|
52
|
+
elsif NOTICE_STATES.include?(entry.state)
|
|
53
|
+
@skipped << Skipped.new(path: entry.path, state: entry.state)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def plan_update(path, local, tracked)
|
|
58
|
+
host = @status.host_info(path)
|
|
59
|
+
if host.nil?
|
|
60
|
+
problem(path, :host_deleted)
|
|
61
|
+
elsif !host.writable?
|
|
62
|
+
problem(path, :not_writable)
|
|
63
|
+
elsif @status.host_blob(path) != tracked
|
|
64
|
+
# 最後に取得した後にホストで変更された内容を、上書きしないようにする
|
|
65
|
+
problem(path, :host_modified, :pull)
|
|
66
|
+
else
|
|
67
|
+
@actions << Action.new(path: path, kind: :update, blob: local, host: host)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def plan_create(path, local)
|
|
72
|
+
if @status.host_info(path)
|
|
73
|
+
problem(path, :exists)
|
|
74
|
+
elsif FileRules.host_backup?(path)
|
|
75
|
+
problem(path, :backup_name)
|
|
76
|
+
else
|
|
77
|
+
@actions << Action.new(path: path, kind: :create, blob: local, host: nil)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 理由は plan.<reason>、案内は plan.<guide>_guide(省略時は理由と同じ名前)
|
|
82
|
+
def problem(path, reason, guide = reason, **params)
|
|
83
|
+
@problems << Problem.new(path: path, reason: t(:"plan.#{reason}", **params), guide: t(:"plan.#{guide}_guide"))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def unmerged_message = t(:"plan.unmerged", branch: HostTracking::BRANCH)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moonraker
|
|
4
|
+
module Bosun
|
|
5
|
+
# ホスト(H)、host-trackingの先頭(T)、HEAD(L)の3つを比べて、ファイルごとの状態を判定する
|
|
6
|
+
#
|
|
7
|
+
# docs/design.md の「状態の判定」を参照。
|
|
8
|
+
class SyncStatus
|
|
9
|
+
# 表示する順に並べる。見出しと案内はMessagesの state.<状態>.label と state.<状態>.guide
|
|
10
|
+
STATES = %i[host_modified local_modified both_modified host_deleted both_deleted host_read_only
|
|
11
|
+
local_deleted host_modified_local_deleted local_added added_on_both untracked synced].freeze
|
|
12
|
+
|
|
13
|
+
def self.label(state) = Messages.t(:"state.#{state}.label")
|
|
14
|
+
|
|
15
|
+
# 解消するための手順の案内(同期済みにはない)
|
|
16
|
+
def self.guide(state)
|
|
17
|
+
return nil if state == :synced
|
|
18
|
+
|
|
19
|
+
Messages.t(:"state.#{state}.guide", branch: HostTracking::BRANCH, dir: HostTracking::CONFIG_DIR)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# pathはRoot configからのパス
|
|
23
|
+
Entry = Data.define(:path, :state)
|
|
24
|
+
# ホストで名前を変更した可能性があるファイルの組
|
|
25
|
+
Rename = Data.define(:from, :to)
|
|
26
|
+
|
|
27
|
+
attr_reader :entries, :renames
|
|
28
|
+
|
|
29
|
+
# strict: trueなら、トラッキング中のファイルをすべて取得して内容を比べる
|
|
30
|
+
def initialize(git:, client:, tracking:, strict: false)
|
|
31
|
+
@git = git
|
|
32
|
+
@client = client
|
|
33
|
+
@tracking = tracking
|
|
34
|
+
@strict = strict
|
|
35
|
+
compute
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def tip = @tracking.tip
|
|
39
|
+
|
|
40
|
+
# host-trackingに、まだ現在のブランチに取り込んでいないコミットがあるか
|
|
41
|
+
def unmerged? = !tip.nil? && !@git.ancestor?(tip, "HEAD")
|
|
42
|
+
|
|
43
|
+
def by_state(state) = @entries.select { |entry| entry.state == state }
|
|
44
|
+
|
|
45
|
+
# pathはRoot configからのパス
|
|
46
|
+
def host_info(path) = @host[path]
|
|
47
|
+
def tracked_blob(path) = @tracked[path]
|
|
48
|
+
def local_blob(path) = @local[path]
|
|
49
|
+
def local_mode(path) = @local_modes[path]
|
|
50
|
+
|
|
51
|
+
# ホストの内容を取得し、blobのSHA-1を返す
|
|
52
|
+
def host_blob(path)
|
|
53
|
+
@git.hash_object(@client.download(FileRules::CONFIG_ROOT, path), write: false)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def compute
|
|
59
|
+
@host = @client.list_files(FileRules::CONFIG_ROOT).to_h { |info| [info.path, info] }
|
|
60
|
+
@tracked = tip ? tree(tip) : {}
|
|
61
|
+
@local = tree("HEAD")
|
|
62
|
+
@local_modes = @git.ls_tree_modes("HEAD", HostTracking::CONFIG_DIR)
|
|
63
|
+
.transform_keys { |path| HostTracking.host_path(path) }
|
|
64
|
+
@recorded = @tracking.recorded
|
|
65
|
+
|
|
66
|
+
untracked_candidates = @host.keys.select { |path| FileRules.exclusion(FileRules::CONFIG_ROOT, @host[path]).nil? }
|
|
67
|
+
paths = (@tracked.keys | @local.keys | untracked_candidates).sort
|
|
68
|
+
@entries = paths.map { |path| Entry.new(path: path, state: state_of(path)) }
|
|
69
|
+
@renames = detect_renames
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def tree(rev)
|
|
73
|
+
@git.ls_tree(rev, HostTracking::CONFIG_DIR).transform_keys { |path| HostTracking.host_path(path) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def state_of(path)
|
|
77
|
+
host = @host[path]
|
|
78
|
+
tracked = @tracked[path]
|
|
79
|
+
local = @local[path]
|
|
80
|
+
|
|
81
|
+
if tracked
|
|
82
|
+
return local ? :host_deleted : :both_deleted unless host
|
|
83
|
+
return :host_read_only unless host.writable?
|
|
84
|
+
|
|
85
|
+
host_same = host_same?(path, host, tracked)
|
|
86
|
+
if local.nil?
|
|
87
|
+
host_same ? :local_deleted : :host_modified_local_deleted
|
|
88
|
+
elsif local == tracked
|
|
89
|
+
host_same ? :synced : :host_modified
|
|
90
|
+
else
|
|
91
|
+
host_same ? :local_modified : :both_modified
|
|
92
|
+
end
|
|
93
|
+
elsif local
|
|
94
|
+
host ? :added_on_both : :local_added
|
|
95
|
+
else
|
|
96
|
+
:untracked
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# ホストの内容が、最後に取得した内容(blob)と同じか
|
|
101
|
+
def host_same?(path, host, blob)
|
|
102
|
+
unless @strict
|
|
103
|
+
recorded = @recorded[HostTracking.repo_path(path)]
|
|
104
|
+
if recorded
|
|
105
|
+
return true if recorded.modified == host.modified && recorded.size == host.size
|
|
106
|
+
# サイズが違えば内容も違う
|
|
107
|
+
return false if recorded.size != host.size
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
# 更新日時だけが違う場合などは、取得して内容を比べる
|
|
111
|
+
host_blob(path) == blob
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# ホストで削除されたファイルと同じ内容のトラッキングしていないファイルを、名前の変更の候補とする
|
|
115
|
+
def detect_renames
|
|
116
|
+
deleted = (by_state(:host_deleted) + by_state(:both_deleted)).map(&:path)
|
|
117
|
+
untracked = by_state(:untracked).map(&:path)
|
|
118
|
+
return [] if deleted.empty? || untracked.empty?
|
|
119
|
+
|
|
120
|
+
deleted.flat_map do |from|
|
|
121
|
+
blob = @tracked[from]
|
|
122
|
+
size = @git.blob_size(blob)
|
|
123
|
+
untracked.filter_map do |to|
|
|
124
|
+
Rename.new(from: from, to: to) if @host[to].size == size && host_blob(to) == blob
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "bosun/version"
|
|
4
|
+
require_relative "bosun/error"
|
|
5
|
+
require_relative "bosun/messages"
|
|
6
|
+
require_relative "bosun/config"
|
|
7
|
+
require_relative "bosun/client"
|
|
8
|
+
require_relative "bosun/file_rules"
|
|
9
|
+
require_relative "bosun/git"
|
|
10
|
+
require_relative "bosun/host_tracking"
|
|
11
|
+
require_relative "bosun/sync_status"
|
|
12
|
+
require_relative "bosun/push_plan"
|
|
13
|
+
require_relative "bosun/context"
|
|
14
|
+
require_relative "bosun/commands/base"
|
|
15
|
+
require_relative "bosun/commands/roots"
|
|
16
|
+
require_relative "bosun/commands/ls"
|
|
17
|
+
require_relative "bosun/commands/get"
|
|
18
|
+
require_relative "bosun/commands/fetching"
|
|
19
|
+
require_relative "bosun/commands/add"
|
|
20
|
+
require_relative "bosun/commands/pull"
|
|
21
|
+
require_relative "bosun/commands/status_report"
|
|
22
|
+
require_relative "bosun/commands/status"
|
|
23
|
+
require_relative "bosun/commands/audit"
|
|
24
|
+
require_relative "bosun/commands/diff"
|
|
25
|
+
require_relative "bosun/commands/push"
|
|
26
|
+
require_relative "bosun/commands/removing"
|
|
27
|
+
require_relative "bosun/commands/forget"
|
|
28
|
+
require_relative "bosun/commands/rm"
|
|
29
|
+
require_relative "bosun/commands/backup"
|
|
30
|
+
require_relative "bosun/commands/export"
|
|
31
|
+
require_relative "bosun/cli"
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: moonraker-bosun
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- akira yamada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: bosun fetches configuration files and Moonraker database contents from
|
|
13
|
+
a Klipper host through the Moonraker API, records them in a dedicated git branch,
|
|
14
|
+
and pushes local changes back to the host after checking that nothing has changed
|
|
15
|
+
there.
|
|
16
|
+
email:
|
|
17
|
+
- akira@arika.org
|
|
18
|
+
executables:
|
|
19
|
+
- bosun
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- CHANGELOG.md
|
|
24
|
+
- LICENSE
|
|
25
|
+
- README.ja.md
|
|
26
|
+
- README.md
|
|
27
|
+
- docs/design.md
|
|
28
|
+
- docs/moonraker-api.md
|
|
29
|
+
- exe/bosun
|
|
30
|
+
- lib/moonraker/bosun.rb
|
|
31
|
+
- lib/moonraker/bosun/cli.rb
|
|
32
|
+
- lib/moonraker/bosun/client.rb
|
|
33
|
+
- lib/moonraker/bosun/commands/add.rb
|
|
34
|
+
- lib/moonraker/bosun/commands/audit.rb
|
|
35
|
+
- lib/moonraker/bosun/commands/backup.rb
|
|
36
|
+
- lib/moonraker/bosun/commands/base.rb
|
|
37
|
+
- lib/moonraker/bosun/commands/diff.rb
|
|
38
|
+
- lib/moonraker/bosun/commands/export.rb
|
|
39
|
+
- lib/moonraker/bosun/commands/fetching.rb
|
|
40
|
+
- lib/moonraker/bosun/commands/forget.rb
|
|
41
|
+
- lib/moonraker/bosun/commands/get.rb
|
|
42
|
+
- lib/moonraker/bosun/commands/ls.rb
|
|
43
|
+
- lib/moonraker/bosun/commands/pull.rb
|
|
44
|
+
- lib/moonraker/bosun/commands/push.rb
|
|
45
|
+
- lib/moonraker/bosun/commands/removing.rb
|
|
46
|
+
- lib/moonraker/bosun/commands/rm.rb
|
|
47
|
+
- lib/moonraker/bosun/commands/roots.rb
|
|
48
|
+
- lib/moonraker/bosun/commands/status.rb
|
|
49
|
+
- lib/moonraker/bosun/commands/status_report.rb
|
|
50
|
+
- lib/moonraker/bosun/config.rb
|
|
51
|
+
- lib/moonraker/bosun/context.rb
|
|
52
|
+
- lib/moonraker/bosun/error.rb
|
|
53
|
+
- lib/moonraker/bosun/file_rules.rb
|
|
54
|
+
- lib/moonraker/bosun/git.rb
|
|
55
|
+
- lib/moonraker/bosun/host_tracking.rb
|
|
56
|
+
- lib/moonraker/bosun/messages.rb
|
|
57
|
+
- lib/moonraker/bosun/messages/catalog.rb
|
|
58
|
+
- lib/moonraker/bosun/push_plan.rb
|
|
59
|
+
- lib/moonraker/bosun/sync_status.rb
|
|
60
|
+
- lib/moonraker/bosun/version.rb
|
|
61
|
+
homepage: https://github.com/arika/moonraker-bosun
|
|
62
|
+
licenses:
|
|
63
|
+
- MIT
|
|
64
|
+
metadata:
|
|
65
|
+
allowed_push_host: https://rubygems.org
|
|
66
|
+
source_code_uri: https://github.com/arika/moonraker-bosun
|
|
67
|
+
changelog_uri: https://github.com/arika/moonraker-bosun/blob/main/CHANGELOG.md
|
|
68
|
+
rubygems_mfa_required: 'true'
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '3.4'
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 4.0.20
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: Track Klipper configuration files on a Moonraker host with git
|
|
86
|
+
test_files: []
|