dotsync 0.3.3 → 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 +13 -0
- data/Gemfile.lock +1 -1
- data/README.md +50 -0
- data/lib/dotsync/actions/concerns/mappings_transfer.rb +65 -8
- data/lib/dotsync/actions/pull_action.rb +2 -0
- 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,15 @@
|
|
|
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
|
+
|
|
1
13
|
## [0.3.3] - 2026-02-16
|
|
2
14
|
|
|
3
15
|
### Added
|
|
@@ -461,6 +473,7 @@ Add gem executables
|
|
|
461
473
|
|
|
462
474
|
Initial version
|
|
463
475
|
|
|
476
|
+
[0.4.0]: https://github.com/dsaenztagarro/dotsync/compare/v0.3.3...v0.4.0
|
|
464
477
|
[0.3.3]: https://github.com/dsaenztagarro/dotsync/compare/v0.3.2...v0.3.3
|
|
465
478
|
[0.3.2]: https://github.com/dsaenztagarro/dotsync/compare/v0.3.1...v0.3.2
|
|
466
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:
|
|
@@ -48,15 +48,13 @@ module Dotsync
|
|
|
48
48
|
info("Environment variables:", icon: :env_vars)
|
|
49
49
|
|
|
50
50
|
rows = env_vars.map { |env_var| [env_var, ENV[env_var]] }.sort_by(&:first)
|
|
51
|
-
|
|
52
|
-
logger.log(table)
|
|
51
|
+
logger.log(Dotsync::TableRenderer.new(rows: rows).render)
|
|
53
52
|
logger.log("")
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
def show_mappings_legend
|
|
57
56
|
info("Mappings Legend:", icon: :legend)
|
|
58
|
-
|
|
59
|
-
logger.log(table)
|
|
57
|
+
logger.log(Dotsync::TableRenderer.new(rows: MAPPINGS_LEGEND).render)
|
|
60
58
|
logger.log("")
|
|
61
59
|
end
|
|
62
60
|
|
|
@@ -70,15 +68,13 @@ module Dotsync
|
|
|
70
68
|
colorize_env_vars(mapping.original_dest)
|
|
71
69
|
]
|
|
72
70
|
end
|
|
73
|
-
|
|
74
|
-
logger.log(table)
|
|
71
|
+
logger.log(Dotsync::TableRenderer.new(headings: ["Flags", "Source", "Destination"], rows: rows).render)
|
|
75
72
|
logger.log("")
|
|
76
73
|
end
|
|
77
74
|
|
|
78
75
|
def show_differences_legend
|
|
79
76
|
info("Differences Legend:", icon: :legend)
|
|
80
|
-
|
|
81
|
-
logger.log(table)
|
|
77
|
+
logger.log(Dotsync::TableRenderer.new(rows: DIFFERENCES_LEGEND).render)
|
|
82
78
|
logger.log("")
|
|
83
79
|
end
|
|
84
80
|
|
|
@@ -97,6 +93,8 @@ module Dotsync
|
|
|
97
93
|
logger.log("")
|
|
98
94
|
|
|
99
95
|
show_content_diffs if diff_content && has_modifications?
|
|
96
|
+
|
|
97
|
+
show_orphan_preview if respond_to?(:manifests_xdg_data_home, true)
|
|
100
98
|
end
|
|
101
99
|
|
|
102
100
|
def show_content_diffs
|
|
@@ -138,6 +136,28 @@ module Dotsync
|
|
|
138
136
|
end
|
|
139
137
|
end
|
|
140
138
|
|
|
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
|
+
|
|
141
161
|
def execute_hooks(force: false)
|
|
142
162
|
valid_mappings.each_with_index do |mapping, idx|
|
|
143
163
|
next unless mapping.has_hooks?
|
|
@@ -230,6 +250,43 @@ module Dotsync
|
|
|
230
250
|
mappings.select(&:valid?)
|
|
231
251
|
end
|
|
232
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
|
+
|
|
233
290
|
def all_dest_files(mapping)
|
|
234
291
|
if File.directory?(mapping.dest)
|
|
235
292
|
files = []
|
|
@@ -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)
|
|
@@ -33,6 +34,7 @@ module Dotsync
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
transfer_mappings
|
|
37
|
+
cleanup_orphans
|
|
36
38
|
execute_hooks(force: options[:force_hooks])
|
|
37
39
|
action("Mappings pulled", icon: :done)
|
|
38
40
|
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
|