appydave-tools 0.37.0 → 0.38.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/docs/code-quality/README.md +177 -0
- data/docs/code-quality/implementation-plan.md +712 -0
- data/docs/code-quality/report-2025-01-21.md +342 -0
- data/lib/appydave/tools/dam/errors.rb +31 -0
- data/lib/appydave/tools/dam/file_helper.rb +43 -0
- data/lib/appydave/tools/dam/git_helper.rb +89 -0
- data/lib/appydave/tools/dam/project_listing.rb +4 -25
- data/lib/appydave/tools/dam/repo_push.rb +2 -7
- data/lib/appydave/tools/dam/repo_status.rb +7 -24
- data/lib/appydave/tools/dam/s3_operations.rb +1 -5
- data/lib/appydave/tools/dam/status.rb +6 -19
- data/lib/appydave/tools/dam/sync_from_ssd.rb +1 -7
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools.rb +3 -0
- data/package.json +1 -1
- metadata +7 -1
|
@@ -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,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
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'find'
|
|
4
|
-
|
|
5
3
|
# rubocop:disable Style/FormatStringToken
|
|
6
4
|
# Disabled: Using simple unannotated tokens (%s) for straightforward string formatting
|
|
7
5
|
# Annotated tokens (%<foo>s) add unnecessary complexity for simple table formatting
|
|
@@ -69,7 +67,7 @@ module Appydave
|
|
|
69
67
|
# Gather project data
|
|
70
68
|
project_data = projects.map do |project|
|
|
71
69
|
project_path = Config.project_path(brand_arg, project)
|
|
72
|
-
size = calculate_directory_size(project_path)
|
|
70
|
+
size = FileHelper.calculate_directory_size(project_path)
|
|
73
71
|
modified = File.mtime(project_path)
|
|
74
72
|
|
|
75
73
|
{
|
|
@@ -113,7 +111,7 @@ module Appydave
|
|
|
113
111
|
# Gather project data
|
|
114
112
|
project_data = matches.map do |project|
|
|
115
113
|
project_path = Config.project_path(brand_arg, project)
|
|
116
|
-
size = calculate_directory_size(project_path)
|
|
114
|
+
size = FileHelper.calculate_directory_size(project_path)
|
|
117
115
|
modified = File.mtime(project_path)
|
|
118
116
|
|
|
119
117
|
{
|
|
@@ -146,23 +144,10 @@ module Appydave
|
|
|
146
144
|
# Calculate total size of all projects in a brand
|
|
147
145
|
def self.calculate_total_size(brand, projects)
|
|
148
146
|
projects.sum do |project|
|
|
149
|
-
calculate_directory_size(Config.project_path(brand, project))
|
|
147
|
+
FileHelper.calculate_directory_size(Config.project_path(brand, project))
|
|
150
148
|
end
|
|
151
149
|
end
|
|
152
150
|
|
|
153
|
-
# Calculate size of a directory in bytes
|
|
154
|
-
def self.calculate_directory_size(path)
|
|
155
|
-
return 0 unless Dir.exist?(path)
|
|
156
|
-
|
|
157
|
-
total = 0
|
|
158
|
-
Find.find(path) do |file_path|
|
|
159
|
-
total += File.size(file_path) if File.file?(file_path)
|
|
160
|
-
rescue StandardError
|
|
161
|
-
# Skip files we can't read
|
|
162
|
-
end
|
|
163
|
-
total
|
|
164
|
-
end
|
|
165
|
-
|
|
166
151
|
# Find the most recent modification time across all projects
|
|
167
152
|
def self.find_last_modified(brand, projects)
|
|
168
153
|
return Time.at(0) if projects.empty?
|
|
@@ -174,13 +159,7 @@ module Appydave
|
|
|
174
159
|
|
|
175
160
|
# Format size in human-readable format
|
|
176
161
|
def self.format_size(bytes)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
units = %w[B KB MB GB TB]
|
|
180
|
-
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
181
|
-
exp = [exp, units.length - 1].min
|
|
182
|
-
|
|
183
|
-
format('%.1f %s', bytes.to_f / (1024**exp), units[exp])
|
|
162
|
+
FileHelper.format_size(bytes)
|
|
184
163
|
end
|
|
185
164
|
|
|
186
165
|
# Format date/time in readable format
|
|
@@ -105,16 +105,11 @@ module Appydave
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
def uncommitted_changes?
|
|
108
|
-
|
|
109
|
-
!output.empty?
|
|
110
|
-
rescue StandardError
|
|
111
|
-
false
|
|
108
|
+
GitHelper.uncommitted_changes?(brand_path)
|
|
112
109
|
end
|
|
113
110
|
|
|
114
111
|
def commits_ahead
|
|
115
|
-
|
|
116
|
-
rescue StandardError
|
|
117
|
-
0
|
|
112
|
+
GitHelper.commits_ahead(brand_path)
|
|
118
113
|
end
|
|
119
114
|
|
|
120
115
|
def show_push_summary(output)
|
|
@@ -107,49 +107,32 @@ module Appydave
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
def current_branch
|
|
110
|
-
|
|
111
|
-
rescue StandardError
|
|
112
|
-
'unknown'
|
|
110
|
+
GitHelper.current_branch(brand_path)
|
|
113
111
|
end
|
|
114
112
|
|
|
115
113
|
def remote_url
|
|
116
|
-
|
|
117
|
-
result.empty? ? nil : result
|
|
118
|
-
rescue StandardError
|
|
119
|
-
nil
|
|
114
|
+
GitHelper.remote_url(brand_path)
|
|
120
115
|
end
|
|
121
116
|
|
|
122
117
|
def modified_files_count
|
|
123
|
-
|
|
124
|
-
rescue StandardError
|
|
125
|
-
0
|
|
118
|
+
GitHelper.modified_files_count(brand_path)
|
|
126
119
|
end
|
|
127
120
|
|
|
128
121
|
def untracked_files_count
|
|
129
|
-
|
|
130
|
-
rescue StandardError
|
|
131
|
-
0
|
|
122
|
+
GitHelper.untracked_files_count(brand_path)
|
|
132
123
|
end
|
|
133
124
|
|
|
134
125
|
def commits_ahead
|
|
135
|
-
|
|
136
|
-
rescue StandardError
|
|
137
|
-
0
|
|
126
|
+
GitHelper.commits_ahead(brand_path)
|
|
138
127
|
end
|
|
139
128
|
|
|
140
129
|
def commits_behind
|
|
141
|
-
|
|
142
|
-
rescue StandardError
|
|
143
|
-
0
|
|
130
|
+
GitHelper.commits_behind(brand_path)
|
|
144
131
|
end
|
|
145
132
|
|
|
146
133
|
# Check if repo has uncommitted changes (matches old script: git diff-index --quiet HEAD --)
|
|
147
134
|
def uncommitted_changes?
|
|
148
|
-
|
|
149
|
-
system("git -C \"#{brand_path}\" diff-index --quiet HEAD -- 2>/dev/null")
|
|
150
|
-
!$CHILD_STATUS.success?
|
|
151
|
-
rescue StandardError
|
|
152
|
-
false
|
|
135
|
+
GitHelper.uncommitted_changes?(brand_path)
|
|
153
136
|
end
|
|
154
137
|
|
|
155
138
|
# Show file list using git status --short (matches old script)
|
|
@@ -768,11 +768,7 @@ module Appydave
|
|
|
768
768
|
|
|
769
769
|
# Calculate total size of a directory
|
|
770
770
|
def calculate_directory_size(dir_path)
|
|
771
|
-
|
|
772
|
-
Dir.glob(File.join(dir_path, '**', '*'), File::FNM_DOTMATCH).each do |file|
|
|
773
|
-
total += File.size(file) if File.file?(file)
|
|
774
|
-
end
|
|
775
|
-
total
|
|
771
|
+
FileHelper.calculate_directory_size(dir_path)
|
|
776
772
|
end
|
|
777
773
|
end
|
|
778
774
|
end
|