revtree 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -2
  3. data/lib/revtree.rb +166 -12
  4. metadata +27 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7c82aecb58c070a313c7d7e7897c7c038d66dfa35d717fae991eeeb19245f6f
4
- data.tar.gz: 6ffc2b90332f026b0cd32c7f6daf7af377e28ced7c679633d6807416d1e9e1ff
3
+ metadata.gz: e1e3a7315f79eab2d444a7b708fa4412dc14fa0d17aff060848a74c4275eefcc
4
+ data.tar.gz: 712bbd90a68a4cd071c1bd647cb9c36752aa01c0c0122cff32d585895695bd1f
5
5
  SHA512:
6
- metadata.gz: b21ee3950eae8d1c09f2d42124d68653c0c21f1f4c0efe1e582d9444cb4bcc90620cb8778a7ed900aeba9f3beab85ca6cca45dfa596ec3f49f29240b7a33cb13
7
- data.tar.gz: 2c0762cb314b65c1c14b8904cef4c18d0d434001193fe84d7f136fe9923de4cfa3bf73bcc253ce295dacb6ecb941479f8f84547fa3760c49006b305c31e720e0
6
+ metadata.gz: abeccda61e8bd8afbdc553550d43a22e786e20e57e0563f2d3e64e8accf2b67152cfc449b9417a8f10ecc3acf9bd58be2a6cde8402d1d0709586368b3f34054b
7
+ data.tar.gz: a90318c8711df1e9ce50d0fde521c7713e4923316be3e5f65e18a73489fbb76a3b9563651a2031f5ba69e19c11f511eeb713f2700e85c64a13cb22817ed5f2f1
data/README.md CHANGED
@@ -14,6 +14,8 @@ It also provides methods to print and serialize the tree structure, as well as t
14
14
  - **Serialization**: Supports serialization to and from JSON format for easy storage and transfer.
15
15
  - **Pretty Printing**: Provides a method to print the directory tree with statuses.
16
16
  - **Iterate with `for_each`**: Traverse the tree and apply a block to files matching specific statuses.
17
+ - **Watch for Changes**: Automatically monitors a directory for changes with `watch`, and triggers actions based on file changes.
18
+ - **Customizable Watch Interval**: Set a custom interval between checks with the `with_interval` method.
17
19
 
18
20
  ## Install
19
21
 
@@ -39,7 +41,7 @@ sudo make install
39
41
  git clone https://github.com/juliankahlert/revtree.git
40
42
  cd revtree
41
43
  gem build revtree.gemspec
42
- sudo gem install --local revtree-0.1.0.gem
44
+ sudo gem install --local revtree-0.1.2.gem
43
45
  ```
44
46
 
45
47
  ## API Documentation
@@ -70,6 +72,18 @@ sudo gem install --local revtree-0.1.0.gem
70
72
  - `&block` (Proc): A block to execute for each file matching the given statuses.
71
73
  - **Behavior**: Iterates over files in the tree, executing the block for each file whose status matches one of the statuses in the whitelist.
72
74
 
75
+ ### `#watch(status_whitelist = [:modified, :added, :removed], &block)`
76
+
77
+ - **Parameters**:
78
+ - `status_whitelist` (Array<Symbol>): List of statuses to watch (e.g., `[:added, :removed]`).
79
+ - `&block` (Proc): A block to execute when a file matching the given statuses is changed.
80
+
81
+ ### `#with_interval(interval)`
82
+
83
+ - **Parameters**:
84
+ - `interval` (Integer): Interval (in seconds) between checks for changes.
85
+ - **Returns**: The `RevTree` instance, enabling method chaining.
86
+
73
87
  ### `RevTree.from_h(h)`
74
88
 
75
89
  - **Parameters**:
@@ -84,6 +98,8 @@ sudo gem install --local revtree-0.1.0.gem
84
98
 
85
99
  ## Example Usage
86
100
 
101
+ ### Comparing two directory structures
102
+
87
103
  ```ruby
88
104
  #!/bin/env ruby
89
105
 
@@ -105,9 +121,27 @@ result_tree.for_each([:added, :removed]) do |file, full_path|
105
121
  end
106
122
  ```
107
123
 
124
+ ### Watching a directory for changes
125
+
126
+ ```ruby
127
+ #!/bin/env ruby
128
+
129
+ require 'revtree'
130
+
131
+ # Create a RevTree for the current directory, including only .rb and .md files
132
+ file_tree = RevTree.new('./', ['*.rb', '*.md'])
133
+
134
+ # Watch for changes in the directory
135
+ file_tree.with_interval(10).watch([:modified, :added, :removed]) do |file, full_path|
136
+ puts "File #{file.name} #{file.status} in #{full_path}"
137
+ end
138
+ ```
139
+
140
+ This second example demonstrates the `watch` method, which continuously monitors the directory tree for changes and triggers a block of code when changes (additions, modifications, or deletions) are detected. The `with_interval` method customizes the time between checks.
141
+
108
142
  ## Encouragement for Contribution
109
143
 
110
- Vontributions from the community are welcome!
144
+ Contributions from the community are welcome!
111
145
  If you find any issues or have ideas for new features, please feel free to submit a pull request or open an issue.
112
146
  Your input helps make RevTree better for everyone.
113
147
 
data/lib/revtree.rb CHANGED
@@ -26,9 +26,31 @@ require 'digest'
26
26
  require 'pathname'
27
27
  require 'json'
28
28
 
29
+ # The `RevTree` class provides a tree structure representing file directories and
30
+ # files, allowing for version tracking based on MD5 hashes.
31
+ #
32
+ # This class can traverse directories, compare versions of trees, and serialize/deserialize
33
+ # itself to/from JSON.
29
34
  class RevTree
30
- attr_reader :children, :type, :name, :rev, :status
35
+ # @return [Array<RevTree>] the list of children in the tree (empty for files)
36
+ attr_reader :children
37
+
38
+ # @return [Symbol] the type of the node (:folder or :file)
39
+ attr_reader :type
40
+
41
+ # @return [String] the name of the file or directory
42
+ attr_reader :name
43
+
44
+ # @return [String] the revision (MD5 hash) of the file or directory
45
+ attr_reader :rev
31
46
 
47
+ # @return [Symbol] the status of the file or directory (:unmodified, :modified, :added, :removed)
48
+ attr_reader :status
49
+
50
+ # Initializes a new `RevTree` object representing a directory or file.
51
+ #
52
+ # @param path [String, Pathname] the path to the file or directory
53
+ # @param whitelist [Array<String>, nil] a list of file patterns to include (optional)
32
54
  def initialize(path, whitelist = nil)
33
55
  @path = Pathname.new(path)
34
56
  @name = @path.basename.to_s
@@ -42,21 +64,19 @@ class RevTree
42
64
  end
43
65
  end
44
66
 
45
- def calculate_file_rev
46
- Digest::MD5.file(@path).hexdigest
47
- end
48
-
49
- def calculate_directory_rev
50
- Digest::MD5.hexdigest(@children.map(&:rev).join)
51
- end
52
-
67
+ # Prints the tree structure, including file names and statuses, to the console.
68
+ #
69
+ # @param indent [Integer] the indentation level (default: 0)
70
+ # @return [void]
53
71
  def print_tree(indent = 0)
54
72
  status_str = @status ? " (status: #{@status})" : ''
55
73
  puts "#{' ' * indent}#{@type == :folder ? '[Folder]' : '[File]'} #{@name} (rev: #{@rev})#{status_str}"
56
74
  @children.each { |child| child.print_tree(indent + 1) }
57
75
  end
58
76
 
59
- # Serialize the RevTree object to JSON
77
+ # Serializes the `RevTree` object to a hash.
78
+ #
79
+ # @return [Hash] a hash representing the object
60
80
  def to_h
61
81
  {
62
82
  type: @type,
@@ -67,14 +87,25 @@ class RevTree
67
87
  }
68
88
  end
69
89
 
70
- def to_json(*_args)
90
+ # Converts the `RevTree` object to JSON format.
91
+ #
92
+ # @return [String] a JSON string representing the object
93
+ def to_json
71
94
  JSON.pretty_generate(self.to_h)
72
95
  end
73
96
 
97
+ # Reconstructs a `RevTree` object from a hash.
98
+ #
99
+ # @param h [Hash] the hash to deserialize
100
+ # @return [RevTree] the reconstructed `RevTree` object
74
101
  def self.from_h(h)
75
102
  new_tree(h[:name], h[:type].to_sym, h[:rev], h[:children], h[:status])
76
103
  end
77
104
 
105
+ # Reconstructs a `RevTree` object from a JSON string.
106
+ #
107
+ # @param json_str [String] the JSON string to deserialize
108
+ # @return [RevTree] the reconstructed `RevTree` object
78
109
  def self.from_json(json_str)
79
110
  data = JSON.parse(json_str, symbolize_names: true)
80
111
  file_tree = from_h(data)
@@ -82,14 +113,79 @@ class RevTree
82
113
  file_tree
83
114
  end
84
115
 
85
- def for_each(status_whitelist, &block)
116
+ # Executes a block of code for each file matching the provided status whitelist.
117
+ #
118
+ # @param status_whitelist [Array<Symbol>] the list of statuses to match (:added, :modified, etc.)
119
+ # @yield [node, full_path] the block to be executed for each matching file
120
+ # @yieldparam node [RevTree] the current node being traversed
121
+ # @yieldparam full_path [String] the full path of the current node
122
+ # @return [void]
123
+ def for_each(status_whitelist = [:unmodified, :modified, :added, :removed], &block)
86
124
  return unless block_given?
87
125
 
88
126
  RevTree.traverse_tree(self, status_whitelist, @path, &block)
89
127
  end
90
128
 
129
+ # Watches the tree for changes
130
+ #
131
+ # Compares the refreshed tree to its last version and calls the provided block
132
+ # for each node that matches the statuses in the `status_whitelist`.
133
+ #
134
+ # @param status_whitelist [Array<Symbol>] the list of statuses to match (:added, :modified, etc.)
135
+ # @yield [node, full_path] the block to be executed for each matching file
136
+ # @yieldparam node [RevTree] the node that was changed
137
+ # @yieldparam full_path [String] the full path of the node
138
+ # @return [void]
139
+ def watch(status_whitelist = [:modified, :added, :removed], &block)
140
+ current_tree = self
141
+ @interval ||= 5
142
+
143
+ Signal.trap('INT') { exit }
144
+ Signal.trap('TERM') { exit }
145
+ loop do
146
+ sleep @interval
147
+
148
+ new_tree = RevTree.new(@path, @whitelist)
149
+ diff_tree = RevTree.compare(current_tree, new_tree)
150
+
151
+ next if diff_tree.nil?
152
+
153
+ diff_tree.for_each(status_whitelist) do |node, full_path|
154
+ block.call(node, full_path)
155
+ end
156
+
157
+ current_tree = new_tree
158
+ end
159
+ end
160
+
161
+ # Sets the interval for the watch method's sleep duration.
162
+ #
163
+ # @param interval [Integer] the number of seconds to sleep between checks
164
+ # @return [RevTree] the current instance for chaining
165
+ def with_interval(interval)
166
+ @interval = interval
167
+ self
168
+ end
169
+
91
170
  private
92
171
 
172
+ # Calculates the MD5 hash for the file.
173
+ #
174
+ # @return [String] the MD5 hash of the file
175
+ def calculate_file_rev
176
+ Digest::MD5.file(@path).hexdigest
177
+ end
178
+
179
+ # Calculates the MD5 hash for the directory based on its children.
180
+ #
181
+ # @return [String] the MD5 hash of the directory
182
+ def calculate_directory_rev
183
+ Digest::MD5.hexdigest(@children.map(&:rev).join)
184
+ end
185
+
186
+ # Initializes the directory node by traversing its children.
187
+ #
188
+ # @return [void]
93
189
  def init_dir
94
190
  @type = :folder
95
191
  @children = @path.children
@@ -98,12 +194,23 @@ class RevTree
98
194
  @rev = calculate_directory_rev
99
195
  end
100
196
 
197
+ # Initializes the file node by calculating its revision.
198
+ #
199
+ # @return [void]
101
200
  def init_file
102
201
  @type = :file
103
202
  @children = []
104
203
  @rev = calculate_file_rev
105
204
  end
106
205
 
206
+ # Rebuilds a `RevTree` from its serialized components.
207
+ #
208
+ # @param name [String] the name of the node
209
+ # @param type [Symbol] the type of the node (:folder or :file)
210
+ # @param rev [String] the revision hash of the node
211
+ # @param children [Array<Hash>] the child nodes (if any)
212
+ # @param status [Symbol] the status of the node (:unmodified, :modified, etc.)
213
+ # @return [RevTree] the reconstructed tree
107
214
  def self.new_tree(name, type, rev, children, status = :unmodified)
108
215
  tree = allocate
109
216
  tree.instance_variable_set(:@name, name)
@@ -118,6 +225,10 @@ class RevTree
118
225
  tree
119
226
  end
120
227
 
228
+ # Determines whether a file or directory should be included in the tree.
229
+ #
230
+ # @param path [Pathname] the path to the file or directory
231
+ # @return [Boolean] `true` if the path should be included, `false` otherwise
121
232
  def include_in_tree?(path)
122
233
  return false if path.directory? && path.basename.to_s.start_with?('.')
123
234
 
@@ -127,6 +238,11 @@ class RevTree
127
238
  @whitelist.any? { |p| File.fnmatch?(p, path.basename.to_s) }
128
239
  end
129
240
 
241
+ # Compares two `RevTree` nodes (old and new) and returns a tree with appropriate status.
242
+ #
243
+ # @param old [RevTree, nil] the old version of the tree
244
+ # @param new [RevTree, nil] the new version of the tree
245
+ # @return [RevTree, nil] the resulting tree with status updates or `nil`
130
246
  def self.compare(old, new)
131
247
  return nil if old.nil? && new.nil?
132
248
 
@@ -140,18 +256,31 @@ class RevTree
140
256
  end
141
257
  end
142
258
 
259
+ # Handles the addition of a new node.
260
+ #
261
+ # @param new [RevTree] the new node
262
+ # @return [RevTree] the node with the status set to `:added`
143
263
  def self.handle_addition(new)
144
264
  with_status = new.dup
145
265
  with_status.instance_variable_set(:@status, :added)
146
266
  with_status
147
267
  end
148
268
 
269
+ # Handles the removal of an old node.
270
+ #
271
+ # @param old [RevTree] the old node
272
+ # @return [RevTree] the node with the status set to `:removed`
149
273
  def self.handle_removal(old)
150
274
  with_status = old.dup
151
275
  with_status.instance_variable_set(:@status, :removed)
152
276
  with_status
153
277
  end
154
278
 
279
+ # Handles the modification of a node.
280
+ #
281
+ # @param old [RevTree] the old node
282
+ # @param new [RevTree] the new node
283
+ # @return [RevTree] the node with the status set to `:modified`
155
284
  def self.handle_modification(old, new)
156
285
  if old.type == :folder && new.type == :folder
157
286
  compare_folders(old, new, :modified)
@@ -162,6 +291,12 @@ class RevTree
162
291
  end
163
292
  end
164
293
 
294
+ # Compares two folder nodes and returns a merged node with status updates.
295
+ #
296
+ # @param old [RevTree] the old folder node
297
+ # @param new [RevTree] the new folder node
298
+ # @param status [Symbol] the status to apply (:modified or :unmodified)
299
+ # @return [RevTree] the resulting folder node with status updates
165
300
  def self.compare_folders(old, new, status)
166
301
  combined_children = merge_children(old.children, new.children)
167
302
  with_status = new.dup
@@ -174,6 +309,11 @@ class RevTree
174
309
  with_status
175
310
  end
176
311
 
312
+ # Handles the unmodified status of a node.
313
+ #
314
+ # @param old [RevTree] the old node
315
+ # @param new [RevTree] the new node
316
+ # @return [RevTree] the node with the status set to `:unmodified`
177
317
  def self.handle_unmodified(old, new)
178
318
  if old.type == :folder && new.type == :folder
179
319
  compare_folders(old, new, :unmodified)
@@ -184,6 +324,11 @@ class RevTree
184
324
  end
185
325
  end
186
326
 
327
+ # Merges the children of two nodes.
328
+ #
329
+ # @param old_children [Array<RevTree>] the children of the old node
330
+ # @param new_children [Array<RevTree>] the children of the new node
331
+ # @return [Array<Array<RevTree, RevTree>>] an array of paired old and new children
187
332
  def self.merge_children(old_children, new_children)
188
333
  all_names = (old_children.map(&:name) + new_children.map(&:name)).uniq
189
334
  all_names.map do |name|
@@ -193,6 +338,15 @@ class RevTree
193
338
  end
194
339
  end
195
340
 
341
+ # Traverses the tree and executes a block for each file matching the provided status whitelist.
342
+ #
343
+ # @param node [RevTree] the current node being traversed
344
+ # @param status_whitelist [Array<Symbol>] the list of statuses to match
345
+ # @param current_path [Pathname] the current path
346
+ # @yield [node, full_path] the block to be executed for each matching file
347
+ # @yieldparam node [RevTree] the current node being traversed
348
+ # @yieldparam full_path [String] the full path of the current node
349
+ # @return [void]
196
350
  def self.traverse_tree(node, status_whitelist, current_path, &block)
197
351
  if node.type == :file && status_whitelist.include?(node.status)
198
352
  block.call(node, File.expand_path(current_path.to_s))
metadata CHANGED
@@ -1,15 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: revtree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Kahlert
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-06 00:00:00.000000000 Z
11
+ date: 2024-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: yard
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.9.37
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.9'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.9.37
13
33
  - !ruby/object:Gem::Dependency
14
34
  name: rspec
15
35
  requirement: !ruby/object:Gem::Requirement
@@ -31,7 +51,8 @@ dependencies:
31
51
  - !ruby/object:Gem::Version
32
52
  version: '3.4'
33
53
  description: RevTree builds a recursive directory tree and compares file revisions.
34
- It can mark files and folders as added, removed, modified, or unmodified.
54
+ It can mark files and folders as added, removed, modified, or unmodified. For convenience,
55
+ RevTree also allows you to watch for changes in a directory tree.
35
56
  email:
36
57
  - 90937526+juliankahlert@users.noreply.github.com
37
58
  executables: []
@@ -43,7 +64,9 @@ files:
43
64
  homepage: https://github.com/juliankahlert/revtree
44
65
  licenses:
45
66
  - MIT
46
- metadata: {}
67
+ metadata:
68
+ homepage_uri: https://github.com/juliankahlert/revtree
69
+ source_code_uri: https://github.com/juliankahlert/revtree
47
70
  post_install_message:
48
71
  rdoc_options: []
49
72
  require_paths: