revtree 0.1.0 → 0.1.2

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.
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: