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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -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/brand_resolver.rb +118 -0
- data/lib/appydave/tools/dam/config.rb +2 -25
- 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/project_resolver.rb +2 -2
- 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 +4 -0
- data/package.json +1 -1
- metadata +8 -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,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
|
-
#
|
|
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
|
-
|
|
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
|