dotsync 0.4.5 → 0.4.6

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: 98beade2b2482ee27ecf6843e8f4f214fd81a0070c06d661213bce4c80e2d57a
4
- data.tar.gz: b512b8ad21fe3966b40d0f09fbffe7a67f90a3a2f784b703e3892113bd9992f0
3
+ metadata.gz: bc1ccae4ba1b2bdaf065236e1485938a8c547ffdee68dd20aadf6d178ccf7cb5
4
+ data.tar.gz: 0c9fd5df972d3375ffe7ff1ab6a5429cf3f8b8fe76fde9ed6ed29899efdca802
5
5
  SHA512:
6
- metadata.gz: 7b533ef15747bc2c19f7f6675961b1d025954e691e595a29a863b673d99229a99972215f888554bc7ef2508930e10ab4457420a23f0fe05464f39a07f028cd59
7
- data.tar.gz: e39af50f1673a26cc825f64fe6416cc7ec9db39ed5d03bb652e781f7cf93e2db6f46a7d5eb8e7e1aa019ee2b49b7fd551ce53af8514e27637342b97eacf85745
6
+ metadata.gz: 25921bff0c90dbe95be3c4e05a627de73540619023b805277cdf63be18cf670b96dd0ed239328ecd97449d66616b62aad58404296c2dbe16351e722af7846bd5
7
+ data.tar.gz: 3ba8b41dacfcd5bd122a862f2887609f39024dc9cada9301dd524227019303ec9c154f3d2377bf53f599a1a57534ba1dac7d5aa377b28233e76180ca74ede399
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [0.4.6] - 2026-04-07
2
+
3
+ ### Changed
4
+
5
+ - Maximize cached diff reuse to eliminate redundant directory traversals (#43)
6
+ - Build source index during first source walk in `DirectoryDiffer`, removing duplicate `build_source_index` traversal
7
+ - Pass pre-computed removal paths from diff to `FileTransfer`, skipping second destination scan in force mode
8
+ - Add `removal_rel_paths` field to `Diff` model to carry relative paths for `FileTransfer`
9
+ - Skip `show_orphan_preview` directory scan in display phase when no differences exist
10
+
1
11
  ## [0.4.5] - 2026-04-07
2
12
 
3
13
  ### Fixed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dotsync (0.4.5)
4
+ dotsync (0.4.6)
5
5
  fileutils (~> 1.7.3)
6
6
  find (~> 0.2.0)
7
7
  listen (~> 3.9.0)
@@ -94,7 +94,7 @@ module Dotsync
94
94
 
95
95
  show_content_diffs if diff_content && has_modifications?
96
96
 
97
- show_orphan_preview if respond_to?(:manifests_xdg_data_home, true)
97
+ show_orphan_preview if has_differences? && respond_to?(:manifests_xdg_data_home, true)
98
98
  end
99
99
 
100
100
  def show_content_diffs
@@ -115,13 +115,14 @@ module Dotsync
115
115
  mutex = Mutex.new
116
116
 
117
117
  # Only transfer mappings that have actual differences (uses cached diffs)
118
- changed_mappings = valid_mappings.each_with_index.filter_map do |mapping, idx|
119
- mapping if differs[idx].any?
118
+ changed_pairs = valid_mappings.each_with_index.filter_map do |mapping, idx|
119
+ [mapping, differs[idx]] if differs[idx].any?
120
120
  end
121
121
 
122
122
  # Process mappings in parallel - each mapping is independent
123
- Dotsync::Parallel.each(changed_mappings) do |mapping|
124
- Dotsync::FileTransfer.new(mapping).transfer
123
+ Dotsync::Parallel.each(changed_pairs) do |mapping, diff|
124
+ removals = diff.removal_rel_paths.map { |rel| File.join(mapping.dest, rel) }
125
+ Dotsync::FileTransfer.new(mapping, removals: removals).transfer
125
126
  rescue Dotsync::PermissionError => e
126
127
  mutex.synchronize { errors << ["Permission denied: #{e.message}", "Try: chmod +w <path> or check file permissions"] }
127
128
  rescue Dotsync::DiskFullError => e
@@ -3,13 +3,14 @@
3
3
  module Dotsync
4
4
  # Represents the differences between two directories
5
5
  class Diff
6
- attr_reader :additions, :modifications, :removals, :modification_pairs
6
+ attr_reader :additions, :modifications, :removals, :modification_pairs, :removal_rel_paths
7
7
 
8
- def initialize(additions: [], modifications: [], removals: [], modification_pairs: [])
8
+ def initialize(additions: [], modifications: [], removals: [], modification_pairs: [], removal_rel_paths: [])
9
9
  @additions = additions
10
10
  @modifications = modifications
11
11
  @removals = removals
12
12
  @modification_pairs = modification_pairs
13
+ @removal_rel_paths = removal_rel_paths
13
14
  end
14
15
 
15
16
  def any?
@@ -11,11 +11,11 @@ module Dotsync
11
11
  #
12
12
  # This class implements several optimizations to handle large directory trees efficiently:
13
13
  #
14
- # 1. **Pre-indexed source tree** (see #build_source_index)
15
- # Instead of calling File.exist? for each destination file (disk I/O per file),
16
- # we build a Set of all source paths upfront. Checking Set#include? is O(1) in memory
17
- # vs O(1) disk I/O, which is orders of magnitude faster for large trees.
18
- # Impact: ~100x faster for directories with thousands of files.
14
+ # 1. **Source index built during first walk** (see #diff_mapping_directories)
15
+ # The source tree walk that finds additions/modifications also builds a Set of all
16
+ # source paths. In force mode, this Set enables O(1) lookups during the destination
17
+ # walk — replacing per-file File.exist? calls (disk I/O) with hash lookups (memory).
18
+ # Impact: ~100x faster for directories with thousands of files, single source traversal.
19
19
  #
20
20
  # 2. **Early directory pruning with Find.prune** (see #diff_mapping_directories)
21
21
  # When an `only` filter is configured, we prune entire directory subtrees that
@@ -58,6 +58,12 @@ module Dotsync
58
58
  modification_pairs = []
59
59
  removals = []
60
60
 
61
+ # OPTIMIZATION: Build the source index during the first walk.
62
+ # This Set is used in force mode for O(1) lookups during the destination walk,
63
+ # replacing per-file File.exist? calls (disk I/O) with hash lookups (memory).
64
+ # Building it here avoids a second traversal of the source tree.
65
+ source_index = Set.new
66
+
61
67
  # Walk the source tree to find additions and modifications.
62
68
  # Uses bidirectional_include? with Find.prune to skip directories
63
69
  # that are outside the `only` filter, avoiding unnecessary traversal.
@@ -72,6 +78,8 @@ module Dotsync
72
78
  next
73
79
  end
74
80
 
81
+ source_index << src_path
82
+
75
83
  dest_path = File.join(mapping_dest, rel_path)
76
84
 
77
85
  if !File.exist?(dest_path)
@@ -86,11 +94,6 @@ module Dotsync
86
94
 
87
95
  # In force mode, also find files in destination that don't exist in source (removals).
88
96
  if force?
89
- # OPTIMIZATION: Pre-index source tree into a Set for O(1) lookups.
90
- # This replaces per-file File.exist? calls (disk I/O) with hash lookups (memory).
91
- # For a destination with thousands of files, this is orders of magnitude faster.
92
- source_index = build_source_index
93
-
94
97
  Find.find(mapping_dest) do |dest_path|
95
98
  rel_path = dest_path.sub(/^#{Regexp.escape(mapping_dest)}\/?/, "")
96
99
  next if rel_path.empty?
@@ -118,11 +121,16 @@ module Dotsync
118
121
  filtered_modifications = filter_ignores(modifications)
119
122
  modification_pairs = modification_pairs.select { |pair| filtered_modifications.include?(pair[:rel_path]) }
120
123
 
124
+ filtered_removals = filter_ignores(removals)
125
+
121
126
  additions = relative_to_absolute(filter_ignores(additions), mapping_original_dest)
122
127
  modifications = relative_to_absolute(filtered_modifications, mapping_original_dest)
123
- removals = relative_to_absolute(filter_ignores(removals), mapping_original_dest)
128
+ removals = relative_to_absolute(filtered_removals, mapping_original_dest)
124
129
 
125
- Dotsync::Diff.new(additions: additions, modifications: modifications, removals: removals, modification_pairs: modification_pairs)
130
+ Dotsync::Diff.new(
131
+ additions: additions, modifications: modifications, removals: removals,
132
+ modification_pairs: modification_pairs, removal_rel_paths: filtered_removals
133
+ )
126
134
  end
127
135
 
128
136
  def diff_mapping_files
@@ -140,26 +148,6 @@ module Dotsync
140
148
  Dotsync::Diff.new(additions: additions, modifications: modifications, modification_pairs: modification_pairs)
141
149
  end
142
150
 
143
- # Builds a Set of all source paths for O(1) existence checks.
144
- #
145
- # This is used during the destination walk (force mode) to check if a destination
146
- # file exists in the source. Using a Set avoids repeated File.exist? calls,
147
- # replacing disk I/O with memory lookups.
148
- #
149
- # @return [Set<String>] Set of absolute source paths
150
- def build_source_index
151
- index = Set.new
152
- Find.find(mapping_src) do |src_path|
153
- # Apply the same pruning logic as the main source walk
154
- unless @mapping.bidirectional_include?(src_path)
155
- Find.prune
156
- next
157
- end
158
- index << src_path
159
- end
160
- index
161
- end
162
-
163
151
  def filter_ignores(all_paths)
164
152
  return all_paths unless ignores.any?
165
153
  all_paths.reject do |path|
@@ -5,17 +5,17 @@ module Dotsync
5
5
  # Initializes a new FileTransfer instance
6
6
  #
7
7
  # @param mapping [Dotsync::Mapping] the mapping object containing source, destination, force, and ignore details
8
- # @option mapping [String] :src the source directory path
9
- # @option mapping [String] :dest the destination directory path
10
- # @option mapping [Boolean] :force? optional flag to force actions
11
- # @option mapping [Array<String>] :ignores optional list of files/directories to ignore
12
- def initialize(mapping)
8
+ # @param removals [Array<String>, nil] pre-computed absolute paths to remove in force mode.
9
+ # When provided (from cached diff), skips the destination tree traversal in cleanup_folder.
10
+ # When nil, falls back to scanning the destination tree (used by backup transfers).
11
+ def initialize(mapping, removals: nil)
13
12
  @mapping = mapping
14
13
  @src = mapping.src
15
14
  @dest = mapping.dest
16
15
  @force = mapping.force?
17
16
  @inclusions = mapping.inclusions || []
18
17
  @ignores = mapping.ignores || []
18
+ @removals = removals
19
19
  end
20
20
 
21
21
  def transfer
@@ -43,7 +43,13 @@ module Dotsync
43
43
  end
44
44
  transfer_file(@src, target_dest)
45
45
  else
46
- cleanup_folder(@dest) if @force
46
+ if @force
47
+ if @removals
48
+ remove_precomputed_files
49
+ else
50
+ cleanup_folder(@dest)
51
+ end
52
+ end
47
53
  transfer_folder(@src, @dest)
48
54
  end
49
55
  end
@@ -131,6 +137,28 @@ module Dotsync
131
137
  end
132
138
  end
133
139
 
140
+ # Removes files using pre-computed removal paths from the cached diff,
141
+ # avoiding a full destination tree traversal. After deleting files, walks
142
+ # up to clean empty parent directories.
143
+ def remove_precomputed_files
144
+ dest_expanded = File.expand_path(@dest)
145
+
146
+ @removals.each do |path|
147
+ next unless File.file?(path)
148
+
149
+ FileUtils.rm(path)
150
+
151
+ # Clean up empty parent directories up to the destination root
152
+ dir = File.dirname(path)
153
+ while dir != dest_expanded && dir.start_with?(dest_expanded)
154
+ break unless Dir.empty?(dir)
155
+
156
+ FileUtils.rmdir(dir)
157
+ dir = File.dirname(dir)
158
+ end
159
+ end
160
+ end
161
+
134
162
  def cleanup_folder(target_dir)
135
163
  target_dir = File.expand_path(target_dir)
136
164
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dotsync
4
- VERSION = "0.4.5"
4
+ VERSION = "0.4.6"
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.4.5
4
+ version: 0.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Sáenz