dotsync 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/CHANGELOG.md +24 -0
- data/Gemfile.lock +1 -1
- data/README.md +50 -0
- data/exe/dotsync +5 -0
- data/lib/dotsync/actions/base_action.rb +1 -0
- data/lib/dotsync/actions/concerns/mappings_transfer.rb +97 -12
- data/lib/dotsync/actions/pull_action.rb +4 -2
- data/lib/dotsync/actions/push_action.rb +2 -2
- data/lib/dotsync/config/concerns/sync_mappings.rb +1 -0
- data/lib/dotsync/config/pull_action_config.rb +4 -0
- data/lib/dotsync/loaders/pull_loader.rb +2 -1
- data/lib/dotsync/loaders/push_loader.rb +1 -1
- data/lib/dotsync/models/mapping.rb +23 -5
- data/lib/dotsync/utils/config_cache.rb +68 -2
- data/lib/dotsync/utils/manifest.rb +45 -0
- data/lib/dotsync/utils/table_renderer.rb +20 -0
- data/lib/dotsync/version.rb +1 -1
- data/lib/dotsync.rb +2 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b49b79875a41ac8a64361efc61108dc18ee203b08804daf40e709e18972ed88
|
|
4
|
+
data.tar.gz: f3eaf2a153a9757d98018900245ac7020596aab2844dbf1841d99e5e7ae055c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3315fa7273357e2a61c494e1fa3abdd31ba2e43089888f8dc5174e8bc70ce69fe4bbf553a3db2d647d437b9dae81811056dcc43f7d31bef56736346628ef76b1
|
|
7
|
+
data.tar.gz: fa0bdc870da7374523db60c823d527e0ec8c1da8809548b3f4aeaaec03872cf3f96e08bff0c27d73524c80677ef6e765725416d74b57f83d66f57f86cfb31772
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## [0.4.0] - 2026-03-01
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- Add manifest-based orphan cleanup for non-force sync mappings (#32)
|
|
6
|
+
- Add `source` directive for config file indirection (#29) (#30)
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- CHANGELOG: remove manual version
|
|
11
|
+
- Decouple Terminal::Table from domain logic via TableRenderer (#28)
|
|
12
|
+
|
|
13
|
+
## [0.3.3] - 2026-02-16
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Add --force-hooks flag to re-run hooks when no files changed (#25) (#26)
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Update version
|
|
22
|
+
|
|
1
23
|
## [0.3.2] - 2026-02-14
|
|
2
24
|
|
|
3
25
|
### Fixed
|
|
@@ -451,5 +473,7 @@ Add gem executables
|
|
|
451
473
|
|
|
452
474
|
Initial version
|
|
453
475
|
|
|
476
|
+
[0.4.0]: https://github.com/dsaenztagarro/dotsync/compare/v0.3.3...v0.4.0
|
|
477
|
+
[0.3.3]: https://github.com/dsaenztagarro/dotsync/compare/v0.3.2...v0.3.3
|
|
454
478
|
[0.3.2]: https://github.com/dsaenztagarro/dotsync/compare/v0.3.1...v0.3.2
|
|
455
479
|
[0.3.1]: https://github.com/dsaenztagarro/dotsync/compare/v0.3.0...v0.3.1
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -20,6 +20,7 @@ Dotsync is a powerful Ruby gem for managing and synchronizing your dotfiles acro
|
|
|
20
20
|
- **Automatic Backups**: Pull operations create timestamped backups for easy recovery
|
|
21
21
|
- **Live Watching**: Continuously monitor and sync changes in real-time with `watch` command
|
|
22
22
|
- **Config Includes**: Compose configs from a shared base + machine-specific overlays with `include`
|
|
23
|
+
- **Config Source**: Point local config to your dotfiles repo with `source` — changes are visible immediately without syncing
|
|
23
24
|
- **Post-Sync Hooks**: Run commands automatically after files change (e.g., codesigning, chmod, service reload)
|
|
24
25
|
- **Customizable Output**: Control verbosity and customize icons to match your preferences
|
|
25
26
|
- **Auto-Updates**: Get notified when new versions are available
|
|
@@ -37,6 +38,7 @@ Dotsync is a powerful Ruby gem for managing and synchronizing your dotfiles acro
|
|
|
37
38
|
- [Mapping Options (force, only, ignore)](#force-only-and-ignore-options-in-mappings)
|
|
38
39
|
- [Post-Sync Hooks](#post-sync-hooks)
|
|
39
40
|
- [Config Includes](#config-includes)
|
|
41
|
+
- [Config Source](#config-source)
|
|
40
42
|
- [Safety Features](#safety-features)
|
|
41
43
|
- [Customizing Icons](#customizing-icons)
|
|
42
44
|
- [Automatic Update Checks](#automatic-update-checks)
|
|
@@ -627,6 +629,54 @@ paths = ["~/.config/nvim/", "~/.config/zsh/"]
|
|
|
627
629
|
- The `include` key is consumed and does not appear in the merged config
|
|
628
630
|
- Cache invalidation tracks both the overlay and included file's mtime/size
|
|
629
631
|
|
|
632
|
+
#### Config Source
|
|
633
|
+
|
|
634
|
+
Use `source` to point your local config at the authoritative copy in your dotfiles repository. Instead of syncing config files back and forth, dotsync reads the config directly from the repo. Changes are visible immediately — no pull needed.
|
|
635
|
+
|
|
636
|
+
**The problem it solves:** When config files change in your dotfiles repo, you normally need to `ds pull` to update local copies, then run `ds pull` again so the new config takes effect. With `source`, dotsync reads from the repo directly, eliminating this two-pass problem.
|
|
637
|
+
|
|
638
|
+
**Setup:**
|
|
639
|
+
|
|
640
|
+
```toml
|
|
641
|
+
# ~/.config/dotsync.toml (one-time setup — never changes)
|
|
642
|
+
source = "$XDG_CONFIG_HOME_MIRROR/dotsync/dotsync.mbp_personal.toml"
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
```toml
|
|
646
|
+
# ~/Code/dotfiles/xdg_config_home/dotsync/dotsync.mbp_personal.toml (the real config)
|
|
647
|
+
include = "dotsync.base.toml"
|
|
648
|
+
|
|
649
|
+
[[sync.xdg_config]]
|
|
650
|
+
path = "zsh"
|
|
651
|
+
ignore = [".zsh_sessions", ".zsh_history", ".zcompdump"]
|
|
652
|
+
|
|
653
|
+
[[sync.xdg_config]]
|
|
654
|
+
path = "nvim"
|
|
655
|
+
force = true
|
|
656
|
+
ignore = ["lazy-lock.json"]
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
The local file is a thin pointer that never changes. The real config (and its `include` base) live in the repo and are read directly.
|
|
660
|
+
|
|
661
|
+
**Rules:**
|
|
662
|
+
- `source` must be the **only** key in the pointer file — no other config keys allowed
|
|
663
|
+
- The source file can use `include` to compose with a base config (resolved relative to the source file)
|
|
664
|
+
- Chained sources (a source file pointing to another source) are not supported
|
|
665
|
+
- Environment variables are expanded in the `source` path (e.g., `$XDG_CONFIG_HOME_MIRROR`)
|
|
666
|
+
- Cache invalidation tracks the pointer file, the source file, and any included file
|
|
667
|
+
|
|
668
|
+
**Per-machine setup:**
|
|
669
|
+
|
|
670
|
+
```
|
|
671
|
+
dotfiles/xdg_config_home/dotsync/
|
|
672
|
+
dotsync.base.toml # shared mappings, hooks, watch paths
|
|
673
|
+
dotsync.mbp_personal.toml # include + personal-only mappings
|
|
674
|
+
dotsync.mbp_work.toml # include + work-only mappings
|
|
675
|
+
dotsync.mac_mini.toml # include + mac-mini-only mappings
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
Each machine's `~/.config/dotsync.toml` is a one-line pointer to its overlay. Edit the repo files and changes take effect immediately — no syncing, no two-pass.
|
|
679
|
+
|
|
630
680
|
### Safety Features
|
|
631
681
|
|
|
632
682
|
Dotsync includes several safety mechanisms to prevent accidental data loss:
|
data/exe/dotsync
CHANGED
|
@@ -52,6 +52,7 @@ opt_parser = OptionParser.new do |opts|
|
|
|
52
52
|
--only-mappings Show only the mappings section
|
|
53
53
|
-v, --verbose Force showing all available information
|
|
54
54
|
--diff-content Show git-like content diff for modified files
|
|
55
|
+
--force-hooks Run hooks even when no files changed
|
|
55
56
|
--trace Show full error backtraces (for debugging)
|
|
56
57
|
--version Show version number
|
|
57
58
|
-h, --help Show this help message
|
|
@@ -117,6 +118,10 @@ opt_parser = OptionParser.new do |opts|
|
|
|
117
118
|
options[:diff_content] = true
|
|
118
119
|
end
|
|
119
120
|
|
|
121
|
+
opts.on("--force-hooks", "Run hooks even when no files changed") do
|
|
122
|
+
options[:force_hooks] = true
|
|
123
|
+
end
|
|
124
|
+
|
|
120
125
|
opts.on("--trace", "Show full error backtraces (for debugging)") do
|
|
121
126
|
options[:trace] = true
|
|
122
127
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "find"
|
|
4
|
+
|
|
3
5
|
module Dotsync
|
|
4
6
|
# MappingsTransfer provides shared functionality for push/pull actions.
|
|
5
7
|
#
|
|
@@ -46,15 +48,13 @@ module Dotsync
|
|
|
46
48
|
info("Environment variables:", icon: :env_vars)
|
|
47
49
|
|
|
48
50
|
rows = env_vars.map { |env_var| [env_var, ENV[env_var]] }.sort_by(&:first)
|
|
49
|
-
|
|
50
|
-
logger.log(table)
|
|
51
|
+
logger.log(Dotsync::TableRenderer.new(rows: rows).render)
|
|
51
52
|
logger.log("")
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def show_mappings_legend
|
|
55
56
|
info("Mappings Legend:", icon: :legend)
|
|
56
|
-
|
|
57
|
-
logger.log(table)
|
|
57
|
+
logger.log(Dotsync::TableRenderer.new(rows: MAPPINGS_LEGEND).render)
|
|
58
58
|
logger.log("")
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -68,15 +68,13 @@ module Dotsync
|
|
|
68
68
|
colorize_env_vars(mapping.original_dest)
|
|
69
69
|
]
|
|
70
70
|
end
|
|
71
|
-
|
|
72
|
-
logger.log(table)
|
|
71
|
+
logger.log(Dotsync::TableRenderer.new(headings: ["Flags", "Source", "Destination"], rows: rows).render)
|
|
73
72
|
logger.log("")
|
|
74
73
|
end
|
|
75
74
|
|
|
76
75
|
def show_differences_legend
|
|
77
76
|
info("Differences Legend:", icon: :legend)
|
|
78
|
-
|
|
79
|
-
logger.log(table)
|
|
77
|
+
logger.log(Dotsync::TableRenderer.new(rows: DIFFERENCES_LEGEND).render)
|
|
80
78
|
logger.log("")
|
|
81
79
|
end
|
|
82
80
|
|
|
@@ -95,6 +93,8 @@ module Dotsync
|
|
|
95
93
|
logger.log("")
|
|
96
94
|
|
|
97
95
|
show_content_diffs if diff_content && has_modifications?
|
|
96
|
+
|
|
97
|
+
show_orphan_preview if respond_to?(:manifests_xdg_data_home, true)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def show_content_diffs
|
|
@@ -136,20 +136,47 @@ module Dotsync
|
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
-
def
|
|
139
|
+
def cleanup_orphans
|
|
140
|
+
valid_mappings.each do |mapping|
|
|
141
|
+
next unless mapping.has_inclusions? && !mapping.force? && mapping.manifest_key
|
|
142
|
+
|
|
143
|
+
current_files = dest_files_matching_inclusions(mapping)
|
|
144
|
+
manifest = Dotsync::Manifest.new(
|
|
145
|
+
dest_dir: mapping.dest,
|
|
146
|
+
key: mapping.manifest_key,
|
|
147
|
+
xdg_data_home: manifests_xdg_data_home
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
manifest.orphans(current_files).each do |orphan_path|
|
|
151
|
+
next unless File.exist?(orphan_path)
|
|
152
|
+
|
|
153
|
+
FileUtils.rm(orphan_path)
|
|
154
|
+
logger.log("#{Icons.diff_removed}#{orphan_path}", color: Colors.diff_removals)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
manifest.write(current_files)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def execute_hooks(force: false)
|
|
140
162
|
valid_mappings.each_with_index do |mapping, idx|
|
|
141
163
|
next unless mapping.has_hooks?
|
|
142
164
|
|
|
143
165
|
differ = differs[idx]
|
|
144
166
|
changed_files = differ.additions + differ.modifications
|
|
145
|
-
|
|
167
|
+
if changed_files.empty?
|
|
168
|
+
next unless force
|
|
169
|
+
|
|
170
|
+
changed_files = all_dest_files(mapping)
|
|
171
|
+
next if changed_files.empty?
|
|
172
|
+
end
|
|
146
173
|
|
|
147
174
|
runner = Dotsync::HookRunner.new(mapping: mapping, changed_files: changed_files, logger: logger)
|
|
148
175
|
runner.execute
|
|
149
176
|
end
|
|
150
177
|
end
|
|
151
178
|
|
|
152
|
-
def show_hooks_preview
|
|
179
|
+
def show_hooks_preview(force: false)
|
|
153
180
|
hooks_to_run = []
|
|
154
181
|
|
|
155
182
|
valid_mappings.each_with_index do |mapping, idx|
|
|
@@ -157,7 +184,12 @@ module Dotsync
|
|
|
157
184
|
|
|
158
185
|
differ = differs[idx]
|
|
159
186
|
changed_files = differ.additions + differ.modifications
|
|
160
|
-
|
|
187
|
+
if changed_files.empty?
|
|
188
|
+
next unless force
|
|
189
|
+
|
|
190
|
+
changed_files = all_dest_files(mapping)
|
|
191
|
+
next if changed_files.empty?
|
|
192
|
+
end
|
|
161
193
|
|
|
162
194
|
runner = Dotsync::HookRunner.new(mapping: mapping, changed_files: changed_files, logger: logger)
|
|
163
195
|
hooks_to_run.concat(runner.preview)
|
|
@@ -217,5 +249,58 @@ module Dotsync
|
|
|
217
249
|
def valid_mappings
|
|
218
250
|
mappings.select(&:valid?)
|
|
219
251
|
end
|
|
252
|
+
|
|
253
|
+
def show_orphan_preview
|
|
254
|
+
orphans = []
|
|
255
|
+
valid_mappings.each do |mapping|
|
|
256
|
+
next unless mapping.has_inclusions? && !mapping.force? && mapping.manifest_key
|
|
257
|
+
|
|
258
|
+
current_files = dest_files_matching_inclusions(mapping)
|
|
259
|
+
manifest = Dotsync::Manifest.new(
|
|
260
|
+
dest_dir: mapping.dest,
|
|
261
|
+
key: mapping.manifest_key,
|
|
262
|
+
xdg_data_home: manifests_xdg_data_home
|
|
263
|
+
)
|
|
264
|
+
orphans.concat(manifest.orphans(current_files).select { |p| File.exist?(p) })
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
return if orphans.empty?
|
|
268
|
+
|
|
269
|
+
info("Orphans to remove:", icon: :diff)
|
|
270
|
+
orphans.sort.each do |path|
|
|
271
|
+
logger.log("#{Icons.diff_removed}#{path}", color: Colors.diff_removals)
|
|
272
|
+
end
|
|
273
|
+
logger.log("")
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def dest_files_matching_inclusions(mapping)
|
|
277
|
+
return [] unless File.directory?(mapping.dest)
|
|
278
|
+
|
|
279
|
+
files = []
|
|
280
|
+
Find.find(mapping.dest) do |path|
|
|
281
|
+
next if path == mapping.dest
|
|
282
|
+
next if File.directory?(path)
|
|
283
|
+
next unless mapping.include?(path)
|
|
284
|
+
|
|
285
|
+
files << Pathname.new(path).relative_path_from(Pathname.new(mapping.dest)).to_s
|
|
286
|
+
end
|
|
287
|
+
files
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def all_dest_files(mapping)
|
|
291
|
+
if File.directory?(mapping.dest)
|
|
292
|
+
files = []
|
|
293
|
+
Find.find(mapping.dest) do |path|
|
|
294
|
+
next if File.directory?(path)
|
|
295
|
+
|
|
296
|
+
files << path
|
|
297
|
+
end
|
|
298
|
+
files
|
|
299
|
+
elsif File.file?(mapping.dest)
|
|
300
|
+
[mapping.dest]
|
|
301
|
+
else
|
|
302
|
+
[]
|
|
303
|
+
end
|
|
304
|
+
end
|
|
220
305
|
end
|
|
221
306
|
end
|
|
@@ -6,6 +6,7 @@ module Dotsync
|
|
|
6
6
|
include OutputSections
|
|
7
7
|
|
|
8
8
|
def_delegator :@config, :backups_root
|
|
9
|
+
def_delegator :@config, :manifests_xdg_data_home
|
|
9
10
|
|
|
10
11
|
def execute(options = {})
|
|
11
12
|
output_sections = compute_output_sections(options)
|
|
@@ -16,7 +17,7 @@ module Dotsync
|
|
|
16
17
|
show_mappings if output_sections[:mappings]
|
|
17
18
|
show_differences_legend if has_differences? && output_sections[:differences_legend]
|
|
18
19
|
show_differences(diff_content: output_sections[:diff_content]) if output_sections[:differences]
|
|
19
|
-
show_hooks_preview if output_sections[:differences]
|
|
20
|
+
show_hooks_preview(force: options[:force_hooks]) if output_sections[:differences]
|
|
20
21
|
|
|
21
22
|
return unless options[:apply]
|
|
22
23
|
|
|
@@ -33,7 +34,8 @@ module Dotsync
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
transfer_mappings
|
|
36
|
-
|
|
37
|
+
cleanup_orphans
|
|
38
|
+
execute_hooks(force: options[:force_hooks])
|
|
37
39
|
action("Mappings pulled", icon: :done)
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -14,7 +14,7 @@ module Dotsync
|
|
|
14
14
|
show_mappings if output_sections[:mappings]
|
|
15
15
|
show_differences_legend if has_differences? && output_sections[:differences_legend]
|
|
16
16
|
show_differences(diff_content: output_sections[:diff_content]) if output_sections[:differences]
|
|
17
|
-
show_hooks_preview if output_sections[:differences]
|
|
17
|
+
show_hooks_preview(force: options[:force_hooks]) if output_sections[:differences]
|
|
18
18
|
|
|
19
19
|
return unless options[:apply]
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ module Dotsync
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
transfer_mappings
|
|
27
|
-
execute_hooks
|
|
27
|
+
execute_hooks(force: options[:force_hooks])
|
|
28
28
|
action("Mappings pushed", icon: :done)
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -147,6 +147,7 @@ module Dotsync
|
|
|
147
147
|
base["force"] = mapping["force"] if mapping.key?("force")
|
|
148
148
|
base["ignore"] = mapping["ignore"] if mapping.key?("ignore")
|
|
149
149
|
base["only"] = only if only
|
|
150
|
+
base["sync_type"] = shorthand_type
|
|
150
151
|
|
|
151
152
|
# Resolve hooks for direction
|
|
152
153
|
if mapping.key?("hooks")
|
|
@@ -5,7 +5,7 @@ require_relative "../core"
|
|
|
5
5
|
|
|
6
6
|
# Gems needed for pull
|
|
7
7
|
require "toml-rb"
|
|
8
|
-
|
|
8
|
+
require_relative "../utils/table_renderer"
|
|
9
9
|
|
|
10
10
|
# Utils needed for pull
|
|
11
11
|
require_relative "../utils/file_transfer"
|
|
@@ -14,6 +14,7 @@ require_relative "../utils/config_cache"
|
|
|
14
14
|
require_relative "../utils/content_diff"
|
|
15
15
|
require_relative "../utils/parallel"
|
|
16
16
|
require_relative "../utils/hook_runner"
|
|
17
|
+
require_relative "../utils/manifest"
|
|
17
18
|
|
|
18
19
|
# Models
|
|
19
20
|
require_relative "../models/mapping"
|
|
@@ -21,7 +21,7 @@ module Dotsync
|
|
|
21
21
|
class Mapping
|
|
22
22
|
include Dotsync::PathUtils
|
|
23
23
|
|
|
24
|
-
attr_reader :original_src, :original_dest, :original_ignores, :original_only
|
|
24
|
+
attr_reader :original_src, :original_dest, :original_ignores, :original_only, :sync_type
|
|
25
25
|
|
|
26
26
|
def initialize(attributes)
|
|
27
27
|
@original_src = attributes["src"]
|
|
@@ -30,6 +30,7 @@ module Dotsync
|
|
|
30
30
|
@original_only = Array(attributes["only"])
|
|
31
31
|
@force = attributes["force"] || false
|
|
32
32
|
@hooks = Array(attributes["hooks"])
|
|
33
|
+
@sync_type = attributes["sync_type"]
|
|
33
34
|
|
|
34
35
|
@sanitized_src, @sanitized_dest, @sanitized_ignores, @sanitized_only = process_paths(
|
|
35
36
|
@original_src,
|
|
@@ -65,6 +66,27 @@ module Dotsync
|
|
|
65
66
|
@hooks.any?
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
def has_inclusions?
|
|
70
|
+
@original_only.any?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def manifest_key
|
|
74
|
+
return nil unless @sync_type
|
|
75
|
+
|
|
76
|
+
shorthand_base = SyncMappings::SHORTHANDS.dig(@sync_type, :local)
|
|
77
|
+
return @sync_type unless shorthand_base
|
|
78
|
+
|
|
79
|
+
expanded_base = sanitize_path(shorthand_base)
|
|
80
|
+
if dest == expanded_base
|
|
81
|
+
@sync_type
|
|
82
|
+
elsif dest.start_with?("#{expanded_base}/")
|
|
83
|
+
subpath = dest.delete_prefix("#{expanded_base}/")
|
|
84
|
+
"#{@sync_type}--#{subpath}"
|
|
85
|
+
else
|
|
86
|
+
@sync_type
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
68
90
|
def directories?
|
|
69
91
|
File.directory?(src) && File.directory?(dest)
|
|
70
92
|
end
|
|
@@ -211,10 +233,6 @@ module Dotsync
|
|
|
211
233
|
@original_ignores.any?
|
|
212
234
|
end
|
|
213
235
|
|
|
214
|
-
def has_inclusions?
|
|
215
|
-
@original_only.any?
|
|
216
|
-
end
|
|
217
|
-
|
|
218
236
|
def process_paths(raw_src, raw_dest, raw_ignores, raw_only)
|
|
219
237
|
sanitized_src = sanitize_path(raw_src)
|
|
220
238
|
sanitized_dest = sanitize_path(raw_dest)
|
|
@@ -7,6 +7,7 @@ require_relative "config_merger"
|
|
|
7
7
|
module Dotsync
|
|
8
8
|
class ConfigCache
|
|
9
9
|
include Dotsync::XDGBaseDirectory
|
|
10
|
+
include Dotsync::PathUtils
|
|
10
11
|
|
|
11
12
|
def initialize(config_path)
|
|
12
13
|
@config_path = File.expand_path(config_path)
|
|
@@ -50,6 +51,15 @@ module Dotsync
|
|
|
50
51
|
cache_age_days = (Time.now.to_f - meta["cached_at"]) / 86400
|
|
51
52
|
return false if cache_age_days > 7
|
|
52
53
|
|
|
54
|
+
# Check source file validity if present
|
|
55
|
+
if meta["source_file_path"]
|
|
56
|
+
return false unless File.exist?(meta["source_file_path"])
|
|
57
|
+
|
|
58
|
+
source_file_stat = File.stat(meta["source_file_path"])
|
|
59
|
+
return false if source_file_stat.mtime.to_f != meta["source_file_mtime"]
|
|
60
|
+
return false if source_file_stat.size != meta["source_file_size"]
|
|
61
|
+
end
|
|
62
|
+
|
|
53
63
|
# Check include file validity if present
|
|
54
64
|
if meta["include_path"]
|
|
55
65
|
return false unless File.exist?(meta["include_path"])
|
|
@@ -82,13 +92,62 @@ module Dotsync
|
|
|
82
92
|
|
|
83
93
|
def resolve_config
|
|
84
94
|
raw = parse_toml
|
|
85
|
-
|
|
95
|
+
if raw.key?("source")
|
|
96
|
+
resolve_source(raw)
|
|
97
|
+
else
|
|
98
|
+
@merger = ConfigMerger.new(raw, @config_path)
|
|
99
|
+
@merger.resolve
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def resolve_source(raw)
|
|
104
|
+
validate_source!(raw)
|
|
105
|
+
@source_path = resolve_source_path(raw["source"])
|
|
106
|
+
validate_source_exists!
|
|
107
|
+
|
|
108
|
+
source_raw = parse_toml_file(@source_path)
|
|
109
|
+
validate_no_chained_source!(source_raw)
|
|
110
|
+
|
|
111
|
+
@merger = ConfigMerger.new(source_raw, @source_path)
|
|
86
112
|
@merger.resolve
|
|
87
113
|
end
|
|
88
114
|
|
|
115
|
+
def validate_source!(raw)
|
|
116
|
+
unless raw["source"].is_a?(String)
|
|
117
|
+
raise ConfigError, "Config Error: 'source' must be a string path, got #{raw["source"].class}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if raw.keys.any? { |k| k != "source" }
|
|
121
|
+
raise ConfigError,
|
|
122
|
+
"Config Error: 'source' cannot be combined with other keys. " \
|
|
123
|
+
"The source file should contain the full configuration."
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def resolve_source_path(source_value)
|
|
128
|
+
expanded = expand_env_vars(source_value)
|
|
129
|
+
File.expand_path(expanded)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def validate_source_exists!
|
|
133
|
+
unless File.exist?(@source_path)
|
|
134
|
+
raise ConfigError, "Config Error: Source file not found: #{@source_path}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def validate_no_chained_source!(config)
|
|
139
|
+
if config.key?("source")
|
|
140
|
+
raise ConfigError, "Config Error: Chained sources are not supported (found 'source' in #{@source_path})"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
89
144
|
def parse_toml
|
|
145
|
+
parse_toml_file(@config_path)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def parse_toml_file(path)
|
|
90
149
|
require "toml-rb"
|
|
91
|
-
TomlRB.load_file(
|
|
150
|
+
TomlRB.load_file(path)
|
|
92
151
|
end
|
|
93
152
|
|
|
94
153
|
def build_metadata
|
|
@@ -101,6 +160,13 @@ module Dotsync
|
|
|
101
160
|
dotsync_version: Dotsync::VERSION
|
|
102
161
|
}
|
|
103
162
|
|
|
163
|
+
if @source_path
|
|
164
|
+
source_file_stat = File.stat(@source_path)
|
|
165
|
+
meta[:source_file_path] = @source_path
|
|
166
|
+
meta[:source_file_mtime] = source_file_stat.mtime.to_f
|
|
167
|
+
meta[:source_file_size] = source_file_stat.size
|
|
168
|
+
end
|
|
169
|
+
|
|
104
170
|
if @merger&.include_path
|
|
105
171
|
include_stat = File.stat(@merger.include_path)
|
|
106
172
|
meta[:include_path] = @merger.include_path
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Dotsync
|
|
6
|
+
class Manifest
|
|
7
|
+
MANIFESTS_DIR = "dotsync/manifests"
|
|
8
|
+
|
|
9
|
+
# @param dest_dir [String] the mapping's destination directory (absolute)
|
|
10
|
+
# @param key [String] manifest filename key (e.g., "xdg_bin")
|
|
11
|
+
# @param xdg_data_home [String] base path for manifest storage
|
|
12
|
+
def initialize(dest_dir:, key:, xdg_data_home:)
|
|
13
|
+
@dest_dir = dest_dir
|
|
14
|
+
@key = key
|
|
15
|
+
@manifest_path = File.join(xdg_data_home, MANIFESTS_DIR, "#{key}.json")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns array of relative file paths from stored manifest
|
|
19
|
+
# @return [Array<String>]
|
|
20
|
+
def read
|
|
21
|
+
return [] unless File.exist?(@manifest_path)
|
|
22
|
+
|
|
23
|
+
data = JSON.parse(File.read(@manifest_path))
|
|
24
|
+
Array(data["files"])
|
|
25
|
+
rescue JSON::ParserError
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Writes current file list to manifest
|
|
30
|
+
# @param files [Array<String>] relative file paths
|
|
31
|
+
def write(files)
|
|
32
|
+
FileUtils.mkdir_p(File.dirname(@manifest_path))
|
|
33
|
+
File.write(@manifest_path, JSON.pretty_generate({ "files" => files.sort }))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns orphan absolute paths: files in old manifest but not in current_files
|
|
37
|
+
# @param current_files [Array<String>] relative file paths currently synced
|
|
38
|
+
# @return [Array<String>] absolute paths ready for deletion
|
|
39
|
+
def orphans(current_files)
|
|
40
|
+
previous = read
|
|
41
|
+
orphaned = previous - current_files
|
|
42
|
+
orphaned.map { |file| File.join(@dest_dir, file) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "terminal-table"
|
|
4
|
+
|
|
5
|
+
module Dotsync
|
|
6
|
+
class TableRenderer
|
|
7
|
+
attr_reader :rows
|
|
8
|
+
|
|
9
|
+
def initialize(rows:, headings: nil)
|
|
10
|
+
@rows = rows
|
|
11
|
+
@headings = headings
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render
|
|
15
|
+
options = { rows: @rows }
|
|
16
|
+
options[:headings] = @headings if @headings
|
|
17
|
+
Terminal::Table.new(**options).to_s
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/dotsync/version.rb
CHANGED
data/lib/dotsync.rb
CHANGED
|
@@ -11,7 +11,7 @@ require "logger"
|
|
|
11
11
|
require "forwardable" # Ruby standard library
|
|
12
12
|
require "ostruct"
|
|
13
13
|
require "find"
|
|
14
|
-
|
|
14
|
+
require_relative "dotsync/utils/table_renderer"
|
|
15
15
|
|
|
16
16
|
# Base classes
|
|
17
17
|
require_relative "dotsync/errors"
|
|
@@ -34,6 +34,7 @@ require_relative "dotsync/utils/config_cache"
|
|
|
34
34
|
require_relative "dotsync/utils/config_merger"
|
|
35
35
|
require_relative "dotsync/utils/parallel"
|
|
36
36
|
require_relative "dotsync/utils/hook_runner"
|
|
37
|
+
require_relative "dotsync/utils/manifest"
|
|
37
38
|
|
|
38
39
|
# Models
|
|
39
40
|
require_relative "dotsync/models/mapping"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dotsync
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Sáenz
|
|
@@ -340,8 +340,10 @@ files:
|
|
|
340
340
|
- lib/dotsync/utils/file_transfer.rb
|
|
341
341
|
- lib/dotsync/utils/hook_runner.rb
|
|
342
342
|
- lib/dotsync/utils/logger.rb
|
|
343
|
+
- lib/dotsync/utils/manifest.rb
|
|
343
344
|
- lib/dotsync/utils/parallel.rb
|
|
344
345
|
- lib/dotsync/utils/path_utils.rb
|
|
346
|
+
- lib/dotsync/utils/table_renderer.rb
|
|
345
347
|
- lib/dotsync/utils/version_checker.rb
|
|
346
348
|
- lib/dotsync/version.rb
|
|
347
349
|
homepage: https://github.com/dsaenztagarro/dotsync
|