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 +4 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +1 -1
- data/lib/dotsync/actions/concerns/mappings_transfer.rb +6 -5
- data/lib/dotsync/models/diff.rb +3 -2
- data/lib/dotsync/utils/directory_differ.rb +20 -32
- data/lib/dotsync/utils/file_transfer.rb +34 -6
- data/lib/dotsync/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc1ccae4ba1b2bdaf065236e1485938a8c547ffdee68dd20aadf6d178ccf7cb5
|
|
4
|
+
data.tar.gz: 0c9fd5df972d3375ffe7ff1ab6a5429cf3f8b8fe76fde9ed6ed29899efdca802
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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
|
-
|
|
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(
|
|
124
|
-
|
|
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
|
data/lib/dotsync/models/diff.rb
CHANGED
|
@@ -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. **
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
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(
|
|
128
|
+
removals = relative_to_absolute(filtered_removals, mapping_original_dest)
|
|
124
129
|
|
|
125
|
-
Dotsync::Diff.new(
|
|
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
|
-
# @
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/dotsync/version.rb
CHANGED