appydave-tools 0.37.0 → 0.39.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.
@@ -0,0 +1,342 @@
1
+ # Code Quality Analysis - Last 7 Days (Nov 14-21, 2025)
2
+
3
+ ## Summary
4
+ - **Commits analyzed:** 30 commits
5
+ - **Files changed:** 29 unique files
6
+ - **Key areas:** DAM CLI, Configuration system, Project resolution, S3 operations
7
+ - **Analysis date:** 2025-01-21
8
+
9
+ ### Commit Themes
10
+ The last week focused heavily on:
11
+ 1. **Configuration loading fixes** (7x config load calls - commit 94d3ea0)
12
+ 2. **Brand resolution** (v- prefix handling)
13
+ 3. **DAM CLI UX improvements** (validation, table formatting, status displays)
14
+ 4. **Debug infrastructure** (DAM_DEBUG logging for remote debugging)
15
+ 5. **Project resolver** (regex capture group bug fix - commit 9e49668)
16
+
17
+ ## Critical Issues 🔴
18
+
19
+ ### 1. Code Duplication: Git Operations Helper Methods
20
+ - **Found in:**
21
+ - `lib/appydave/tools/dam/status.rb:243-275` (4 methods)
22
+ - `lib/appydave/tools/dam/repo_status.rb:109-144` (6 methods)
23
+ - `lib/appydave/tools/dam/repo_push.rb:107-119` (3 methods)
24
+
25
+ - **Description:** Three classes have nearly identical git helper methods:
26
+ ```ruby
27
+ # DUPLICATED across Status, RepoStatus, RepoPush:
28
+ def current_branch
29
+ `git -C "#{brand_path}" rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
30
+ end
31
+
32
+ def commits_ahead
33
+ `git -C "#{brand_path}" rev-list --count @{upstream}..HEAD 2>/dev/null`.strip.to_i
34
+ end
35
+
36
+ def commits_behind
37
+ `git -C "#{brand_path}" rev-list --count HEAD..@{upstream} 2>/dev/null`.strip.to_i
38
+ end
39
+
40
+ # Plus: remote_url, modified_files_count, untracked_files_count, uncommitted_changes?
41
+ ```
42
+
43
+ - **Impact:**
44
+ - **High maintenance burden** - Git command changes require updates in 3 places
45
+ - **Bug risk** - Bug fixes might miss one location
46
+ - **~90 lines duplicated** across 3 files
47
+ - **Testing burden** - Same behavior tested 3 times
48
+
49
+ - **Recommendation:**
50
+ - Extract to `lib/appydave/tools/dam/git_helper.rb` module:
51
+ ```ruby
52
+ module Appydave::Tools::Dam::GitHelper
53
+ def current_branch(repo_path)
54
+ `git -C "#{repo_path}" rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
55
+ end
56
+ # ... all other git methods
57
+ end
58
+ ```
59
+ - Mix into classes: `include GitHelper`
60
+ - Call: `current_branch(brand_path)`
61
+
62
+ - **Estimated effort:** Medium (2-3 hours including tests)
63
+
64
+ ### 2. Code Duplication: Configuration Loading Pattern
65
+ - **Found in:**
66
+ - `lib/appydave/tools/dam/config.rb` - 7 calls to `Config.configure`
67
+ - `lib/appydave/tools/dam/project_resolver.rb:144`
68
+ - `lib/appydave/tools/dam/status.rb:32`
69
+ - `lib/appydave/tools/dam/repo_status.rb:36,55`
70
+ - `lib/appydave/tools/dam/share_operations.rb:68`
71
+ - Plus 15+ more locations across DAM module
72
+
73
+ - **Description:** `Appydave::Tools::Configuration::Config.configure` called repeatedly, often multiple times in the same method or class
74
+
75
+ - **Pattern Example:**
76
+ ```ruby
77
+ # lib/appydave/tools/dam/config.rb:27-29
78
+ def brand_path(brand_key)
79
+ Appydave::Tools::Configuration::Config.configure # ← Called
80
+ brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(brand_key)
81
+ # ...
82
+ end
83
+
84
+ # lib/appydave/tools/dam/config.rb:51-52
85
+ def project_path(brand_key, project_id)
86
+ Appydave::Tools::Configuration::Config.configure # ← Called again
87
+ brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(brand_key)
88
+ # ...
89
+ end
90
+ ```
91
+
92
+ - **Impact:**
93
+ - **Performance concern** - Config loaded 7+ times in tight loops (commit 94d3ea0 fixed this)
94
+ - **Code noise** - Reduces readability with repeated boilerplate
95
+ - **Unclear responsibility** - Is `Config.configure` idempotent? (Yes, but not obvious)
96
+
97
+ - **Recommendation:**
98
+ - **Option A (Quick fix):** Call once per class in initializer/module load
99
+ - **Option B (Better):** Make `Config.configure` memoized singleton (already done in commit 94d3ea0)
100
+ - **Option C (Best):** Dependency injection - pass config objects instead of calling global
101
+
102
+ - **Estimated effort:** Small (already partially fixed in 94d3ea0)
103
+
104
+ ### 3. Architectural Issue: Brand Resolution Logic Scattered
105
+ - **Found in:**
106
+ - `lib/appydave/tools/dam/config.rb:91-116` (`expand_brand` method)
107
+ - `lib/appydave/tools/dam/project_resolver.rb:118-121` (regex capture + `.sub(/^v-/, '')`)
108
+ - `bin/dam` - Multiple inline brand validation checks
109
+
110
+ - **Description:** Brand key transformation (`appydave` ↔ `v-appydave`) handled in multiple places with slightly different logic:
111
+ ```ruby
112
+ # Config.rb - lookup from brands.json
113
+ brand = brands_config.brands.find { |b| b.key.downcase == shortcut_str.downcase }
114
+ return "v-#{brand.key}" if brand
115
+
116
+ # ProjectResolver.rb - strip v- prefix
117
+ brand_key = brand_with_prefix.sub(/^v-/, '')
118
+
119
+ # Hardcoded shortcuts in Config.rb:111-114
120
+ case normalized
121
+ when 'joy' then 'v-beauty-and-joy'
122
+ when 'ss' then 'v-supportsignal'
123
+ ```
124
+
125
+ - **Impact:**
126
+ - **Fragile** - Adding new brand shortcuts requires changes in multiple files
127
+ - **Inconsistent** - Some code assumes v- prefix, some doesn't
128
+ - **Hard to test** - Logic spread across multiple layers
129
+ - **Bug history** - Recent fixes (commits 94d3ea0, 771a20f, 3966f16) show ongoing confusion
130
+
131
+ - **Recommendation:**
132
+ - Create `BrandResolver` class with clear responsibilities:
133
+ ```ruby
134
+ class BrandResolver
135
+ def self.expand(shortcut) # appydave → v-appydave
136
+ def self.normalize(brand) # v-appydave → appydave
137
+ def self.validate(brand) # raises if invalid
138
+ def self.to_config_key(brand) # always returns key form (no v-)
139
+ def self.to_display(brand) # always returns display form (v-)
140
+ end
141
+ ```
142
+ - Centralize all brand name transformations
143
+ - Document which methods expect which format
144
+
145
+ - **Estimated effort:** Large (1-2 days including refactor + tests)
146
+
147
+ ## Moderate Issues 🟡
148
+
149
+ ### 1. Pattern Inconsistency: Config Loading in Tests
150
+ - **Found in:**
151
+ - `spec/appydave/tools/dam/project_listing_spec.rb:23-29` (extensive mocking)
152
+ - `spec/appydave/tools/dam/project_resolver_spec.rb:304-307` (partial mocking)
153
+
154
+ - **Pattern A (ProjectListingSpec):** Heavy mocking of Config class methods
155
+ ```ruby
156
+ allow(Appydave::Tools::Dam::Config).to receive_messages(
157
+ projects_root: temp_root,
158
+ available_brands: %w[appydave voz]
159
+ )
160
+ allow(Appydave::Tools::Dam::Config).to receive(:brand_path).with('appydave').and_return(brand1_path)
161
+ # 20 lines of allow() statements
162
+ ```
163
+
164
+ - **Pattern B (ProjectResolverSpec):** Shared context with real filesystem
165
+ ```ruby
166
+ include_context 'with vat filesystem and brands', brands: %w[appydave voz]
167
+ # Uses actual temp directories, minimal mocking
168
+ ```
169
+
170
+ - **Impact:**
171
+ - **Confusion** - New contributors don't know which pattern to use
172
+ - **Test brittleness** - Heavy mocking in ProjectListingSpec (20 mocks vs 252 lines = 8% mock density)
173
+ - **Maintenance burden** - Config changes break many mocks
174
+
175
+ - **Recommendation:**
176
+ - **Standardize on Pattern B** - Real filesystem with shared contexts
177
+ - Refactor `project_listing_spec.rb` to use `with vat filesystem` context
178
+ - Reserve mocking for external services (S3, AWS) not internal DAM classes
179
+
180
+ - **Estimated effort:** Medium (half day to refactor tests)
181
+
182
+ ### 2. Pattern Inconsistency: Error Handling
183
+ - **Found in:**
184
+ - `lib/appydave/tools/dam/project_resolver.rb:19` - `raise '❌ Error message'`
185
+ - `lib/appydave/tools/dam/config_loader.rb:27` - `raise ConfigNotFoundError, <<~ERROR`
186
+ - `lib/appydave/tools/dam/share_operations.rb:175` - `raise ArgumentError, 'message'`
187
+ - `bin/dam` - Multiple `rescue StandardError => e` blocks with `puts` + `exit 1`
188
+
189
+ - **Patterns:**
190
+ - **String errors:** `raise '❌ ...'` (ProjectResolver)
191
+ - **Custom exceptions:** `ConfigNotFoundError` (ConfigLoader)
192
+ - **Standard exceptions:** `ArgumentError` (ShareOperations)
193
+ - **CLI error handling:** `rescue → puts → exit` (bin/dam)
194
+
195
+ - **Recommendation:**
196
+ - Define DAM-specific exception hierarchy:
197
+ ```ruby
198
+ module Appydave::Tools::Dam
199
+ class DamError < StandardError; end
200
+ class BrandNotFoundError < DamError; end
201
+ class ProjectNotFoundError < DamError; end
202
+ class ConfigurationError < DamError; end
203
+ end
204
+ ```
205
+ - Use consistently throughout DAM module
206
+ - Keep CLI-level `rescue StandardError` in bin/dam for user-friendly messages
207
+
208
+ - **Estimated effort:** Small (few hours)
209
+
210
+ ### 3. Missing Abstraction: Directory Size Calculation
211
+ - **Found in:**
212
+ - `lib/appydave/tools/dam/project_listing.rb:154-164`
213
+ - `lib/appydave/tools/dam/s3_operations.rb:770-778`
214
+ - `lib/appydave/tools/dam/sync_from_ssd.rb:270-278` (format_bytes)
215
+
216
+ - **Description:** Same directory size calculation logic duplicated 3+ times
217
+
218
+ - **Recommendation:**
219
+ - Extract to `lib/appydave/tools/dam/file_utils.rb`:
220
+ ```ruby
221
+ module FileUtils
222
+ def self.directory_size(path)
223
+ def self.format_size(bytes)
224
+ end
225
+ ```
226
+
227
+ - **Estimated effort:** Small (1 hour)
228
+
229
+ ## Minor Observations 🔵
230
+
231
+ ### 1. Debugging Infrastructure (GOOD - Protected Pattern ✅)
232
+ - **Found in:**
233
+ - `lib/appydave/tools/configuration/models/config_base.rb:33-58`
234
+ - `lib/appydave/tools/dam/project_resolver.rb:149,153`
235
+ - Multiple `puts "DEBUG: ..."` statements
236
+
237
+ - **Description:** DAM_DEBUG and DEBUG env var support added for remote debugging (commit 8844aa9)
238
+
239
+ - **Impact:** **Positive** - Essential for production debugging
240
+
241
+ - **Recommendation:** **Keep as-is** - This is intentional defensive programming, not code bloat
242
+
243
+ ### 2. Regex Capture Group Bug Fixed
244
+ - **Fixed in:** commit 9e49668
245
+ - **Issue:** `Regexp.last_match` reset by `.sub()` call
246
+ - **Solution:** Capture before transformation
247
+ ```ruby
248
+ project = Regexp.last_match(2) # Capture BEFORE
249
+ brand_key = brand_with_prefix.sub(/^v-/, '') # Then modify
250
+ ```
251
+
252
+ - **Impact:** **Positive** - Proper fix, not hidden with mocks
253
+
254
+ ## Positive Patterns ✅
255
+
256
+ ### 1. Test Organization with Shared Contexts
257
+ - **Found in:** `spec/appydave/tools/dam/project_resolver_spec.rb`
258
+ - **Why it's good:**
259
+ - Uses `include_context 'with vat filesystem and brands'`
260
+ - Real temp directories, minimal mocking
261
+ - Tests actual behavior, not implementation details
262
+ - Easy to understand and maintain
263
+
264
+ - **Recommend:** Use this pattern for all DAM specs
265
+
266
+ ### 2. Configuration Debug Logging
267
+ - **Found in:** `lib/appydave/tools/configuration/models/config_base.rb`
268
+ - **Why it's good:**
269
+ - Toggleable with DAM_DEBUG env var
270
+ - Shows config load path, file existence, parse errors
271
+ - Includes JSON output for verification
272
+ - Essential for remote debugging production issues
273
+
274
+ - **Recommend:** Continue this pattern for all critical configuration paths
275
+
276
+ ### 3. Defensive Nil Checking
277
+ - **Found in:** commit a205319
278
+ - **Example:**
279
+ ```ruby
280
+ def resolve(brand, project_hint)
281
+ raise '❌ Project name is required' if project_hint.nil? || project_hint.empty?
282
+ ```
283
+
284
+ - **Why it's good:** Prevents cryptic `include?` errors on nil
285
+ - **Recommend:** Add similar guards to all public methods expecting non-nil arguments
286
+
287
+ ## Prioritized Action Items
288
+
289
+ ### High Priority (Do First)
290
+ 1. [ ] **Extract GitHelper module** - Eliminate 90 lines of duplication (status.rb, repo_status.rb, repo_push.rb)
291
+ - lib/appydave/tools/dam/git_helper.rb:1
292
+ 2. [ ] **Create BrandResolver abstraction** - Centralize brand transformation logic
293
+ - lib/appydave/tools/dam/brand_resolver.rb:1
294
+ 3. [ ] **Refactor project_listing_spec.rb** - Use shared filesystem context instead of 20 mocks
295
+ - spec/appydave/tools/dam/project_listing_spec.rb:1
296
+
297
+ ### Medium Priority (Do Soon)
298
+ 1. [ ] **Define DAM exception hierarchy** - Replace string raises with typed exceptions
299
+ - lib/appydave/tools/dam/errors.rb:1
300
+ 2. [ ] **Extract FileUtils helpers** - Consolidate directory_size and format_size methods
301
+ - lib/appydave/tools/dam/file_utils.rb:1
302
+ 3. [ ] **Document Config.configure memoization** - Add comment explaining idempotent behavior
303
+ - lib/appydave/tools/configuration/config.rb:15
304
+
305
+ ### Low Priority (Future Improvement)
306
+ 1. [ ] **Add integration tests** - Test brand resolution end-to-end across modules
307
+ 2. [ ] **Extract CLI argument parsing** - bin/dam has 200+ line methods, extract parsers
308
+ 3. [ ] **Consider dependency injection** - Pass config objects instead of global Config calls
309
+
310
+ ## Statistics
311
+ - **Total duplicated code:** ~150 lines (90 git methods + 40 directory utils + 20 config calls)
312
+ - **Test mock density:** ProjectListingSpec: 20 mocks / 252 lines = 8% (acceptable, but could improve)
313
+ - **Pattern inconsistencies:** 3 error handling patterns, 2 test patterns
314
+ - **Large methods:** bin/dam has multiple 50+ line methods (help commands, parsers)
315
+ - **Configuration loading:** Fixed in commit 94d3ea0 (7x redundant calls → memoized singleton)
316
+
317
+ ## Analysis Methodology
318
+
319
+ ### Commands Used:
320
+ ```bash
321
+ # Discovery
322
+ git log --since="7 days ago" --pretty=format:"%h - %s (%an, %ar)"
323
+ git log --since="7 days ago" --name-only | sort | uniq -c | sort -rn
324
+
325
+ # Duplication detection
326
+ grep -rn "def.*brand.*path" lib/appydave/tools/dam/
327
+ grep -rn "git -C.*rev-parse|git -C.*rev-list" lib/appydave/tools/dam/
328
+ grep -c "allow.*to receive|double|instance_double" spec/appydave/tools/dam/*.rb
329
+
330
+ # File analysis
331
+ find lib/appydave/tools/dam -name "*.rb" -exec wc -l {} \;
332
+ ```
333
+
334
+ ### Files Analyzed:
335
+ - 12 DAM module files (config.rb, project_resolver.rb, status.rb, repo_status.rb, s3_operations.rb, etc.)
336
+ - 7 spec files
337
+ - bin/dam CLI script
338
+ - Configuration system files
339
+
340
+ ---
341
+
342
+ **Report Generated:** 2025-01-21 by Claude Code Quality Retrospective Analysis
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Dam
6
+ # Centralized brand name resolution and transformation
7
+ #
8
+ # Handles conversion between:
9
+ # - Shortcuts: 'appydave', 'ad', 'joy', 'ss'
10
+ # - Config keys: 'appydave', 'beauty-and-joy', 'supportsignal'
11
+ # - Display names: 'v-appydave', 'v-beauty-and-joy', 'v-supportsignal'
12
+ #
13
+ # @example
14
+ # BrandResolver.expand('ad') # => 'v-appydave'
15
+ # BrandResolver.normalize('v-voz') # => 'voz'
16
+ # BrandResolver.to_config_key('ad') # => 'appydave'
17
+ # BrandResolver.to_display('voz') # => 'v-voz'
18
+ class BrandResolver
19
+ class << self
20
+ # Expand shortcut or key to full display name
21
+ # @param shortcut [String] Brand shortcut or key
22
+ # @return [String] Full brand name with v- prefix
23
+ def expand(shortcut)
24
+ return shortcut.to_s if shortcut.to_s.start_with?('v-')
25
+
26
+ key = to_config_key(shortcut)
27
+ "v-#{key}"
28
+ end
29
+
30
+ # Normalize brand name to config key (strip v- prefix)
31
+ # @param brand [String] Brand name (with or without v-)
32
+ # @return [String] Config key without v- prefix
33
+ def normalize(brand)
34
+ brand.to_s.sub(/^v-/, '')
35
+ end
36
+
37
+ # Convert to config key (handles shortcuts)
38
+ # @param input [String] Shortcut, key, or display name
39
+ # @return [String] Config key
40
+ def to_config_key(input)
41
+ # Strip v- prefix first
42
+ normalized = normalize(input)
43
+
44
+ # Look up from brands.json
45
+ Appydave::Tools::Configuration::Config.configure
46
+ brands_config = Appydave::Tools::Configuration::Config.brands
47
+
48
+ # Check if matches brand key (case-insensitive)
49
+ brand = brands_config.brands.find { |b| b.key.downcase == normalized.downcase }
50
+ return brand.key if brand
51
+
52
+ # Check if matches shortcut (case-insensitive)
53
+ brand = brands_config.brands.find { |b| b.shortcut.downcase == normalized.downcase }
54
+ return brand.key if brand
55
+
56
+ # Fall back to hardcoded shortcuts (backward compatibility)
57
+ case normalized.downcase
58
+ when 'ad' then 'appydave'
59
+ when 'joy' then 'beauty-and-joy'
60
+ when 'ss' then 'supportsignal'
61
+ else
62
+ normalized.downcase
63
+ end
64
+ end
65
+
66
+ # Convert to display name (always v- prefix)
67
+ # @param input [String] Shortcut, key, or display name
68
+ # @return [String] Display name with v- prefix
69
+ def to_display(input)
70
+ expand(input)
71
+ end
72
+
73
+ # Validate brand exists in filesystem
74
+ # @param brand [String] Brand to validate
75
+ # @raise [BrandNotFoundError] if brand invalid
76
+ # @return [String] Config key if valid
77
+ def validate(brand)
78
+ key = to_config_key(brand)
79
+
80
+ # Build brand path (avoiding circular dependency with Config.brand_path)
81
+ Appydave::Tools::Configuration::Config.configure
82
+ brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(key)
83
+
84
+ # If brand has configured video_projects path, use it
85
+ if brand_info.locations.video_projects && !brand_info.locations.video_projects.empty?
86
+ brand_path = brand_info.locations.video_projects
87
+ else
88
+ # Fall back to projects_root + expanded brand name
89
+ root = Config.projects_root
90
+ brand_path = File.join(root, expand(key))
91
+ end
92
+
93
+ unless Dir.exist?(brand_path)
94
+ available = Config.available_brands_display
95
+ raise BrandNotFoundError.new(brand, available)
96
+ end
97
+
98
+ key
99
+ rescue StandardError => e
100
+ raise BrandNotFoundError, e.message unless e.is_a?(BrandNotFoundError)
101
+
102
+ raise
103
+ end
104
+
105
+ # Check if brand exists (returns boolean instead of raising)
106
+ # @param brand [String] Brand to check
107
+ # @return [Boolean] true if brand exists
108
+ def exists?(brand)
109
+ validate(brand)
110
+ true
111
+ rescue BrandNotFoundError
112
+ false
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -85,34 +85,11 @@ module Appydave
85
85
  end
86
86
 
87
87
  # Expand brand shortcut to full brand name
88
- # Reads from brands.json if available, falls back to hardcoded shortcuts
88
+ # Delegates to BrandResolver for centralized brand resolution
89
89
  # @param shortcut [String] Brand shortcut (e.g., 'appydave', 'ad', 'APPYDAVE')
90
90
  # @return [String] Full brand name (e.g., 'v-appydave')
91
91
  def expand_brand(shortcut)
92
- shortcut_str = shortcut.to_s
93
-
94
- return shortcut_str if shortcut_str.start_with?('v-')
95
-
96
- # Try to read from brands.json
97
- Appydave::Tools::Configuration::Config.configure
98
- brands_config = Appydave::Tools::Configuration::Config.brands
99
-
100
- # Check if input matches a brand key (case-insensitive)
101
- brand = brands_config.brands.find { |b| b.key.downcase == shortcut_str.downcase }
102
- return "v-#{brand.key}" if brand
103
-
104
- # Check if input matches a brand shortcut (case-insensitive)
105
- brand = brands_config.brands.find { |b| b.shortcut.downcase == shortcut_str.downcase }
106
- return "v-#{brand.key}" if brand
107
-
108
- # Fall back to hardcoded shortcuts for backwards compatibility
109
- normalized = shortcut_str.downcase
110
- case normalized
111
- when 'joy' then 'v-beauty-and-joy'
112
- when 'ss' then 'v-supportsignal'
113
- else
114
- "v-#{normalized}"
115
- end
92
+ BrandResolver.expand(shortcut)
116
93
  end
117
94
 
118
95
  # Get list of available brands
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Dam
6
+ # Base error for all DAM operations
7
+ class DamError < StandardError; end
8
+
9
+ # Raised when brand directory not found
10
+ class BrandNotFoundError < DamError
11
+ def initialize(brand, available_brands = nil)
12
+ message = "Brand directory not found: #{brand}"
13
+ message += "\n\nAvailable brands:\n#{available_brands}" if available_brands && !available_brands.empty?
14
+ super(message)
15
+ end
16
+ end
17
+
18
+ # Raised when project not found in brand
19
+ class ProjectNotFoundError < DamError; end
20
+
21
+ # Raised when configuration invalid or missing
22
+ class ConfigurationError < DamError; end
23
+
24
+ # Raised when S3 operation fails
25
+ class S3OperationError < DamError; end
26
+
27
+ # Raised when git operation fails
28
+ class GitOperationError < DamError; end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'find'
4
+
5
+ module Appydave
6
+ module Tools
7
+ module Dam
8
+ # File utility methods for DAM operations
9
+ # Provides reusable file and directory helpers
10
+ module FileHelper
11
+ module_function
12
+
13
+ # Calculate total size of directory in bytes
14
+ # @param path [String] Directory path
15
+ # @return [Integer] Size in bytes
16
+ def calculate_directory_size(path)
17
+ return 0 unless Dir.exist?(path)
18
+
19
+ total = 0
20
+ Find.find(path) do |file_path|
21
+ total += File.size(file_path) if File.file?(file_path)
22
+ rescue StandardError
23
+ # Skip files we can't read
24
+ end
25
+ total
26
+ end
27
+
28
+ # Format bytes into human-readable size
29
+ # @param bytes [Integer] Size in bytes
30
+ # @return [String] Formatted size (e.g., "1.5 GB")
31
+ def format_size(bytes)
32
+ return '0 B' if bytes.zero?
33
+
34
+ units = %w[B KB MB GB TB]
35
+ exp = (Math.log(bytes) / Math.log(1024)).to_i
36
+ exp = [exp, units.length - 1].min
37
+
38
+ format('%<size>.1f %<unit>s', size: bytes.to_f / (1024**exp), unit: units[exp])
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Dam
6
+ # Git operations helper for DAM classes
7
+ # Provides reusable git command wrappers
8
+ module GitHelper
9
+ module_function
10
+
11
+ # Get current branch name
12
+ # @param repo_path [String] Path to git repository
13
+ # @return [String] Branch name or 'unknown' if error
14
+ def current_branch(repo_path)
15
+ result = `git -C "#{repo_path}" rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
16
+ result.empty? ? 'unknown' : result
17
+ rescue StandardError
18
+ 'unknown'
19
+ end
20
+
21
+ # Get git remote URL
22
+ # @param repo_path [String] Path to git repository
23
+ # @return [String, nil] Remote URL or nil if not configured
24
+ def remote_url(repo_path)
25
+ result = `git -C "#{repo_path}" remote get-url origin 2>/dev/null`.strip
26
+ result.empty? ? nil : result
27
+ rescue StandardError
28
+ nil
29
+ end
30
+
31
+ # Count commits ahead of remote
32
+ # @param repo_path [String] Path to git repository
33
+ # @return [Integer] Number of commits ahead
34
+ def commits_ahead(repo_path)
35
+ `git -C "#{repo_path}" rev-list --count @{upstream}..HEAD 2>/dev/null`.strip.to_i
36
+ rescue StandardError
37
+ 0
38
+ end
39
+
40
+ # Count commits behind remote
41
+ # @param repo_path [String] Path to git repository
42
+ # @return [Integer] Number of commits behind
43
+ def commits_behind(repo_path)
44
+ `git -C "#{repo_path}" rev-list --count HEAD..@{upstream} 2>/dev/null`.strip.to_i
45
+ rescue StandardError
46
+ 0
47
+ end
48
+
49
+ # Count modified files
50
+ # @param repo_path [String] Path to git repository
51
+ # @return [Integer] Number of modified files
52
+ def modified_files_count(repo_path)
53
+ `git -C "#{repo_path}" status --porcelain 2>/dev/null | grep -E "^.M|^M" | wc -l`.strip.to_i
54
+ rescue StandardError
55
+ 0
56
+ end
57
+
58
+ # Count untracked files
59
+ # @param repo_path [String] Path to git repository
60
+ # @return [Integer] Number of untracked files
61
+ def untracked_files_count(repo_path)
62
+ `git -C "#{repo_path}" status --porcelain 2>/dev/null | grep -E "^\\?\\?" | wc -l`.strip.to_i
63
+ rescue StandardError
64
+ 0
65
+ end
66
+
67
+ # Check if repository has uncommitted changes
68
+ # @param repo_path [String] Path to git repository
69
+ # @return [Boolean] true if changes exist
70
+ def uncommitted_changes?(repo_path)
71
+ system("git -C \"#{repo_path}\" diff-index --quiet HEAD -- 2>/dev/null")
72
+ !$CHILD_STATUS.success?
73
+ rescue StandardError
74
+ false
75
+ end
76
+
77
+ # Fetch from remote
78
+ # @param repo_path [String] Path to git repository
79
+ # @return [Boolean] true if successful
80
+ def fetch(repo_path)
81
+ system("git -C \"#{repo_path}\" fetch 2>/dev/null")
82
+ $CHILD_STATUS.success?
83
+ rescue StandardError
84
+ false
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end