dotsync 0.1.19 → 0.1.22

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 296888a8438bcbc159e621b7e1423429c6a937e7d67ec8970c4b0779d85b840f
4
- data.tar.gz: 2d6fa4363f2a107cd074002ef8d40bb553cf5aa5bb11f52048ec3eb58cf90a09
3
+ metadata.gz: f1bb84e77b3093d786b7f502618d7b7ab060deb8b2ea8b0df2a7c3362690f017
4
+ data.tar.gz: 21a4b2e7b192d0348c900fd5481939b16cdf7e48ecb27780bbd1b20695133ef5
5
5
  SHA512:
6
- metadata.gz: 389498f088842bebc6a45426d1451b9ac5cd00724526fcf92633d7dbf48ad32adc35986067ade21d086963d557a993b58d5a35c6332b5ef6e81ec36065f81600
7
- data.tar.gz: 740404a8803dcc05f053c0f1b7ba57f1187b7b920a8f34af3217162c554e8d263740bad31921bf8249e9b25820a6905651c200da63e8e072573f031d775516cf
6
+ metadata.gz: 2bb46cecd31107adca67df98262c21a646c28089f3f3ab63be668119cbc4231d8af5341433fca6b0a848235940aa0a4c223e004db0054ae7b7770f1ebaa17c7b
7
+ data.tar.gz: 4e6de7cc7c19cbe5408d689f49e4bd565f4970cbc673c1692313114021046ffaf9fd57efb8fd7446001b9e32a4999eb23a1a6410414fa52a0429b91d43b89b11
data/.editorconfig ADDED
@@ -0,0 +1,42 @@
1
+ # EditorConfig helps maintain consistent coding styles across editors and IDEs
2
+ # https://editorconfig.org
3
+
4
+ root = true
5
+
6
+ # Default settings for all files
7
+ [*]
8
+ charset = utf-8
9
+ end_of_line = lf
10
+ insert_final_newline = true
11
+ trim_trailing_whitespace = true
12
+
13
+ # Ruby files
14
+ [*.{rb,rake}]
15
+ indent_style = space
16
+ indent_size = 2
17
+
18
+ # YAML files
19
+ [*.{yml,yaml}]
20
+ indent_style = space
21
+ indent_size = 2
22
+
23
+ # Markdown files
24
+ [*.md]
25
+ indent_style = space
26
+ indent_size = 2
27
+ trim_trailing_whitespace = false
28
+
29
+ # Configuration files
30
+ [*.{toml,json}]
31
+ indent_style = space
32
+ indent_size = 2
33
+
34
+ # Shell scripts
35
+ [*.sh]
36
+ indent_style = space
37
+ indent_size = 2
38
+
39
+ # Gemfile and other Ruby config files
40
+ [{Gemfile,Rakefile,*.gemspec}]
41
+ indent_style = space
42
+ indent_size = 2
@@ -29,10 +29,32 @@ jobs:
29
29
  run: |
30
30
  bundle exec rake
31
31
 
32
+ - name: Display coverage summary
33
+ if: always()
34
+ run: |
35
+ echo "📊 SimpleCov Coverage Report:"
36
+ if [ -f coverage/.last_run.json ]; then
37
+ ruby -rjson -e "data=JSON.parse(File.read('coverage/.last_run.json')); puts \" Line Coverage: #{data['result']['line']}%\"; puts \" Branch Coverage: #{data['result']['branch']}%\""
38
+ else
39
+ echo " ⚠️ Coverage report not found"
40
+ fi
41
+
42
+ - name: Upload coverage report
43
+ uses: actions/upload-artifact@v4
44
+ if: always()
45
+ with:
46
+ name: coverage-report-ruby-${{ matrix.ruby }}
47
+ path: coverage/
48
+ retention-days: 30
49
+
32
50
  - name: Run RuboCop
33
51
  run: |
34
52
  bundle exec rubocop
35
53
 
54
+ - name: Run security audit
55
+ run: |
56
+ bundle exec bundle-audit check --update
57
+
36
58
  - name: Publish to RubyGems
37
59
  if: matrix.ruby == '3.2' && github.ref_name == 'master'
38
60
  run: |
data/CHANGELOG.md CHANGED
@@ -1,3 +1,87 @@
1
+ # 0.1.22
2
+
3
+ **Testing & Quality:**
4
+ - Increase test coverage from 89.03% to 96.13% line coverage (+7.1%)
5
+ - Increase branch coverage from 75.0% to 81.14% (+6.1%)
6
+ - Add comprehensive tests for Runner, Colors, OutputSections, XDGBaseDirectory
7
+ - Add error handling and confirmation prompt tests for PullAction and PushAction
8
+ - Suppress print output during tests for clean terminal output
9
+ - Update SimpleCov thresholds to 95% line / 80% branch
10
+
11
+ **Bug Fixes:**
12
+ - Fix XDGBaseDirectory path expansion to use ~ instead of $HOME literal
13
+ - Fix Colors accessor methods using wrong hash keys
14
+ - Fix OutputSections to hide differences_legend when only_mappings option is true
15
+ - Add nil config protection in Colors.load_custom_colors
16
+
17
+ **CI/CD:**
18
+ - Add SimpleCov coverage reporting to GitHub Actions workflow
19
+ - Display coverage summary in CI logs (line and branch percentages)
20
+ - Upload coverage HTML reports as artifacts with 30-day retention
21
+
22
+ **New Test Files:**
23
+ - spec/dotsync/runner_spec.rb (24 examples)
24
+ - spec/dotsync/colors_spec.rb (14 examples)
25
+ - spec/dotsync/actions/concerns/output_sections_spec.rb (11 examples)
26
+ - spec/dotsync/config/concerns/xdg_base_directory_spec.rb (8 examples)
27
+
28
+ Total: 323 examples, 0 failures, 2 pending
29
+
30
+ # 0.1.21
31
+
32
+ **New Features & Commands:**
33
+ - Add `status` command to show current configuration and mappings without executing actions
34
+ - Add `diff` command as convenient alias for preview mode (push without --apply)
35
+ - Add `init` command as alias for `setup` command
36
+ - Add `--version` flag to display version number
37
+ - Add `--dry-run` flag as explicit alias for preview mode (industry-standard terminology)
38
+ - Add `-y, --yes` flag to skip confirmation prompts for automation and scripting
39
+ - Add `-c, --config PATH` flag to specify custom config file path (enables multiple config workflows)
40
+
41
+ **Safety & User Experience:**
42
+ - Add confirmation prompt before applying changes showing file count and requiring explicit consent
43
+ - Confirmation can be bypassed with `--yes` flag or skipped in `--quiet` mode
44
+ - Improve error messages with actionable guidance for common issues (permissions, disk space, symlinks, type conflicts)
45
+ - Errors are now handled per-mapping, allowing processing to continue for other mappings
46
+ - Watch command now accepts and respects CLI options (--quiet, --no-legend, --no-mappings)
47
+
48
+ **Documentation:**
49
+ - Complete README overhaul documenting all CLI flags and command aliases
50
+ - Add comprehensive "Safety Features" section covering confirmation prompts, backups, and error handling
51
+ - Add "Command Options" section with all flags organized by category
52
+ - Add "Examples" section with 20+ practical usage patterns
53
+ - Enhance "Pro Tips" with guidance on multiple configs, automation, and command aliases
54
+ - Expand "Troubleshooting" section with solutions for confirmation prompts and config file management
55
+ - Update Table of Contents to include Safety Features section
56
+
57
+ **Developer Experience:**
58
+ - Add practical examples to CLI help text showing common usage patterns
59
+ - Improve help text organization with clearer command descriptions
60
+ - Better error handling for missing config file with actionable suggestions
61
+
62
+ # 0.1.20
63
+
64
+ **Robustness & Error Handling:**
65
+ - Add specific error classes for better error handling (`PermissionError`, `DiskFullError`, `SymlinkError`, `TypeConflictError`)
66
+ - Add symlink support with proper preservation of link targets (regular, broken, and relative symlinks)
67
+ - Add type conflict detection to prevent overwriting directories with files or vice versa
68
+ - Enhance FileTransfer error handling for permission issues and disk space errors
69
+
70
+ **Testing & Quality:**
71
+ - Add 16 new test cases covering edge cases and error scenarios
72
+ - Add comprehensive symlink handling tests (regular, broken, relative)
73
+ - Add path traversal security validation tests
74
+ - Add Unicode filename compatibility tests (Russian, Japanese, Chinese, emoji)
75
+ - Add empty directory transfer tests
76
+ - Add Mapping#apply_to tests for path handling and force flag preservation
77
+ - Improve content comparison tests to verify actual file changes
78
+ - Improve path validation tests with more edge cases
79
+ - Total test count increased from 136 to 152 examples
80
+
81
+ **Developer Experience:**
82
+ - All tests passing (152 examples, 0 failures)
83
+ - RuboCop compliant with no offenses
84
+
1
85
  # 0.1.19
2
86
 
3
87
  **Documentation & Testing:**
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dotsync (0.1.19)
4
+ dotsync (0.1.22)
5
5
  fileutils (~> 1.7.3)
6
6
  find (~> 0.2.0)
7
7
  listen (~> 3.9.0)
@@ -14,12 +14,16 @@ GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
16
  ast (2.4.3)
17
+ bundler-audit (0.9.2)
18
+ bundler (>= 1.2.0, < 3)
19
+ thor (~> 1.0)
17
20
  citrus (3.0.2)
18
21
  date (3.5.0)
19
22
  debug (1.11.0)
20
23
  irb (~> 1.10)
21
24
  reline (>= 0.3.8)
22
25
  diff-lcs (1.6.2)
26
+ docile (1.4.1)
23
27
  erb (5.1.3)
24
28
  ffi (1.17.2)
25
29
  ffi (1.17.2-aarch64-linux-gnu)
@@ -118,9 +122,16 @@ GEM
118
122
  prism (>= 1.2, < 2.0)
119
123
  rbs (>= 3, < 5)
120
124
  ruby-progressbar (1.13.0)
125
+ simplecov (0.22.0)
126
+ docile (~> 1.1)
127
+ simplecov-html (~> 0.11)
128
+ simplecov_json_formatter (~> 0.1)
129
+ simplecov-html (0.13.2)
130
+ simplecov_json_formatter (0.1.4)
121
131
  stringio (3.1.7)
122
132
  terminal-table (4.0.0)
123
133
  unicode-display_width (>= 1.1.1, < 4)
134
+ thor (1.4.0)
124
135
  timecop (0.9.10)
125
136
  toml-rb (4.0.0)
126
137
  citrus (~> 3.0, > 3.0)
@@ -144,6 +155,7 @@ PLATFORMS
144
155
  x86_64-linux-musl
145
156
 
146
157
  DEPENDENCIES
158
+ bundler-audit (~> 0.9.0)
147
159
  debug (~> 1.11)
148
160
  dotsync!
149
161
  rake (~> 13.3.0)
@@ -154,6 +166,7 @@ DEPENDENCIES
154
166
  rubocop-rake (~> 0.7.1)
155
167
  rubocop-rspec (~> 3.7.0)
156
168
  ruby-lsp (~> 0.26.1)
169
+ simplecov (~> 0.22.0)
157
170
  timecop (~> 0.9.10)
158
171
 
159
172
  BUNDLED WITH
data/README.md CHANGED
@@ -29,6 +29,7 @@ Dotsync is a powerful Ruby gem for managing and synchronizing your dotfiles acro
29
29
  - [Usage](#usage)
30
30
  - [Executable Commands](#executable-commands)
31
31
  - [Configuration](#configuration)
32
+ - [Safety Features](#safety-features)
32
33
  - [Customizing Icons](#customizing-icons)
33
34
  - [Automatic Update Checks](#automatic-update-checks)
34
35
  - [Pro Tips](#pro-tips)
@@ -102,39 +103,21 @@ Dotsync provides the following commands to manage your dotfiles:
102
103
  > [!IMPORTANT]
103
104
  > By default, both `push` and `pull` commands run in **preview mode** (dry-run). They will show you what changes would be made without actually modifying any files. To apply changes, you **must** use the `--apply` flag.
104
105
 
106
+ #### Core Commands
107
+
105
108
  - **Push**: Transfer dotfiles from your local machine to the destination repository.
106
109
  ```shell
107
- dotsync push --apply [OPTION]
110
+ dotsync push [OPTIONS]
111
+ dotsync push --apply [OPTIONS] # Apply changes
108
112
  ```
109
- Options:
110
- - `-q, --quiet`: Hide all non-essential output (only errors or final status).
111
- - `--no-legend`: Hide all legends for config, mappings, and differences.
112
- - `--no-config`: Hide the config section in the output.
113
- - `--no-mappings`: Hide the mappings and their legend.
114
- - `--no-diff-legend`: Hide the differences legend only.
115
- - `--no-diff`: Hide the differences section itself.
116
- - `--only-diff`: Show only the differences section.
117
- - `--only-config`: Show only the config section.
118
- - `--only-mappings`: Show only the mappings section.
119
- - `-v, --verbose`: Force showing all available information.
120
113
 
121
114
  ![dotsync push](docs/images/dotsync_push.png)
122
115
 
123
116
  - **Pull**: Synchronize dotfiles from the repository to your local machine.
124
117
  ```shell
125
- dotsync pull --apply [OPTION]
118
+ dotsync pull [OPTIONS]
119
+ dotsync pull --apply [OPTIONS] # Apply changes
126
120
  ```
127
- Options:
128
- - `-q, --quiet`: Hide all non-essential output (only errors or final status).
129
- - `--no-legend`: Hide all legends for config, mappings, and differences.
130
- - `--no-config`: Hide the config section in the output.
131
- - `--no-mappings`: Hide the mappings and their legend.
132
- - `--no-diff-legend`: Hide the differences legend only.
133
- - `--no-diff`: Hide the differences section itself.
134
- - `--only-diff`: Show only the differences section.
135
- - `--only-config`: Show only the config section.
136
- - `--only-mappings`: Show only the mappings section.
137
- - `-v, --verbose`: Force showing all available information.
138
121
 
139
122
  During the `pull` operation, `Dotsync::PullAction` creates a backup of the existing files on the destination. These backups are stored in a directory under the XDG path, with each backup organized by a timestamp. To prevent excessive storage usage, only the 10 most recent backups are retained. Older backups are automatically purged, ensuring efficient storage management.
140
123
 
@@ -142,13 +125,90 @@ Dotsync provides the following commands to manage your dotfiles:
142
125
 
143
126
  - **Watch**: Continuously monitor and sync changes between your local machine and the repository.
144
127
  ```shell
145
- dotsync watch
128
+ dotsync watch [OPTIONS]
146
129
  ```
130
+
131
+ The watch command supports the same output control options as push and pull (e.g., `--quiet`, `--no-legend`, `--no-mappings`).
147
132
 
148
- - **Setup**: Generate a default configuration file at `~/.config/dotsync.toml` with example mappings for `pull`, `push`, and `watch`.
133
+ - **Setup** (alias: **init**): Generate a default configuration file at `~/.config/dotsync.toml` with example mappings for `pull`, `push`, and `watch`.
149
134
  ```shell
150
135
  dotsync setup
136
+ dotsync init # Alias for setup
137
+ ```
138
+
139
+ #### Utility Commands
140
+
141
+ - **Status**: Display current configuration and mappings without executing any actions.
142
+ ```shell
143
+ dotsync status
144
+ ```
145
+ This is useful for inspecting your configuration and verifying mappings are correct.
146
+
147
+ - **Diff**: Show differences that would be made (alias for `push` in preview mode).
148
+ ```shell
149
+ dotsync diff
151
150
  ```
151
+ Convenient shorthand for previewing changes without typing `--dry-run`.
152
+
153
+ #### Command Options
154
+
155
+ All push and pull commands support the following options:
156
+
157
+ **Action Control:**
158
+ - `-a, --apply`: Apply changes (without this, commands run in preview mode)
159
+ - `--dry-run`: Explicitly run in preview mode without applying changes (default behavior)
160
+ - `-y, --yes`: Skip confirmation prompt and auto-confirm changes
161
+ - `-c, --config PATH`: Specify a custom config file path (enables multiple config workflows)
162
+
163
+ **Output Control:**
164
+ - `-q, --quiet`: Hide all non-essential output (only errors or final status)
165
+ - `--no-legend`: Hide all legends for config, mappings, and differences
166
+ - `--no-config`: Hide the config section in the output
167
+ - `--no-mappings`: Hide the mappings and their legend
168
+ - `--no-diff-legend`: Hide the differences legend only
169
+ - `--no-diff`: Hide the differences section itself
170
+ - `--only-diff`: Show only the differences section
171
+ - `--only-config`: Show only the config section
172
+ - `--only-mappings`: Show only the mappings section
173
+ - `-v, --verbose`: Force showing all available information
174
+
175
+ **General:**
176
+ - `--version`: Display version number
177
+ - `-h, --help`: Show help message
178
+
179
+ #### Examples
180
+
181
+ ```shell
182
+ # Setup and configuration
183
+ dotsync setup # Create initial config file
184
+ dotsync init # Same as setup (alias)
185
+ dotsync status # View current configuration
186
+
187
+ # Preview changes (dry-run mode)
188
+ dotsync push # Preview push changes
189
+ dotsync pull # Preview pull changes
190
+ dotsync diff # Quick preview (alias for push)
191
+ dotsync push --dry-run # Explicit dry-run flag
192
+
193
+ # Apply changes
194
+ dotsync push --apply # Apply changes with confirmation
195
+ dotsync pull --apply # Apply changes with confirmation
196
+ dotsync push -ay # Apply without confirmation (--apply + --yes)
197
+ dotsync pull --apply --yes # Apply without confirmation
198
+
199
+ # Custom configuration files
200
+ dotsync -c ~/work-dotfiles.toml push # Use work config
201
+ dotsync --config ~/.config/personal.toml pull # Use personal config
202
+
203
+ # Output control
204
+ dotsync pull --quiet # Minimal output
205
+ dotsync push --only-diff # Show only differences
206
+ dotsync pull --apply --yes -q # Silent apply for scripts
207
+
208
+ # Monitoring
209
+ dotsync watch # Watch with default output
210
+ dotsync watch --quiet # Watch with minimal output
211
+ ```
152
212
 
153
213
  ### Configuration
154
214
 
@@ -217,6 +277,98 @@ Each mapping entry supports the following options:
217
277
 
218
278
  These options apply when the source is a directory and are relevant for both `push` and `pull` operations.
219
279
 
280
+ ### Safety Features
281
+
282
+ Dotsync includes several safety mechanisms to prevent accidental data loss:
283
+
284
+ #### Confirmation Prompts
285
+
286
+ Before applying any changes with the `--apply` flag, Dotsync will:
287
+ 1. Show you all differences that will be applied
288
+ 2. Display the total count of files to be modified
289
+ 3. Ask for explicit confirmation: `About to modify X file(s). Continue? [y/N]`
290
+ 4. Only proceed if you type `y` and press Enter
291
+
292
+ **Example:**
293
+ ```shell
294
+ $ dotsync push --apply
295
+
296
+ # ... shows differences ...
297
+
298
+ About to modify 15 file(s).
299
+ Continue? [y/N] y
300
+ ```
301
+
302
+ **Bypassing Confirmation:**
303
+ - Use the `--yes` or `-y` flag to skip confirmation (useful for automation):
304
+ ```shell
305
+ dotsync push --apply --yes
306
+ dotsync pull -ay # Short form
307
+ ```
308
+ - Use the `--quiet` flag (automatically skips prompt and suppresses output)
309
+
310
+ > [!NOTE]
311
+ > No confirmation is shown if there are no differences to apply.
312
+
313
+ #### Automatic Backups
314
+
315
+ When using `pull --apply`, Dotsync automatically:
316
+ - Creates timestamped backups of existing files before overwriting them
317
+ - Stores backups in `~/.cache/dotsync/backups/YYYYMMDDHHMMSS/`
318
+ - Retains only the 10 most recent backups (older ones are purged)
319
+ - Creates backups only when there are actual differences
320
+
321
+ To restore from a backup:
322
+ ```shell
323
+ ls -la ~/.cache/dotsync/backups/
324
+ cp -r ~/.cache/dotsync/backups/20250110143022/* ~/.config/
325
+ ```
326
+
327
+ #### Preview Mode (Dry-Run)
328
+
329
+ By default, all `push` and `pull` commands run in preview mode:
330
+ - Shows exactly what would change without modifying files
331
+ - Must explicitly use `--apply` flag to make changes
332
+ - Use `--dry-run` flag for explicit clarity in scripts
333
+
334
+ #### Enhanced Error Handling
335
+
336
+ Dotsync provides clear, actionable error messages for common issues:
337
+
338
+ - **Permission Errors**:
339
+ ```
340
+ Permission denied: /path/to/file
341
+ Try: chmod +w <path> or check file permissions
342
+ ```
343
+
344
+ - **Disk Full Errors**:
345
+ ```
346
+ Disk full: No space left on device
347
+ Free up disk space and try again
348
+ ```
349
+
350
+ - **Symlink Errors**:
351
+ ```
352
+ Symlink error: Target does not exist
353
+ Check that symlink target exists and is accessible
354
+ ```
355
+
356
+ - **Type Conflicts**:
357
+ ```
358
+ Type conflict: Cannot overwrite directory with file
359
+ Cannot overwrite directory with file or vice versa
360
+ ```
361
+
362
+ Errors are reported per-mapping, allowing Dotsync to continue processing other mappings even if one fails.
363
+
364
+ #### Symlink Support
365
+
366
+ Dotsync properly handles symbolic links:
367
+ - Preserves symlink targets (absolute and relative paths)
368
+ - Handles broken symlinks gracefully
369
+ - Detects type conflicts (e.g., file vs. directory vs. symlink)
370
+ - Provides clear error messages for symlink-related issues
371
+
220
372
  ### Customizing Icons
221
373
 
222
374
  Dotsync allows you to customize the icons displayed in the console output by adding an `[icons]` section to your configuration file (`~/.config/dotsync.toml`). This is useful if you prefer different icons or need compatibility with terminals that don't support Nerd Fonts.
@@ -294,9 +446,36 @@ The check runs after your command completes and uses a cached timestamp to avoid
294
446
  - **Preview Before Applying**: Always run commands without `--apply` first to preview changes:
295
447
  ```shell
296
448
  dotsync pull # Preview changes
449
+ dotsync diff # Quick preview (alias)
297
450
  dotsync pull --apply # Apply after reviewing
298
451
  ```
299
452
 
453
+ - **Check Configuration**: Use the `status` command to inspect your configuration without executing any actions:
454
+ ```shell
455
+ dotsync status # View config and mappings
456
+ ```
457
+
458
+ - **Multiple Config Files**: Use the `-c` flag to maintain separate configurations for different workflows:
459
+ ```bash
460
+ # Work dotfiles
461
+ dotsync -c ~/work-dotfiles.toml push --apply
462
+
463
+ # Personal dotfiles
464
+ dotsync -c ~/.config/personal.toml pull --apply
465
+
466
+ # Server configs
467
+ dotsync --config ~/server.toml push --apply
468
+ ```
469
+
470
+ - **Automation and Scripting**: Use `--yes` flag to skip confirmation prompts:
471
+ ```shell
472
+ # In a script or CI/CD pipeline
473
+ dotsync pull --apply --yes --quiet
474
+
475
+ # Shorthand
476
+ dotsync push -ayq
477
+ ```
478
+
300
479
  - **Using Environment Variables**: Simplify your configuration with mirror environment variables:
301
480
  ```bash
302
481
  # Add to your ~/.zshrc or ~/.bashrc
@@ -317,6 +496,11 @@ The check runs after your command completes and uses a cached timestamp to avoid
317
496
  gem install dotsync
318
497
  ```
319
498
 
499
+ - **Check Version**: Quickly check which version you're running:
500
+ ```shell
501
+ dotsync --version
502
+ ```
503
+
320
504
  - **Disable Update Checks**: If you prefer not to see update notifications:
321
505
  ```shell
322
506
  export DOTSYNC_NO_UPDATE_CHECK=1
@@ -398,8 +582,11 @@ ignore = ["nvim", "cache", "*.log"]
398
582
  **Solution**: Remember to use the `--apply` flag to apply changes. Without it, commands run in preview mode:
399
583
  ```shell
400
584
  dotsync pull --apply
585
+ dotsync push --apply
401
586
  ```
402
587
 
588
+ You can also use the explicit `--dry-run` flag to make preview mode clear in scripts.
589
+
403
590
  ### Permission Denied Errors
404
591
 
405
592
  **Problem**: Getting permission errors when syncing files.
@@ -438,6 +625,48 @@ cp -r ~/.cache/dotsync/backups/YYYYMMDD_HHMMSS/* ~/.config/
438
625
  - Ensure the source directories exist and are accessible
439
626
  - Try stopping and restarting the watch command
440
627
 
628
+ ### Confirmation Prompt Appearing
629
+
630
+ **Problem**: Being prompted to confirm changes when running in scripts or automation.
631
+
632
+ **Solution**: Use the `--yes` or `-y` flag to skip confirmation prompts:
633
+ ```shell
634
+ dotsync push --apply --yes
635
+ dotsync pull -ay # Shorthand
636
+ ```
637
+
638
+ For completely silent operation in scripts:
639
+ ```shell
640
+ dotsync push --apply --yes --quiet
641
+ ```
642
+
643
+ ### Using Multiple Config Files
644
+
645
+ **Problem**: Need different dotfile configurations for work, personal, or different machines.
646
+
647
+ **Solution**: Use the `--config` or `-c` flag to specify custom config files:
648
+ ```shell
649
+ dotsync -c ~/work-dotfiles.toml push
650
+ dotsync --config ~/.config/personal.toml pull
651
+ ```
652
+
653
+ You can maintain separate config files for different environments and switch between them easily.
654
+
655
+ ### Config File Not Found
656
+
657
+ **Problem**: Error message about missing config file.
658
+
659
+ **Solution**: Create a config file using the setup command:
660
+ ```shell
661
+ dotsync setup # Creates ~/.config/dotsync.toml
662
+ dotsync init # Alias for setup
663
+ ```
664
+
665
+ Or specify a custom config file path:
666
+ ```shell
667
+ dotsync -c ~/my-config.toml setup
668
+ ```
669
+
441
670
  ## Development
442
671
 
443
672
  - After checking out the repo, run `bin/setup` to install dependencies.
data/dotsync.gemspec CHANGED
@@ -47,4 +47,6 @@ Gem::Specification.new do |spec|
47
47
  spec.add_development_dependency "rubocop-md", "~> 2.0.3"
48
48
  spec.add_development_dependency "timecop", "~> 0.9.10"
49
49
  spec.add_development_dependency "ruby-lsp", "~> 0.26.1"
50
+ spec.add_development_dependency "simplecov", "~> 0.22.0"
51
+ spec.add_development_dependency "bundler-audit", "~> 0.9.0"
50
52
  end
data/exe/dotsync CHANGED
@@ -4,7 +4,7 @@
4
4
  require_relative "../lib/dotsync"
5
5
  require "optparse"
6
6
 
7
- options = { apply: false }
7
+ options = { apply: false, config_path: nil }
8
8
 
9
9
  opt_parser = OptionParser.new do |opts|
10
10
  opts.banner = <<~BANNER
@@ -12,13 +12,30 @@ opt_parser = OptionParser.new do |opts|
12
12
  Usage: dotsync [command] [options]
13
13
 
14
14
  Commands:
15
- push Upload local changes to the remote
16
- pull Download remote changes to the local
17
- watch Continuously monitor and sync changes
18
- setup Initialize a default configuration file
15
+ push Upload local changes to the remote
16
+ pull Download remote changes to the local
17
+ watch Continuously monitor and sync changes
18
+ setup Initialize a default configuration file
19
+ init Alias for 'setup'
20
+ status Show current configuration and mappings
21
+ diff Show differences (alias for 'push' without --apply)
22
+
23
+ Examples:
24
+ dotsync setup # Create initial config file
25
+ dotsync push # Preview changes (dry-run)
26
+ dotsync push --apply # Apply changes to remote
27
+ dotsync push -ay # Apply without confirmation
28
+ dotsync pull --quiet # Pull with minimal output
29
+ dotsync status # Show current setup
30
+ dotsync diff # Show what would change
31
+ dotsync watch # Monitor and sync continuously
32
+ dotsync -c ~/custom.toml push # Use custom config file
19
33
 
20
34
  Options:
21
35
  -a, --apply Apply changes (push or pull)
36
+ --dry-run Preview changes without applying (default behavior)
37
+ -y, --yes Skip confirmation prompt (auto-confirm)
38
+ -c, --config PATH Specify custom config file path
22
39
  -q, --quiet Hide all non-essential output (only errors or final status)
23
40
  --no-legend Hide all legends for config, mappings, and differences
24
41
  --no-config Hide the config section in the output
@@ -29,6 +46,7 @@ opt_parser = OptionParser.new do |opts|
29
46
  --only-config Show only the config section
30
47
  --only-mappings Show only the mappings section
31
48
  -v, --verbose Force showing all available information
49
+ --version Show version number
32
50
  -h, --help Show this help message
33
51
  BANNER
34
52
 
@@ -36,6 +54,18 @@ opt_parser = OptionParser.new do |opts|
36
54
  options[:apply] = true
37
55
  end
38
56
 
57
+ opts.on("--dry-run", "Preview changes without applying (default behavior)") do
58
+ options[:apply] = false
59
+ end
60
+
61
+ opts.on("-y", "--yes", "Skip confirmation prompt (auto-confirm)") do
62
+ options[:yes] = true
63
+ end
64
+
65
+ opts.on("-c", "--config PATH", "Specify custom config file path") do |path|
66
+ options[:config_path] = path
67
+ end
68
+
39
69
  opts.on("-q", "--quiet", "Hide all non-essential output (only errors or final status)") do
40
70
  options[:quiet] = true
41
71
  end
@@ -76,6 +106,11 @@ opt_parser = OptionParser.new do |opts|
76
106
  options[:verbose] = true
77
107
  end
78
108
 
109
+ opts.on("--version", "Show version number") do
110
+ puts "dotsync #{Dotsync::VERSION}"
111
+ exit
112
+ end
113
+
79
114
  opts.on("-h", "--help", "Show this help message") do
80
115
  puts opt_parser.banner
81
116
  exit
@@ -88,13 +123,27 @@ command = ARGV.shift
88
123
 
89
124
  case command
90
125
  when "push"
91
- Dotsync::Runner.new.run(:push, options)
126
+ Dotsync::Runner.new(config_path: options[:config_path]).run(:push, options)
92
127
  when "pull"
93
- Dotsync::Runner.new.run(:pull, options)
128
+ Dotsync::Runner.new(config_path: options[:config_path]).run(:pull, options)
94
129
  when "watch"
95
- Dotsync::Runner.new.run(:watch)
96
- when "setup"
97
- Dotsync::Runner.new.run(:setup)
130
+ Dotsync::Runner.new(config_path: options[:config_path]).run(:watch, options)
131
+ when "setup", "init"
132
+ Dotsync::Runner.new(config_path: options[:config_path]).run(:setup)
133
+ when "status"
134
+ # Show config and mappings without executing any action
135
+ # Use push action with only-config and only-mappings to display info
136
+ status_options = options.merge(
137
+ only_config: true,
138
+ only_mappings: true,
139
+ no_diff: true,
140
+ apply: false
141
+ )
142
+ Dotsync::Runner.new(config_path: options[:config_path]).run(:push, status_options)
143
+ when "diff"
144
+ # Alias for push in preview mode (default behavior)
145
+ diff_options = options.merge(apply: false)
146
+ Dotsync::Runner.new(config_path: options[:config_path]).run(:push, diff_options)
98
147
  else
99
148
  if command
100
149
  puts "dotsync: no such command '#{command}'"
@@ -80,6 +80,20 @@ module Dotsync
80
80
  def transfer_mappings
81
81
  valid_mappings.each do |mapping|
82
82
  Dotsync::FileTransfer.new(mapping).transfer
83
+ rescue Dotsync::PermissionError => e
84
+ logger.error("Permission denied: #{e.message}")
85
+ logger.info("Try: chmod +w <path> or check file permissions")
86
+ rescue Dotsync::DiskFullError => e
87
+ logger.error("Disk full: #{e.message}")
88
+ logger.info("Free up disk space and try again")
89
+ rescue Dotsync::SymlinkError => e
90
+ logger.error("Symlink error: #{e.message}")
91
+ logger.info("Check that symlink target exists and is accessible")
92
+ rescue Dotsync::TypeConflictError => e
93
+ logger.error("Type conflict: #{e.message}")
94
+ logger.info("Cannot overwrite directory with file or vice versa")
95
+ rescue Dotsync::FileTransferError => e
96
+ logger.error("File transfer failed: #{e.message}")
83
97
  end
84
98
  end
85
99
 
@@ -94,6 +108,15 @@ module Dotsync
94
108
  differs.any? { |differ| differ.any? }
95
109
  end
96
110
 
111
+ def confirm_action
112
+ total_changes = differs.sum { |diff| diff.additions.size + diff.modifications.size + diff.removals.size }
113
+ logger.log("")
114
+ logger.info("About to modify #{total_changes} file(s).", icon: :warning)
115
+ print "Continue? [y/N] "
116
+ response = $stdin.gets
117
+ response && response.strip.downcase == "y"
118
+ end
119
+
97
120
  def mappings_env_vars
98
121
  paths = mappings.flat_map do |mapping|
99
122
  [mapping.original_src, mapping.original_dest]
@@ -11,7 +11,7 @@ module Dotsync
11
11
  env_vars: !(quiet || options[:only_diff] || options[:only_mappings]),
12
12
  mappings_legend: !(quiet || options[:no_legend] || options[:no_mappings] || options[:only_diff]),
13
13
  mappings: !(quiet || options[:no_mappings] || options[:only_diff]),
14
- differences_legend: !(quiet || options[:no_legend] || options[:no_diff_legend] || options[:no_diff] || options[:only_config]),
14
+ differences_legend: !(quiet || options[:no_legend] || options[:no_diff_legend] || options[:no_diff] || options[:only_config] || options[:only_mappings]),
15
15
  differences: !(quiet || options[:no_diff] || options[:only_mappings] || options[:only_config])
16
16
  }
17
17
 
@@ -19,6 +19,11 @@ module Dotsync
19
19
 
20
20
  return unless options[:apply]
21
21
 
22
+ # Confirmation prompt unless --yes flag is provided or no differences
23
+ if has_differences? && !options[:yes] && !options[:quiet]
24
+ return unless confirm_action
25
+ end
26
+
22
27
  if has_differences?
23
28
  if create_backup
24
29
  show_backup
@@ -17,6 +17,11 @@ module Dotsync
17
17
 
18
18
  return unless options[:apply]
19
19
 
20
+ # Confirmation prompt unless --yes flag is provided or no differences
21
+ if has_differences? && !options[:yes] && !options[:quiet]
22
+ return unless confirm_action
23
+ end
24
+
20
25
  transfer_mappings
21
26
  action("Mappings pushed", icon: :done)
22
27
  end
@@ -12,14 +12,14 @@ module Dotsync
12
12
  setup_signal_trap
13
13
  end
14
14
 
15
- def execute
16
- show_mappings_legend
17
- show_mappings
15
+ def execute(options = {})
16
+ show_mappings_legend unless options[:quiet] || options[:no_legend] || options[:no_mappings]
17
+ show_mappings unless options[:quiet] || options[:no_mappings]
18
18
 
19
19
  @listeners.each(&:start)
20
20
 
21
- logger.action("Listening for changes...")
22
- logger.action("Press Ctrl+C to exit.")
21
+ logger.action("Listening for changes...") unless options[:quiet]
22
+ logger.action("Press Ctrl+C to exit.") unless options[:quiet]
23
23
  sleep
24
24
  end
25
25
 
@@ -9,6 +9,7 @@ module Dotsync
9
9
  @custom_colors = {}
10
10
 
11
11
  def self.load_custom_colors(config)
12
+ config ||= {}
12
13
  @custom_colors = {
13
14
  diff_additions: config.dig("colors", "diff_additions") || DEFAULT_DIFF_ADDITIONS,
14
15
  diff_modifications: config.dig("colors", "diff_modifications") || DEFAULT_DIFF_MODIFICATIONS,
@@ -17,15 +18,15 @@ module Dotsync
17
18
  end
18
19
 
19
20
  def self.diff_additions
20
- @custom_colors[:additions] || DEFAULT_DIFF_ADDITIONS
21
+ @custom_colors[:diff_additions] || DEFAULT_DIFF_ADDITIONS
21
22
  end
22
23
 
23
24
  def self.diff_modifications
24
- @custom_colors[:modifications] || DEFAULT_DIFF_MODIFICATIONS
25
+ @custom_colors[:diff_modifications] || DEFAULT_DIFF_MODIFICATIONS
25
26
  end
26
27
 
27
28
  def self.diff_removals
28
- @custom_colors[:removals] || DEFAULT_DIFF_REMOVALS
29
+ @custom_colors[:diff_removals] || DEFAULT_DIFF_REMOVALS
29
30
  end
30
31
 
31
32
  MAPPINGS = {
@@ -12,6 +12,14 @@ module Dotsync
12
12
  # @param [String] path The file path to the configuration file.
13
13
  def initialize(path = Dotsync.config_path)
14
14
  absolute_path = File.expand_path(path)
15
+
16
+ unless File.exist?(absolute_path)
17
+ raise Dotsync::ConfigError,
18
+ "Config file not found: #{absolute_path}\n\n" \
19
+ "To create a default configuration file, run:\n" \
20
+ " dotsync setup"
21
+ end
22
+
15
23
  @config = TomlRB.load_file(absolute_path)
16
24
  validate!
17
25
  end
@@ -4,19 +4,19 @@ module Dotsync
4
4
  # https://specifications.freedesktop.org/basedir-spec/latest/
5
5
  module XDGBaseDirectory
6
6
  def xdg_data_home
7
- File.expand_path(ENV["XDG_DATA_HOME"] || "$HOME/.local/share")
7
+ File.expand_path(ENV["XDG_DATA_HOME"] || "~/.local/share")
8
8
  end
9
9
 
10
10
  def xdg_config_home
11
- File.expand_path(ENV["XDG_CONFIG_HOME"] || "$HOME/.config")
11
+ File.expand_path(ENV["XDG_CONFIG_HOME"] || "~/.config")
12
12
  end
13
13
 
14
14
  def xdg_cache_home
15
- File.expand_path(ENV["XDG_CACHE_HOME"] || "$HOME/.cache")
15
+ File.expand_path(ENV["XDG_CACHE_HOME"] || "~/.cache")
16
16
  end
17
17
 
18
18
  def xdg_bin_home
19
- File.expand_path(ENV["XDG_BIN_HOME"] || "$HOME/.local/bin")
19
+ File.expand_path(ENV["XDG_BIN_HOME"] || "~/.local/bin")
20
20
  end
21
21
  end
22
22
  end
@@ -3,4 +3,9 @@
3
3
  module Dotsync
4
4
  class Error < StandardError; end
5
5
  class ConfigError < StandardError; end
6
+ class FileTransferError < Error; end
7
+ class PermissionError < FileTransferError; end
8
+ class DiskFullError < FileTransferError; end
9
+ class SymlinkError < FileTransferError; end
10
+ class TypeConflictError < FileTransferError; end
6
11
  end
@@ -58,11 +58,17 @@ module Dotsync
58
58
  end
59
59
 
60
60
  def valid?
61
+ return false unless paths_are_distinct?
62
+ return false unless paths_not_nested?
61
63
  directories? || files? || file_present_in_src_only?
62
64
  end
63
65
 
64
66
  def file_changed?
65
- files_present? && (File.size(src) != File.size(dest))
67
+ return false unless files_present?
68
+ # Check size first for quick comparison
69
+ return true if File.size(src) != File.size(dest)
70
+ # If sizes match, compare content
71
+ FileUtils.compare_file(src, dest) == false
66
72
  end
67
73
 
68
74
  def backup_possible?
@@ -154,5 +160,16 @@ module Dotsync
154
160
  end
155
161
  [sanitized_src, sanitized_dest, sanitized_ignores, sanitized_only]
156
162
  end
163
+
164
+ def paths_are_distinct?
165
+ src != dest
166
+ end
167
+
168
+ def paths_not_nested?
169
+ # Check if dest is inside src or vice versa
170
+ return false if dest.start_with?("#{src}/")
171
+ return false if src.start_with?("#{dest}/")
172
+ true
173
+ end
157
174
  end
158
175
  end
@@ -2,8 +2,9 @@
2
2
 
3
3
  module Dotsync
4
4
  class Runner
5
- def initialize(logger: nil)
5
+ def initialize(logger: nil, config_path: nil)
6
6
  @logger = logger || Dotsync::Logger.new
7
+ @config_path = config_path
7
8
  end
8
9
 
9
10
  # action_name should be a symbol, e.g., :pull, :watch, :sync
@@ -44,7 +45,7 @@ module Dotsync
44
45
  require "toml-rb"
45
46
  require "fileutils"
46
47
 
47
- config_path = File.expand_path(Dotsync.config_path)
48
+ config_path = File.expand_path(@config_path || Dotsync.config_path)
48
49
  FileUtils.mkdir_p(File.dirname(config_path))
49
50
 
50
51
  example_mappings = {
@@ -46,7 +46,7 @@ module Dotsync
46
46
  if !File.exist?(dest_path)
47
47
  additions << rel_path
48
48
  elsif File.file?(src_path) && File.file?(dest_path)
49
- if File.size(src_path) != File.size(dest_path)
49
+ if files_differ?(src_path, dest_path)
50
50
  modifications << rel_path
51
51
  end
52
52
  end
@@ -92,5 +92,13 @@ module Dotsync
92
92
  end
93
93
  end
94
94
  end
95
+
96
+ def files_differ?(src_path, dest_path)
97
+ # First check size for quick comparison
98
+ return true if File.size(src_path) != File.size(dest_path)
99
+
100
+ # If sizes match, compare content
101
+ FileUtils.compare_file(src_path, dest_path) == false
102
+ end
95
103
  end
96
104
  end
@@ -20,7 +20,28 @@ module Dotsync
20
20
 
21
21
  def transfer
22
22
  if File.file?(@src)
23
- transfer_file(@src, @dest)
23
+ # Check if we're trying to overwrite a directory with a file
24
+ if File.exist?(@dest) && File.directory?(@dest) && !File.symlink?(@dest)
25
+ # If @dest is a directory and NOT just a parent directory for the file,
26
+ # this is a conflict. The check is: if @dest path exactly matches where
27
+ # we want the file to be (not a parent dir), then it's a conflict.
28
+ # We determine this by checking if File.basename(@src) already appears
29
+ # to be accounted for in @dest path.
30
+ dest_basename = File.basename(@dest)
31
+ src_basename = File.basename(@src)
32
+
33
+ if dest_basename == src_basename
34
+ raise Dotsync::TypeConflictError, "Cannot overwrite directory '#{@dest}' with file '#{@src}'"
35
+ end
36
+ end
37
+
38
+ # If dest is a directory, compute the target file path
39
+ target_dest = if File.directory?(@dest)
40
+ File.join(@dest, File.basename(@src))
41
+ else
42
+ @dest
43
+ end
44
+ transfer_file(@src, target_dest)
24
45
  else
25
46
  cleanup_folder(@dest) if @force
26
47
  transfer_folder(@src, @dest)
@@ -31,8 +52,29 @@ module Dotsync
31
52
  attr_reader :mapping, :ignores
32
53
 
33
54
  def transfer_file(file_src, file_dest)
55
+ # Check for type conflicts before transfer
56
+ if File.exist?(file_dest) && File.directory?(file_dest)
57
+ raise Dotsync::TypeConflictError, "Cannot overwrite directory '#{file_dest}' with file '#{file_src}'"
58
+ end
59
+
34
60
  FileUtils.mkdir_p(File.dirname(file_dest))
35
- FileUtils.cp(file_src, file_dest)
61
+
62
+ # Use atomic write: copy to temp file, then rename
63
+ # This prevents corruption if copy is interrupted
64
+ temp_file = "#{file_dest}.tmp.#{Process.pid}"
65
+ begin
66
+ FileUtils.cp(file_src, temp_file)
67
+ FileUtils.mv(temp_file, file_dest, force: true)
68
+ rescue Errno::EACCES, Errno::EPERM => e
69
+ FileUtils.rm_f(temp_file) if File.exist?(temp_file)
70
+ raise Dotsync::PermissionError, "Permission denied: #{e.message}"
71
+ rescue Errno::ENOSPC => e
72
+ FileUtils.rm_f(temp_file) if File.exist?(temp_file)
73
+ raise Dotsync::DiskFullError, "Disk full: #{e.message}"
74
+ rescue StandardError => e
75
+ FileUtils.rm_f(temp_file) if File.exist?(temp_file)
76
+ raise Dotsync::FileTransferError, "Transfer failed: #{e.message}"
77
+ end
36
78
  end
37
79
 
38
80
  def transfer_folder(folder_src, folder_dest)
@@ -53,14 +95,42 @@ module Dotsync
53
95
  next if mapping.ignore?(full_path)
54
96
 
55
97
  target = File.join(folder_dest, File.basename(path))
56
- if File.file?(full_path)
57
- FileUtils.cp(full_path, target)
58
- else
98
+ if File.symlink?(full_path)
99
+ transfer_symlink(full_path, target)
100
+ elsif File.file?(full_path)
101
+ transfer_file(full_path, target)
102
+ elsif File.directory?(full_path)
59
103
  transfer_folder(full_path, target)
60
104
  end
61
105
  end
62
106
  end
63
107
 
108
+ def transfer_symlink(symlink_src, symlink_dest)
109
+ # Check if we're trying to overwrite a regular file or directory with a symlink
110
+ if File.exist?(symlink_dest) && !File.symlink?(symlink_dest)
111
+ if File.directory?(symlink_dest)
112
+ raise Dotsync::TypeConflictError, "Cannot overwrite directory '#{symlink_dest}' with symlink '#{symlink_src}'"
113
+ end
114
+ end
115
+
116
+ FileUtils.mkdir_p(File.dirname(symlink_dest))
117
+
118
+ # Get the target the symlink points to
119
+ link_target = File.readlink(symlink_src)
120
+
121
+ begin
122
+ # Remove existing symlink if present
123
+ FileUtils.rm(symlink_dest) if File.exist?(symlink_dest) || File.symlink?(symlink_dest)
124
+
125
+ # Create the new symlink
126
+ File.symlink(link_target, symlink_dest)
127
+ rescue Errno::EACCES, Errno::EPERM => e
128
+ raise Dotsync::PermissionError, "Permission denied creating symlink: #{e.message}"
129
+ rescue StandardError => e
130
+ raise Dotsync::SymlinkError, "Failed to create symlink: #{e.message}"
131
+ end
132
+ end
133
+
64
134
  def cleanup_folder(target_dir)
65
135
  target_dir = File.expand_path(target_dir)
66
136
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dotsync
4
- VERSION = "0.1.19"
4
+ VERSION = "0.1.22"
5
5
  end
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.1.19
4
+ version: 0.1.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Sáenz
@@ -248,6 +248,34 @@ dependencies:
248
248
  - - "~>"
249
249
  - !ruby/object:Gem::Version
250
250
  version: 0.26.1
251
+ - !ruby/object:Gem::Dependency
252
+ name: simplecov
253
+ requirement: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - "~>"
256
+ - !ruby/object:Gem::Version
257
+ version: 0.22.0
258
+ type: :development
259
+ prerelease: false
260
+ version_requirements: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - "~>"
263
+ - !ruby/object:Gem::Version
264
+ version: 0.22.0
265
+ - !ruby/object:Gem::Dependency
266
+ name: bundler-audit
267
+ requirement: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - "~>"
270
+ - !ruby/object:Gem::Version
271
+ version: 0.9.0
272
+ type: :development
273
+ prerelease: false
274
+ version_requirements: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - "~>"
277
+ - !ruby/object:Gem::Version
278
+ version: 0.9.0
251
279
  description: Keep in sync your dotfiles across machines with a single TOML file
252
280
  email:
253
281
  - david.saenz.tagarro@gmail.com
@@ -256,6 +284,7 @@ executables:
256
284
  extensions: []
257
285
  extra_rdoc_files: []
258
286
  files:
287
+ - ".editorconfig"
259
288
  - ".github/workflows/gem-push.yml"
260
289
  - ".github/workflows/gem-push.yml.bak"
261
290
  - ".gitignore"