dotsync 0.2.1 → 0.2.3
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 +50 -0
- data/Gemfile.lock +1 -1
- data/README.md +105 -4
- data/lib/dotsync/actions/concerns/mappings_transfer.rb +39 -2
- data/lib/dotsync/actions/pull_action.rb +2 -0
- data/lib/dotsync/actions/push_action.rb +2 -0
- data/lib/dotsync/config/concerns/sync_mappings.rb +42 -0
- data/lib/dotsync/config/pull_action_config.rb +19 -1
- data/lib/dotsync/config/push_action_config.rb +19 -1
- data/lib/dotsync/errors.rb +1 -0
- data/lib/dotsync/icons.rb +7 -0
- data/lib/dotsync/loaders/pull_loader.rb +1 -0
- data/lib/dotsync/loaders/push_loader.rb +1 -0
- data/lib/dotsync/models/mapping.rb +30 -2
- data/lib/dotsync/utils/hook_runner.rb +48 -0
- data/lib/dotsync/version.rb +1 -1
- data/lib/dotsync.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c060e4f84252048c1b038e350efcf900bc5b62b6bab5285dc9b1a2a9d0d0da0f
|
|
4
|
+
data.tar.gz: 5bef34fde923690c3ffaf68b3d5ed77d2195006d91d8b4f2c59853533c285339
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7049966bedd67e25b008ad9a7bae1697f54d189dc7f666bd42495a242af68b8c9494ead4c3e5310e500ce516db47270d5ae0f0382035b50df5d09543a631192f
|
|
7
|
+
data.tar.gz: 7abedab0ed8d49946e5b832ad38390968f17c6a7e32da3a3d774a47117767f1eec9dc3cbefd20a2f815eed5b091dcab73c4e9612b21d67fb1b7f85a0b8499ed1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
|
+
## [0.2.3] - 2026-02-08
|
|
2
|
+
|
|
3
|
+
**New Features:**
|
|
4
|
+
- Add per-mapping post-sync hooks that run commands after files are transferred
|
|
5
|
+
- `post_sync` — runs after sync in both directions (`[[sync]]` mappings)
|
|
6
|
+
- `post_push` — runs only after push (`[[sync]]` and `[[push]]` mappings)
|
|
7
|
+
- `post_pull` — runs only after pull (`[[sync]]` and `[[pull]]` mappings)
|
|
8
|
+
- Template variables: `{files}` (shell-quoted changed dest paths), `{src}`, `{dest}`
|
|
9
|
+
- Hooks only execute when files actually changed and only with `--apply`
|
|
10
|
+
- Hook failures log errors but do not abort remaining hooks or mappings
|
|
11
|
+
- Preview mode shows what commands would run without executing
|
|
12
|
+
- Add `HookError` error class for hook-related errors
|
|
13
|
+
- Add hook icon () to mappings legend with custom icon support
|
|
14
|
+
|
|
15
|
+
**New Files:**
|
|
16
|
+
- `lib/dotsync/utils/hook_runner.rb` — HookRunner utility with execute, preview, and template expansion
|
|
17
|
+
- `spec/dotsync/utils/hook_runner_spec.rb` — Comprehensive tests for HookRunner
|
|
18
|
+
|
|
19
|
+
**Documentation:**
|
|
20
|
+
- Add "Post-Sync Hooks" section to README with hook types, examples, template variables, and real-world use cases
|
|
21
|
+
- Add post-sync hooks to Key Features and Table of Contents
|
|
22
|
+
|
|
23
|
+
**Testing:**
|
|
24
|
+
- Add 35 new test examples covering hooks across all layers
|
|
25
|
+
- HookRunner: template expansion, multiple commands, failure handling, shell-escaped paths, preview mode
|
|
26
|
+
- Mapping: hooks attribute, has_hooks?, hook icon display
|
|
27
|
+
- SyncMappings: direction resolution, array concatenation, shorthand hooks, validation of invalid keys
|
|
28
|
+
- PushActionConfig/PullActionConfig: hook extraction, validation of unidirectional constraints
|
|
29
|
+
- PushAction/PullAction: hook execution with changes, skipped without changes, skipped in dry-run
|
|
30
|
+
- Total: 467 examples, 0 failures | Line: 96.45% | Branch: 82.86%
|
|
31
|
+
|
|
32
|
+
## [0.2.2] - 2025-02-07
|
|
33
|
+
|
|
34
|
+
**New Features:**
|
|
35
|
+
- Add glob pattern support to `only` filter (#15)
|
|
36
|
+
- `*` matches any sequence of characters (e.g., `local.*.plist`)
|
|
37
|
+
- `?` matches any single character (e.g., `config.?`)
|
|
38
|
+
- `[charset]` matches any character in the set (e.g., `log.[0-9]`)
|
|
39
|
+
- Glob and exact paths can be mixed in the same `only` array
|
|
40
|
+
- Non-glob entries retain existing exact path matching behavior
|
|
41
|
+
|
|
42
|
+
**Documentation:**
|
|
43
|
+
- Document glob pattern support in README with examples
|
|
44
|
+
- Add "Glob patterns" to the `only` option important behaviors section
|
|
45
|
+
|
|
46
|
+
**Testing:**
|
|
47
|
+
- Add unit tests for glob matching in `include?`, `bidirectional_include?`, `skip?`, `should_prune_directory?`
|
|
48
|
+
- Add integration tests for glob patterns in FileTransfer (including force mode)
|
|
49
|
+
- Add integration tests for glob patterns in DirectoryDiffer
|
|
50
|
+
- All 432 tests pass with 96.29% line coverage
|
|
1
51
|
## [0.2.1] - 2025-02-06
|
|
2
52
|
|
|
3
53
|
**Performance Optimizations:**
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Dotsync is a powerful Ruby gem for managing and synchronizing your dotfiles acro
|
|
|
19
19
|
- **Smart Filtering**: Use `force`, `only`, and `ignore` options to precisely control what gets synced
|
|
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
|
+
- **Post-Sync Hooks**: Run commands automatically after files change (e.g., codesigning, chmod, service reload)
|
|
22
23
|
- **Customizable Output**: Control verbosity and customize icons to match your preferences
|
|
23
24
|
- **Auto-Updates**: Get notified when new versions are available
|
|
24
25
|
|
|
@@ -33,6 +34,7 @@ Dotsync is a powerful Ruby gem for managing and synchronizing your dotfiles acro
|
|
|
33
34
|
- [Bidirectional Sync Mappings (Recommended)](#bidirectional-sync-mappings-recommended)
|
|
34
35
|
- [Alternative: Unidirectional Mappings](#alternative-unidirectional-mappings)
|
|
35
36
|
- [Mapping Options (force, only, ignore)](#force-only-and-ignore-options-in-mappings)
|
|
37
|
+
- [Post-Sync Hooks](#post-sync-hooks)
|
|
36
38
|
- [Safety Features](#safety-features)
|
|
37
39
|
- [Customizing Icons](#customizing-icons)
|
|
38
40
|
- [Automatic Update Checks](#automatic-update-checks)
|
|
@@ -401,11 +403,11 @@ ignore = ["lazy-lock.json"]
|
|
|
401
403
|
|
|
402
404
|
##### `only` Option
|
|
403
405
|
|
|
404
|
-
An array of relative paths (files or directories) to selectively transfer from the source. This option provides precise control over which files get synchronized.
|
|
406
|
+
An array of relative paths (files or directories) or glob patterns to selectively transfer from the source. This option provides precise control over which files get synchronized.
|
|
405
407
|
|
|
406
408
|
**How it works:**
|
|
407
409
|
- Paths are relative to the `src` directory
|
|
408
|
-
- You can specify entire directories or
|
|
410
|
+
- You can specify entire directories, individual files, or glob patterns (`*`, `?`, `[charset]`)
|
|
409
411
|
- Parent directories are automatically created as needed
|
|
410
412
|
- Other files in the source are ignored
|
|
411
413
|
- With `force = true`, only files matching the `only` filter are cleaned up in the destination
|
|
@@ -437,7 +439,27 @@ This transfers only specific configuration files from different subdirectories:
|
|
|
437
439
|
|
|
438
440
|
The parent directories (`bundle/`, `ghc/`, `cabal/`) are created automatically in the destination, but other files in those directories are not transferred.
|
|
439
441
|
|
|
440
|
-
**Example 4:
|
|
442
|
+
**Example 4: Glob patterns**
|
|
443
|
+
```toml
|
|
444
|
+
[[sync.home]]
|
|
445
|
+
path = "Library/LaunchAgents"
|
|
446
|
+
only = ["local.*.plist"]
|
|
447
|
+
```
|
|
448
|
+
This transfers only files matching the glob pattern — e.g., `local.brew.upgrade.plist`, `local.ollama.plist` — while ignoring system-generated plists like `com.apple.*.plist`.
|
|
449
|
+
|
|
450
|
+
Supported glob characters:
|
|
451
|
+
- `*` — matches any sequence of characters (e.g., `local.*.plist`)
|
|
452
|
+
- `?` — matches any single character (e.g., `config.?`)
|
|
453
|
+
- `[charset]` — matches any character in the set (e.g., `log.[0-9]`)
|
|
454
|
+
|
|
455
|
+
Glob patterns can be mixed with exact paths in the same `only` array:
|
|
456
|
+
```toml
|
|
457
|
+
[[sync.home]]
|
|
458
|
+
path = "Library/LaunchAgents"
|
|
459
|
+
only = ["local.*.plist", "README.md"]
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Example 5: Deeply nested paths**
|
|
441
463
|
```toml
|
|
442
464
|
[[sync.xdg_config]]
|
|
443
465
|
only = ["nvim/lua/plugins/init.lua", "nvim/lua/config/settings.lua"]
|
|
@@ -447,7 +469,8 @@ This transfers only specific Lua files from deeply nested paths within the nvim
|
|
|
447
469
|
**Important behaviors:**
|
|
448
470
|
- **File-specific paths**: When specifying individual files (e.g., `"bundle/config"`), only that file is managed. Sibling files in the same directory are not affected, even with `force = true`.
|
|
449
471
|
- **Directory paths**: When specifying directories (e.g., `"nvim"`), all contents of that directory are managed, including subdirectories.
|
|
450
|
-
- **
|
|
472
|
+
- **Glob patterns**: When using patterns (e.g., `"local.*.plist"`), only files whose names match the pattern are managed. Non-matching files in the same directory are untouched.
|
|
473
|
+
- **Combining with `force`**: With `force = true` and directory paths, files in the destination directory that don't exist in the source are removed. With file-specific paths or glob patterns, only matching files are managed.
|
|
451
474
|
|
|
452
475
|
##### `ignore` Option
|
|
453
476
|
|
|
@@ -478,6 +501,84 @@ This configuration:
|
|
|
478
501
|
|
|
479
502
|
These options apply when the source is a directory and are relevant for both `push` and `pull` operations.
|
|
480
503
|
|
|
504
|
+
#### Post-Sync Hooks
|
|
505
|
+
|
|
506
|
+
Hooks let you run commands automatically after a mapping's files are transferred. Hooks only execute when files actually changed, and only when using `--apply`.
|
|
507
|
+
|
|
508
|
+
##### Hook Types
|
|
509
|
+
|
|
510
|
+
| Hook | Description | Valid in |
|
|
511
|
+
|------|-------------|----------|
|
|
512
|
+
| `post_sync` | Runs after sync in both directions | `[[sync]]` mappings |
|
|
513
|
+
| `post_push` | Runs only after push | `[[sync]]` and `[[push]]` mappings |
|
|
514
|
+
| `post_pull` | Runs only after pull | `[[sync]]` and `[[pull]]` mappings |
|
|
515
|
+
|
|
516
|
+
For sync mappings, hooks are resolved by direction:
|
|
517
|
+
- **Push**: `post_sync` + `post_push` commands
|
|
518
|
+
- **Pull**: `post_sync` + `post_pull` commands
|
|
519
|
+
|
|
520
|
+
##### Examples
|
|
521
|
+
|
|
522
|
+
**Single command (shorthand mapping):**
|
|
523
|
+
```toml
|
|
524
|
+
[[sync.xdg_bin]]
|
|
525
|
+
force = true
|
|
526
|
+
|
|
527
|
+
[sync.xdg_bin.hooks]
|
|
528
|
+
post_sync = "codesign -s - {files}"
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Multiple commands (explicit sync):**
|
|
532
|
+
```toml
|
|
533
|
+
[[sync.mappings]]
|
|
534
|
+
local = "$XDG_CONFIG_HOME/scripts"
|
|
535
|
+
remote = "$XDG_CONFIG_HOME_MIRROR/scripts"
|
|
536
|
+
|
|
537
|
+
[sync.mappings.hooks]
|
|
538
|
+
post_sync = ["codesign -s - {files}", "chmod 700 {files}"]
|
|
539
|
+
post_pull = "launchctl kickstart -k gui/$(id -u)/com.example.service"
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**Unidirectional mapping:**
|
|
543
|
+
```toml
|
|
544
|
+
[[pull.mappings]]
|
|
545
|
+
src = "$DOTFILES_DIR/scripts"
|
|
546
|
+
dest = "$HOME/Scripts"
|
|
547
|
+
|
|
548
|
+
[pull.mappings.hooks]
|
|
549
|
+
post_pull = ["codesign -s - {files}", "chmod 700 {files}"]
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
##### Template Variables
|
|
553
|
+
|
|
554
|
+
| Variable | Description |
|
|
555
|
+
|----------|-------------|
|
|
556
|
+
| `{files}` | Shell-quoted paths of changed destination files |
|
|
557
|
+
| `{src}` | The mapping's source path |
|
|
558
|
+
| `{dest}` | The mapping's destination path |
|
|
559
|
+
|
|
560
|
+
##### Real-World Examples
|
|
561
|
+
|
|
562
|
+
```toml
|
|
563
|
+
# Codesign scripts after pulling (macOS Ventura+ requirement for LaunchAgents)
|
|
564
|
+
[[sync.xdg_bin]]
|
|
565
|
+
force = true
|
|
566
|
+
|
|
567
|
+
[sync.xdg_bin.hooks]
|
|
568
|
+
post_pull = "codesign -s - {files}"
|
|
569
|
+
|
|
570
|
+
# Reload a LaunchAgent after pulling config changes
|
|
571
|
+
[[pull.mappings]]
|
|
572
|
+
src = "$DOTFILES_DIR/LaunchAgents/com.example.plist"
|
|
573
|
+
dest = "$HOME/Library/LaunchAgents/com.example.plist"
|
|
574
|
+
|
|
575
|
+
[pull.mappings.hooks]
|
|
576
|
+
post_pull = "launchctl kickstart -k gui/$(id -u)/com.example"
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
> [!NOTE]
|
|
580
|
+
> In preview mode (without `--apply`), hooks are displayed as a preview showing what commands would run. Hook failures log errors but do not abort remaining hooks or mappings.
|
|
581
|
+
|
|
481
582
|
### Safety Features
|
|
482
583
|
|
|
483
584
|
Dotsync includes several safety mechanisms to prevent accidental data loss:
|
|
@@ -23,8 +23,9 @@ module Dotsync
|
|
|
23
23
|
|
|
24
24
|
MAPPINGS_LEGEND = [
|
|
25
25
|
[Icons.force, "The source will overwrite the destination"],
|
|
26
|
-
[Icons.only, "
|
|
27
|
-
[Icons.ignore, "
|
|
26
|
+
[Icons.only, "Filtered by 'only' whitelist"],
|
|
27
|
+
[Icons.ignore, "Filtered by 'ignore' blacklist"],
|
|
28
|
+
[Icons.hook, "Post-sync hooks configured"],
|
|
28
29
|
[Icons.invalid, "Invalid paths detected in the source or destination"]
|
|
29
30
|
]
|
|
30
31
|
|
|
@@ -135,6 +136,42 @@ module Dotsync
|
|
|
135
136
|
end
|
|
136
137
|
end
|
|
137
138
|
|
|
139
|
+
def execute_hooks
|
|
140
|
+
valid_mappings.each_with_index do |mapping, idx|
|
|
141
|
+
next unless mapping.has_hooks?
|
|
142
|
+
|
|
143
|
+
differ = differs[idx]
|
|
144
|
+
changed_files = differ.additions + differ.modifications
|
|
145
|
+
next if changed_files.empty?
|
|
146
|
+
|
|
147
|
+
runner = Dotsync::HookRunner.new(mapping: mapping, changed_files: changed_files, logger: logger)
|
|
148
|
+
runner.execute
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def show_hooks_preview
|
|
153
|
+
hooks_to_run = []
|
|
154
|
+
|
|
155
|
+
valid_mappings.each_with_index do |mapping, idx|
|
|
156
|
+
next unless mapping.has_hooks?
|
|
157
|
+
|
|
158
|
+
differ = differs[idx]
|
|
159
|
+
changed_files = differ.additions + differ.modifications
|
|
160
|
+
next if changed_files.empty?
|
|
161
|
+
|
|
162
|
+
runner = Dotsync::HookRunner.new(mapping: mapping, changed_files: changed_files, logger: logger)
|
|
163
|
+
hooks_to_run.concat(runner.preview)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
return if hooks_to_run.empty?
|
|
167
|
+
|
|
168
|
+
info("Hooks to run:", icon: :hook)
|
|
169
|
+
hooks_to_run.each do |command|
|
|
170
|
+
logger.log(" #{command}")
|
|
171
|
+
end
|
|
172
|
+
logger.log("")
|
|
173
|
+
end
|
|
174
|
+
|
|
138
175
|
private
|
|
139
176
|
# Computes diffs for all valid mappings.
|
|
140
177
|
#
|
|
@@ -16,6 +16,7 @@ module Dotsync
|
|
|
16
16
|
show_mappings if output_sections[:mappings]
|
|
17
17
|
show_differences_legend if has_differences? && output_sections[:differences_legend]
|
|
18
18
|
show_differences(diff_content: output_sections[:diff_content]) if output_sections[:differences]
|
|
19
|
+
show_hooks_preview if output_sections[:differences]
|
|
19
20
|
|
|
20
21
|
return unless options[:apply]
|
|
21
22
|
|
|
@@ -32,6 +33,7 @@ module Dotsync
|
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
transfer_mappings
|
|
36
|
+
execute_hooks
|
|
35
37
|
action("Mappings pulled", icon: :done)
|
|
36
38
|
end
|
|
37
39
|
|
|
@@ -14,6 +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
18
|
|
|
18
19
|
return unless options[:apply]
|
|
19
20
|
|
|
@@ -23,6 +24,7 @@ module Dotsync
|
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
transfer_mappings
|
|
27
|
+
execute_hooks
|
|
26
28
|
action("Mappings pushed", icon: :done)
|
|
27
29
|
end
|
|
28
30
|
end
|
|
@@ -96,6 +96,12 @@ module Dotsync
|
|
|
96
96
|
base["ignore"] = mapping["ignore"] if mapping.key?("ignore")
|
|
97
97
|
base["only"] = mapping["only"] if mapping.key?("only")
|
|
98
98
|
|
|
99
|
+
# Resolve hooks for direction
|
|
100
|
+
if mapping.key?("hooks")
|
|
101
|
+
resolved = resolve_hooks_for_direction(mapping["hooks"], direction)
|
|
102
|
+
base["hooks"] = resolved if resolved.any?
|
|
103
|
+
end
|
|
104
|
+
|
|
99
105
|
base
|
|
100
106
|
end
|
|
101
107
|
|
|
@@ -142,9 +148,41 @@ module Dotsync
|
|
|
142
148
|
base["ignore"] = mapping["ignore"] if mapping.key?("ignore")
|
|
143
149
|
base["only"] = only if only
|
|
144
150
|
|
|
151
|
+
# Resolve hooks for direction
|
|
152
|
+
if mapping.key?("hooks")
|
|
153
|
+
resolved = resolve_hooks_for_direction(mapping["hooks"], direction)
|
|
154
|
+
base["hooks"] = resolved if resolved.any?
|
|
155
|
+
end
|
|
156
|
+
|
|
145
157
|
base
|
|
146
158
|
end
|
|
147
159
|
|
|
160
|
+
def resolve_hooks_for_direction(raw_hooks, direction)
|
|
161
|
+
return [] unless raw_hooks.is_a?(Hash)
|
|
162
|
+
|
|
163
|
+
hooks = Array(raw_hooks["post_sync"])
|
|
164
|
+
case direction
|
|
165
|
+
when :push
|
|
166
|
+
hooks += Array(raw_hooks["post_push"])
|
|
167
|
+
when :pull
|
|
168
|
+
hooks += Array(raw_hooks["post_pull"])
|
|
169
|
+
end
|
|
170
|
+
hooks
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def validate_hooks!(hooks, context)
|
|
174
|
+
return unless hooks.is_a?(Hash)
|
|
175
|
+
|
|
176
|
+
valid_keys = %w[post_sync post_push post_pull]
|
|
177
|
+
invalid_keys = hooks.keys - valid_keys
|
|
178
|
+
return if invalid_keys.empty?
|
|
179
|
+
|
|
180
|
+
raise Dotsync::ConfigError,
|
|
181
|
+
"Configuration error in #{context}: " \
|
|
182
|
+
"Invalid hook key(s): #{invalid_keys.join(", ")}. " \
|
|
183
|
+
"Valid keys are: #{valid_keys.join(", ")}"
|
|
184
|
+
end
|
|
185
|
+
|
|
148
186
|
def build_path(base, path)
|
|
149
187
|
path ? File.join(base, path) : base
|
|
150
188
|
end
|
|
@@ -171,6 +209,8 @@ module Dotsync
|
|
|
171
209
|
"Configuration error in sync.mappings ##{index + 1}: " \
|
|
172
210
|
"Each mapping must have 'local' and 'remote' keys."
|
|
173
211
|
end
|
|
212
|
+
|
|
213
|
+
validate_hooks!(mapping["hooks"], "sync.mappings ##{index + 1}") if mapping.key?("hooks")
|
|
174
214
|
end
|
|
175
215
|
end
|
|
176
216
|
|
|
@@ -184,6 +224,8 @@ module Dotsync
|
|
|
184
224
|
"Configuration error in sync.#{shorthand_type} ##{index + 1}: " \
|
|
185
225
|
"Each mapping must be a table."
|
|
186
226
|
end
|
|
227
|
+
|
|
228
|
+
validate_hooks!(mapping["hooks"], "sync.#{shorthand_type} ##{index + 1}") if mapping.is_a?(Hash) && mapping.key?("hooks")
|
|
187
229
|
end
|
|
188
230
|
end
|
|
189
231
|
end
|
|
@@ -22,7 +22,15 @@ module Dotsync
|
|
|
22
22
|
|
|
23
23
|
def section_mappings
|
|
24
24
|
return [] unless section && section["mappings"]
|
|
25
|
-
Array(section["mappings"]).map
|
|
25
|
+
Array(section["mappings"]).map do |mapping|
|
|
26
|
+
attrs = mapping.dup
|
|
27
|
+
if attrs.key?("hooks") && attrs["hooks"].is_a?(Hash)
|
|
28
|
+
resolved = Array(attrs["hooks"]["post_pull"])
|
|
29
|
+
attrs["hooks"] = resolved.any? ? resolved : nil
|
|
30
|
+
attrs.delete("hooks") unless attrs["hooks"]
|
|
31
|
+
end
|
|
32
|
+
Dotsync::Mapping.new(attrs)
|
|
33
|
+
end
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
def validate!
|
|
@@ -46,6 +54,16 @@ module Dotsync
|
|
|
46
54
|
unless mapping.is_a?(Hash) && mapping.key?("src") && mapping.key?("dest")
|
|
47
55
|
raise "Configuration error in pull mapping ##{index + 1}: Each mapping must have 'src' and 'dest' keys."
|
|
48
56
|
end
|
|
57
|
+
|
|
58
|
+
if mapping.is_a?(Hash) && mapping.key?("hooks") && mapping["hooks"].is_a?(Hash)
|
|
59
|
+
invalid_keys = mapping["hooks"].keys - ["post_pull"]
|
|
60
|
+
if invalid_keys.any?
|
|
61
|
+
raise Dotsync::ConfigError,
|
|
62
|
+
"Configuration error in pull mapping ##{index + 1}: " \
|
|
63
|
+
"Only 'post_pull' hooks are allowed in [pull] mappings. " \
|
|
64
|
+
"Invalid key(s): #{invalid_keys.join(", ")}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
49
67
|
end
|
|
50
68
|
end
|
|
51
69
|
end
|
|
@@ -17,7 +17,15 @@ module Dotsync
|
|
|
17
17
|
|
|
18
18
|
def section_mappings
|
|
19
19
|
return [] unless section && section["mappings"]
|
|
20
|
-
Array(section["mappings"]).map
|
|
20
|
+
Array(section["mappings"]).map do |mapping|
|
|
21
|
+
attrs = mapping.dup
|
|
22
|
+
if attrs.key?("hooks") && attrs["hooks"].is_a?(Hash)
|
|
23
|
+
resolved = Array(attrs["hooks"]["post_push"])
|
|
24
|
+
attrs["hooks"] = resolved.any? ? resolved : nil
|
|
25
|
+
attrs.delete("hooks") unless attrs["hooks"]
|
|
26
|
+
end
|
|
27
|
+
Dotsync::Mapping.new(attrs)
|
|
28
|
+
end
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
def validate!
|
|
@@ -41,6 +49,16 @@ module Dotsync
|
|
|
41
49
|
unless mapping.is_a?(Hash) && mapping.key?("src") && mapping.key?("dest")
|
|
42
50
|
raise "Configuration error in push mapping ##{index + 1}: Each mapping must have 'src' and 'dest' keys."
|
|
43
51
|
end
|
|
52
|
+
|
|
53
|
+
if mapping.is_a?(Hash) && mapping.key?("hooks") && mapping["hooks"].is_a?(Hash)
|
|
54
|
+
invalid_keys = mapping["hooks"].keys - ["post_push"]
|
|
55
|
+
if invalid_keys.any?
|
|
56
|
+
raise Dotsync::ConfigError,
|
|
57
|
+
"Configuration error in push mapping ##{index + 1}: " \
|
|
58
|
+
"Only 'post_push' hooks are allowed in [push] mappings. " \
|
|
59
|
+
"Invalid key(s): #{invalid_keys.join(", ")}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
44
62
|
end
|
|
45
63
|
end
|
|
46
64
|
end
|
data/lib/dotsync/errors.rb
CHANGED
data/lib/dotsync/icons.rb
CHANGED
|
@@ -18,6 +18,7 @@ module Dotsync
|
|
|
18
18
|
DEFAULT_ONLY = " "
|
|
19
19
|
DEFAULT_IGNORE = " "
|
|
20
20
|
DEFAULT_INVALID = " "
|
|
21
|
+
DEFAULT_HOOK = " "
|
|
21
22
|
|
|
22
23
|
# Default Mappings Differences icons
|
|
23
24
|
DEFAULT_DIFF_CREATED = " "
|
|
@@ -50,6 +51,7 @@ module Dotsync
|
|
|
50
51
|
only: config.dig("icons", "only") || DEFAULT_ONLY,
|
|
51
52
|
ignore: config.dig("icons", "ignore") || DEFAULT_IGNORE,
|
|
52
53
|
invalid: config.dig("icons", "invalid") || DEFAULT_INVALID,
|
|
54
|
+
hook: config.dig("icons", "hook") || DEFAULT_HOOK,
|
|
53
55
|
# Differences Legend
|
|
54
56
|
diff_created: config.dig("icons", "diff_created") || DEFAULT_DIFF_CREATED,
|
|
55
57
|
diff_updated: config.dig("icons", "diff_updated") || DEFAULT_DIFF_UPDATED,
|
|
@@ -75,6 +77,10 @@ module Dotsync
|
|
|
75
77
|
@custom_icons[:invalid] || DEFAULT_INVALID
|
|
76
78
|
end
|
|
77
79
|
|
|
80
|
+
def self.hook
|
|
81
|
+
@custom_icons[:hook] || DEFAULT_HOOK
|
|
82
|
+
end
|
|
83
|
+
|
|
78
84
|
# Differences Legend methods
|
|
79
85
|
|
|
80
86
|
def self.diff_created
|
|
@@ -100,6 +106,7 @@ module Dotsync
|
|
|
100
106
|
diff: DIFF,
|
|
101
107
|
force: -> { force },
|
|
102
108
|
ignore: -> { ignore },
|
|
109
|
+
hook: DEFAULT_HOOK,
|
|
103
110
|
pull: PULL,
|
|
104
111
|
push: PUSH,
|
|
105
112
|
watch: WATCH,
|
|
@@ -13,6 +13,7 @@ require_relative "../utils/directory_differ"
|
|
|
13
13
|
require_relative "../utils/config_cache"
|
|
14
14
|
require_relative "../utils/content_diff"
|
|
15
15
|
require_relative "../utils/parallel"
|
|
16
|
+
require_relative "../utils/hook_runner"
|
|
16
17
|
|
|
17
18
|
# Models
|
|
18
19
|
require_relative "../models/mapping"
|
|
@@ -13,6 +13,7 @@ require_relative "../utils/directory_differ"
|
|
|
13
13
|
require_relative "../utils/config_cache"
|
|
14
14
|
require_relative "../utils/content_diff"
|
|
15
15
|
require_relative "../utils/parallel"
|
|
16
|
+
require_relative "../utils/hook_runner"
|
|
16
17
|
|
|
17
18
|
# Models
|
|
18
19
|
require_relative "../models/mapping"
|
|
@@ -29,6 +29,7 @@ module Dotsync
|
|
|
29
29
|
@original_ignores = Array(attributes["ignore"])
|
|
30
30
|
@original_only = Array(attributes["only"])
|
|
31
31
|
@force = attributes["force"] || false
|
|
32
|
+
@hooks = Array(attributes["hooks"])
|
|
32
33
|
|
|
33
34
|
@sanitized_src, @sanitized_dest, @sanitized_ignores, @sanitized_only = process_paths(
|
|
34
35
|
@original_src,
|
|
@@ -58,6 +59,12 @@ module Dotsync
|
|
|
58
59
|
@force
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
attr_reader :hooks
|
|
63
|
+
|
|
64
|
+
def has_hooks?
|
|
65
|
+
@hooks.any?
|
|
66
|
+
end
|
|
67
|
+
|
|
61
68
|
def directories?
|
|
62
69
|
File.directory?(src) && File.directory?(dest)
|
|
63
70
|
end
|
|
@@ -103,6 +110,7 @@ module Dotsync
|
|
|
103
110
|
msg << Icons.force if force?
|
|
104
111
|
msg << Icons.only if has_inclusions?
|
|
105
112
|
msg << Icons.ignore if has_ignores?
|
|
113
|
+
msg << Icons.hook if has_hooks?
|
|
106
114
|
msg << Icons.invalid unless valid?
|
|
107
115
|
msg.join
|
|
108
116
|
end
|
|
@@ -140,13 +148,13 @@ module Dotsync
|
|
|
140
148
|
def include?(path)
|
|
141
149
|
return true unless has_inclusions?
|
|
142
150
|
return true if path == src
|
|
143
|
-
inclusions.any? { |inclusion|
|
|
151
|
+
inclusions.any? { |inclusion| inclusion_matches?(inclusion, path) }
|
|
144
152
|
end
|
|
145
153
|
|
|
146
154
|
def bidirectional_include?(path)
|
|
147
155
|
return true unless has_inclusions?
|
|
148
156
|
return true if path == src
|
|
149
|
-
inclusions.any? { |inclusion|
|
|
157
|
+
inclusions.any? { |inclusion| inclusion_matches?(inclusion, path) || inclusion_is_ancestor?(path, inclusion) }
|
|
150
158
|
end
|
|
151
159
|
|
|
152
160
|
def ignore?(path)
|
|
@@ -179,6 +187,26 @@ module Dotsync
|
|
|
179
187
|
end
|
|
180
188
|
|
|
181
189
|
private
|
|
190
|
+
def glob_pattern?(path)
|
|
191
|
+
path.match?(/[*?\[]/)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def inclusion_matches?(inclusion, path)
|
|
195
|
+
if glob_pattern?(inclusion)
|
|
196
|
+
File.fnmatch(inclusion, path)
|
|
197
|
+
else
|
|
198
|
+
path_is_parent_or_same?(inclusion, path)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def inclusion_is_ancestor?(path, inclusion)
|
|
203
|
+
if glob_pattern?(inclusion)
|
|
204
|
+
path_is_parent_or_same?(path, File.dirname(inclusion))
|
|
205
|
+
else
|
|
206
|
+
path_is_parent_or_same?(path, inclusion)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
182
210
|
def has_ignores?
|
|
183
211
|
@original_ignores.any?
|
|
184
212
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
6
|
+
module Dotsync
|
|
7
|
+
class HookRunner
|
|
8
|
+
def initialize(mapping:, changed_files:, logger:)
|
|
9
|
+
@mapping = mapping
|
|
10
|
+
@changed_files = changed_files
|
|
11
|
+
@logger = logger
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute
|
|
15
|
+
@mapping.hooks.map do |command|
|
|
16
|
+
expanded = expand_template(command)
|
|
17
|
+
run_command(expanded)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def preview
|
|
22
|
+
@mapping.hooks.map { |command| expand_template(command) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
def expand_template(command)
|
|
27
|
+
files_str = @changed_files.map { |f| Shellwords.escape(f) }.join(" ")
|
|
28
|
+
|
|
29
|
+
command
|
|
30
|
+
.gsub("{files}", files_str)
|
|
31
|
+
.gsub("{src}", @mapping.src)
|
|
32
|
+
.gsub("{dest}", @mapping.dest)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def run_command(command)
|
|
36
|
+
stdout, stderr, status = Open3.capture3(command)
|
|
37
|
+
|
|
38
|
+
if status.success?
|
|
39
|
+
@logger.info("Hook succeeded: #{command}", icon: :hook)
|
|
40
|
+
else
|
|
41
|
+
@logger.error("Hook failed: #{command}")
|
|
42
|
+
@logger.error(" #{stderr.strip}") unless stderr.strip.empty?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
{ command: command, stdout: stdout, stderr: stderr, success: status.success? }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/dotsync/version.rb
CHANGED
data/lib/dotsync.rb
CHANGED
|
@@ -32,6 +32,7 @@ require_relative "dotsync/utils/directory_differ"
|
|
|
32
32
|
require_relative "dotsync/utils/version_checker"
|
|
33
33
|
require_relative "dotsync/utils/config_cache"
|
|
34
34
|
require_relative "dotsync/utils/parallel"
|
|
35
|
+
require_relative "dotsync/utils/hook_runner"
|
|
35
36
|
|
|
36
37
|
# Models
|
|
37
38
|
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.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Sáenz
|
|
@@ -337,6 +337,7 @@ files:
|
|
|
337
337
|
- lib/dotsync/utils/content_diff.rb
|
|
338
338
|
- lib/dotsync/utils/directory_differ.rb
|
|
339
339
|
- lib/dotsync/utils/file_transfer.rb
|
|
340
|
+
- lib/dotsync/utils/hook_runner.rb
|
|
340
341
|
- lib/dotsync/utils/logger.rb
|
|
341
342
|
- lib/dotsync/utils/parallel.rb
|
|
342
343
|
- lib/dotsync/utils/path_utils.rb
|