dotsync 0.1.27 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d2cef06dff635c8202f87424d1532d0db5cfdf7e26e1c0c98daee44acdd12e4
4
- data.tar.gz: 38484cab4e99854a8b54259e222ace225834000c9d9a6df8005dff8b1baa228c
3
+ metadata.gz: 492eda1f730e490322f6ff7fe85fd3fb91a1bd337f191f13d9574d63a5a9a326
4
+ data.tar.gz: f797e4c6a06258d312c3264be88cda022cb02f8053ed31159de54ce585ef9e3a
5
5
  SHA512:
6
- metadata.gz: 22d5fe759b17a723377363c22f05f77cedf57fd6ac41b4c8d379c2f2b2bf4fb42022a0c92707083356a096385fe51b1175c9865fb1f78fde58ba41b94be9fa17
7
- data.tar.gz: 3fb40ad4ac4835cf0c3a45e8bb3d872197dc60bb79d04b8a6ff09d5626d6b92f63865b035d5eb7cc923f12319b4651e08ffe36e808296d002f88712e5bc54730
6
+ metadata.gz: 0d9bd4948c5d7de46e30443ca19ae4fabfc8d0caf3410b2a5e8bb519c8dc27384e032c06db32bd2a9bad041f6642707e8223f4affad58aa540cde4e277663b77
7
+ data.tar.gz: f20438f023966222c67f1a5397dfa8110d0d1cf4985fe67a82f0f8cbe615a9b25bdb88db176db33f1a0bf5109dccb1c5cce4889d74ab259dbd8bdd6e1716c2cf
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## [0.2.0] - 2025-02-06
2
+
3
+ **New Features:**
4
+ - Add `--diff-content` CLI option to display git-like unified diff output for modified files
5
+ - Shows actual content changes without needing external tools like `nvim -d`
6
+ - Color-coded output: blue for additions, red for deletions, cyan for hunk headers
7
+ - Automatically skips binary files
8
+ - Works with both `push` and `pull` commands
9
+
10
+ **Performance Optimizations:**
11
+ - Add parallel processing for mapping operations
12
+ - New `Dotsync::Parallel` utility module with thread-pool based execution
13
+ - Diff computation runs in parallel across multiple mappings
14
+ - File transfers execute concurrently for independent mappings
15
+ - Thread-safe error collection and reporting
16
+ - Add directory pruning optimization
17
+ - New `should_prune_directory?` method for early-exit during traversal
18
+ - Skips entire directory subtrees that are ignored or outside inclusion lists
19
+ - Reduces filesystem operations for large excluded directories
20
+
1
21
  ## [0.1.26] - 2025-01-11
2
22
 
3
23
  **Breaking Changes:**
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dotsync (0.1.27)
4
+ dotsync (0.2.0)
5
5
  fileutils (~> 1.7.3)
6
6
  find (~> 0.2.0)
7
7
  listen (~> 3.9.0)
@@ -87,28 +87,33 @@ module Dotsync
87
87
  end
88
88
 
89
89
  def transfer_mappings
90
- valid_mappings.each do |mapping|
90
+ errors = []
91
+ mutex = Mutex.new
92
+
93
+ Dotsync::Parallel.each(valid_mappings) do |mapping|
91
94
  Dotsync::FileTransfer.new(mapping).transfer
92
95
  rescue Dotsync::PermissionError => e
93
- logger.error("Permission denied: #{e.message}")
94
- logger.info("Try: chmod +w <path> or check file permissions")
96
+ mutex.synchronize { errors << ["Permission denied: #{e.message}", "Try: chmod +w <path> or check file permissions"] }
95
97
  rescue Dotsync::DiskFullError => e
96
- logger.error("Disk full: #{e.message}")
97
- logger.info("Free up disk space and try again")
98
+ mutex.synchronize { errors << ["Disk full: #{e.message}", "Free up disk space and try again"] }
98
99
  rescue Dotsync::SymlinkError => e
99
- logger.error("Symlink error: #{e.message}")
100
- logger.info("Check that symlink target exists and is accessible")
100
+ mutex.synchronize { errors << ["Symlink error: #{e.message}", "Check that symlink target exists and is accessible"] }
101
101
  rescue Dotsync::TypeConflictError => e
102
- logger.error("Type conflict: #{e.message}")
103
- logger.info("Cannot overwrite directory with file or vice versa")
102
+ mutex.synchronize { errors << ["Type conflict: #{e.message}", "Cannot overwrite directory with file or vice versa"] }
104
103
  rescue Dotsync::FileTransferError => e
105
- logger.error("File transfer failed: #{e.message}")
104
+ mutex.synchronize { errors << ["File transfer failed: #{e.message}", nil] }
105
+ end
106
+
107
+ # Report all errors after parallel execution
108
+ errors.each do |error_msg, info_msg|
109
+ logger.error(error_msg)
110
+ logger.info(info_msg) if info_msg
106
111
  end
107
112
  end
108
113
 
109
114
  private
110
115
  def differs
111
- @differs ||= valid_mappings.map do |mapping|
116
+ @differs ||= Dotsync::Parallel.map(valid_mappings) do |mapping|
112
117
  Dotsync::DirectoryDiffer.new(mapping).diff
113
118
  end
114
119
  end
@@ -12,6 +12,7 @@ require_relative "../utils/file_transfer"
12
12
  require_relative "../utils/directory_differ"
13
13
  require_relative "../utils/config_cache"
14
14
  require_relative "../utils/content_diff"
15
+ require_relative "../utils/parallel"
15
16
 
16
17
  # Models
17
18
  require_relative "../models/mapping"
@@ -12,6 +12,7 @@ require_relative "../utils/file_transfer"
12
12
  require_relative "../utils/directory_differ"
13
13
  require_relative "../utils/config_cache"
14
14
  require_relative "../utils/content_diff"
15
+ require_relative "../utils/parallel"
15
16
 
16
17
  # Models
17
18
  require_relative "../models/mapping"
@@ -140,6 +140,16 @@ module Dotsync
140
140
  ignore?(path) || !include?(path)
141
141
  end
142
142
 
143
+ # Returns true if a directory can be entirely skipped during destination walks.
144
+ # A directory should be pruned if:
145
+ # 1. It's ignored, OR
146
+ # 2. It has inclusions AND the path is neither included nor a parent of any inclusion
147
+ def should_prune_directory?(path)
148
+ return true if ignore?(path)
149
+ return false unless has_inclusions?
150
+ !bidirectional_include?(path)
151
+ end
152
+
143
153
  private
144
154
  def has_ignores?
145
155
  @original_ignores.any?
@@ -61,6 +61,12 @@ module Dotsync
61
61
 
62
62
  src_path = File.join(mapping_src, rel_path)
63
63
 
64
+ # Prune entire directory trees that are outside the inclusion list or ignored
65
+ if File.directory?(dest_path) && @mapping.should_prune_directory?(src_path)
66
+ Find.prune
67
+ next
68
+ end
69
+
64
70
  next if @mapping.skip?(src_path)
65
71
 
66
72
  if !File.exist?(src_path)
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dotsync
4
+ # Simple thread-based parallel execution for independent operations.
5
+ # Uses a configurable thread pool to process items concurrently.
6
+ module Parallel
7
+ # Default number of threads (matches typical CPU core count)
8
+ DEFAULT_THREADS = 4
9
+
10
+ # Executes a block for each item in the collection using parallel threads.
11
+ # Returns results in the same order as the input collection.
12
+ #
13
+ # @param items [Array] Collection of items to process
14
+ # @param threads [Integer] Number of parallel threads (default: 4)
15
+ # @yield [item] Block to execute for each item
16
+ # @return [Array] Results in same order as input
17
+ #
18
+ # @example
19
+ # results = Dotsync::Parallel.map(urls, threads: 8) do |url|
20
+ # fetch(url)
21
+ # end
22
+ def self.map(items, threads: DEFAULT_THREADS, &block)
23
+ return [] if items.empty?
24
+ return items.map(&block) if items.size == 1
25
+
26
+ # Limit threads to item count
27
+ thread_count = [threads, items.size].min
28
+
29
+ # Create indexed work items
30
+ work_queue = Queue.new
31
+ items.each_with_index { |item, idx| work_queue << [idx, item] }
32
+
33
+ # Results array (pre-sized for thread safety with index assignment)
34
+ results = Array.new(items.size)
35
+ mutex = Mutex.new
36
+ errors = []
37
+
38
+ # Spawn worker threads
39
+ workers = thread_count.times.map do
40
+ Thread.new do
41
+ loop do
42
+ idx, item = work_queue.pop(true) rescue break
43
+ begin
44
+ results[idx] = yield(item)
45
+ rescue => e
46
+ mutex.synchronize { errors << e }
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ # Wait for completion
53
+ workers.each(&:join)
54
+
55
+ # Re-raise first error if any occurred
56
+ raise errors.first unless errors.empty?
57
+
58
+ results
59
+ end
60
+
61
+ # Executes a block for each item in parallel, ignoring return values.
62
+ # Useful for side-effect operations like file transfers.
63
+ #
64
+ # @param items [Array] Collection of items to process
65
+ # @param threads [Integer] Number of parallel threads (default: 4)
66
+ # @yield [item] Block to execute for each item
67
+ def self.each(items, threads: DEFAULT_THREADS, &block)
68
+ map(items, threads: threads, &block)
69
+ nil
70
+ end
71
+ end
72
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dotsync
4
- VERSION = "0.1.27"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/dotsync.rb CHANGED
@@ -31,6 +31,7 @@ require_relative "dotsync/utils/file_transfer"
31
31
  require_relative "dotsync/utils/directory_differ"
32
32
  require_relative "dotsync/utils/version_checker"
33
33
  require_relative "dotsync/utils/config_cache"
34
+ require_relative "dotsync/utils/parallel"
34
35
 
35
36
  # Models
36
37
  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.1.27
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Sáenz
@@ -338,6 +338,7 @@ files:
338
338
  - lib/dotsync/utils/directory_differ.rb
339
339
  - lib/dotsync/utils/file_transfer.rb
340
340
  - lib/dotsync/utils/logger.rb
341
+ - lib/dotsync/utils/parallel.rb
341
342
  - lib/dotsync/utils/path_utils.rb
342
343
  - lib/dotsync/utils/version_checker.rb
343
344
  - lib/dotsync/version.rb