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,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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moonraker
4
+ module Bosun
5
+ VERSION = "0.1.0"
6
+ end
7
+ 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: []