appydave-tools 0.18.5 → 0.20.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 +14 -0
- data/bin/dam +110 -0
- data/docs/dam/prd-git-integration.md +871 -0
- data/docs/dam/usage.md +138 -1
- data/lib/appydave/tools/configuration/models/brands_config.rb +4 -1
- data/lib/appydave/tools/dam/config.rb +38 -0
- data/lib/appydave/tools/dam/manifest_generator.rb +13 -0
- data/lib/appydave/tools/dam/repo_push.rb +131 -0
- data/lib/appydave/tools/dam/repo_status.rb +140 -0
- data/lib/appydave/tools/dam/repo_sync.rb +122 -0
- data/lib/appydave/tools/dam/status.rb +278 -0
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools.rb +4 -0
- data/package.json +1 -1
- metadata +6 -1
data/docs/dam/usage.md
CHANGED
|
@@ -154,6 +154,143 @@ dam list appydave 'b6*'
|
|
|
154
154
|
# Output: Lists b60, b61, b62...b69 projects
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
### Status & Monitoring
|
|
158
|
+
|
|
159
|
+
#### `dam status [brand] [project]`
|
|
160
|
+
Show unified status for project or brand (local, S3, SSD, git).
|
|
161
|
+
|
|
162
|
+
**Brand status:**
|
|
163
|
+
```bash
|
|
164
|
+
dam status appydave
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Output:**
|
|
168
|
+
```
|
|
169
|
+
📊 Brand Status: v-appydave
|
|
170
|
+
📡 Git Remote: git@github.com:klueless-io/v-appydave.git
|
|
171
|
+
|
|
172
|
+
🌿 Branch: main
|
|
173
|
+
📡 Remote: git@github.com:klueless-io/v-appydave.git
|
|
174
|
+
✓ Working directory clean
|
|
175
|
+
✓ Up to date with remote
|
|
176
|
+
|
|
177
|
+
📋 Manifest Summary:
|
|
178
|
+
Total projects: 114
|
|
179
|
+
Local: 74
|
|
180
|
+
S3 staging: 12
|
|
181
|
+
SSD backup: 67
|
|
182
|
+
|
|
183
|
+
Storyline projects: 3
|
|
184
|
+
FliVideo projects: 111
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Project status:**
|
|
188
|
+
```bash
|
|
189
|
+
dam status appydave b65
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Output:**
|
|
193
|
+
```
|
|
194
|
+
📊 Status: v-appydave/b65-guy-monroe-marketing-plan
|
|
195
|
+
|
|
196
|
+
Storage:
|
|
197
|
+
📁 Local: ✓ exists (flat structure)
|
|
198
|
+
Heavy files: no
|
|
199
|
+
Light files: yes
|
|
200
|
+
|
|
201
|
+
☁️ S3 Staging: ✓ exists
|
|
202
|
+
Local staging files: 3
|
|
203
|
+
|
|
204
|
+
💾 SSD Backup: ✓ exists
|
|
205
|
+
Path: b65-guy-monroe-marketing-plan
|
|
206
|
+
|
|
207
|
+
Git:
|
|
208
|
+
🌿 Branch: main
|
|
209
|
+
📡 Remote: git@github.com:klueless-io/v-appydave.git
|
|
210
|
+
↕️ Status: Clean working directory
|
|
211
|
+
🔄 Sync: Up to date
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Auto-detect from PWD:**
|
|
215
|
+
```bash
|
|
216
|
+
cd ~/dev/video-projects/v-appydave/b65-project
|
|
217
|
+
dam status
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Git Repository Commands
|
|
221
|
+
|
|
222
|
+
#### `dam repo-status [brand] [--all]`
|
|
223
|
+
Check git status for brand repositories.
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Single brand
|
|
227
|
+
dam repo-status appydave
|
|
228
|
+
|
|
229
|
+
# All brands
|
|
230
|
+
dam repo-status --all
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Output:**
|
|
234
|
+
```
|
|
235
|
+
🔍 Git Status: v-appydave
|
|
236
|
+
|
|
237
|
+
🌿 Branch: main
|
|
238
|
+
📡 Remote: git@github.com:klueless-io/v-appydave.git
|
|
239
|
+
✓ Working directory clean
|
|
240
|
+
✓ Up to date with remote
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
#### `dam repo-sync [brand] [--all]`
|
|
244
|
+
Pull updates for brand repositories.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# Single brand
|
|
248
|
+
dam repo-sync appydave
|
|
249
|
+
|
|
250
|
+
# All brands
|
|
251
|
+
dam repo-sync --all
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Output:**
|
|
255
|
+
```
|
|
256
|
+
🔄 Syncing: v-appydave
|
|
257
|
+
|
|
258
|
+
✓ Already up to date
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Safety features:**
|
|
262
|
+
- Skips brands with uncommitted changes (prevents conflicts)
|
|
263
|
+
- Shows summary when syncing multiple brands
|
|
264
|
+
|
|
265
|
+
#### `dam repo-push [brand] [project]`
|
|
266
|
+
Push changes for brand repository.
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# Push all changes
|
|
270
|
+
dam repo-push appydave
|
|
271
|
+
|
|
272
|
+
# Validate project exists before push
|
|
273
|
+
dam repo-push appydave b65
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Output:**
|
|
277
|
+
```
|
|
278
|
+
📤 Pushing: v-appydave
|
|
279
|
+
|
|
280
|
+
✓ Project validated: b65-guy-monroe-marketing-plan
|
|
281
|
+
|
|
282
|
+
📤 Pushing 2 commit(s)...
|
|
283
|
+
|
|
284
|
+
✓ Push successful
|
|
285
|
+
|
|
286
|
+
main -> main
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Safety features:**
|
|
290
|
+
- Warns if uncommitted changes detected
|
|
291
|
+
- Optional project validation against manifest
|
|
292
|
+
- Shows push summary
|
|
293
|
+
|
|
157
294
|
### S3 Sync Commands
|
|
158
295
|
|
|
159
296
|
#### `dam s3-up [brand] [project] [--dry-run]`
|
|
@@ -649,4 +786,4 @@ dam s3-up appydave b65
|
|
|
649
786
|
|
|
650
787
|
---
|
|
651
788
|
|
|
652
|
-
**Last Updated:** 2025-11-
|
|
789
|
+
**Last Updated:** 2025-11-10
|
|
@@ -117,6 +117,7 @@ module Appydave
|
|
|
117
117
|
'type' => 'owned',
|
|
118
118
|
'youtube_channels' => [],
|
|
119
119
|
'team' => [],
|
|
120
|
+
'git_remote' => nil,
|
|
120
121
|
'locations' => {
|
|
121
122
|
'video_projects' => '',
|
|
122
123
|
'ssd_backup' => ''
|
|
@@ -144,7 +145,7 @@ module Appydave
|
|
|
144
145
|
|
|
145
146
|
# Type-safe class to access brand properties
|
|
146
147
|
class BrandInfo
|
|
147
|
-
attr_accessor :key, :name, :shortcut, :type, :youtube_channels, :team, :locations, :aws, :settings
|
|
148
|
+
attr_accessor :key, :name, :shortcut, :type, :youtube_channels, :team, :git_remote, :locations, :aws, :settings
|
|
148
149
|
|
|
149
150
|
def initialize(key, data)
|
|
150
151
|
@key = key
|
|
@@ -153,6 +154,7 @@ module Appydave
|
|
|
153
154
|
@type = data['type'] || 'owned'
|
|
154
155
|
@youtube_channels = data['youtube_channels'] || []
|
|
155
156
|
@team = data['team'] || []
|
|
157
|
+
@git_remote = data['git_remote']
|
|
156
158
|
@locations = BrandLocation.new(data['locations'] || {})
|
|
157
159
|
@aws = BrandAws.new(data['aws'] || {})
|
|
158
160
|
@settings = BrandSettings.new(data['settings'] || {})
|
|
@@ -165,6 +167,7 @@ module Appydave
|
|
|
165
167
|
'type' => @type,
|
|
166
168
|
'youtube_channels' => @youtube_channels,
|
|
167
169
|
'team' => @team,
|
|
170
|
+
'git_remote' => @git_remote,
|
|
168
171
|
'locations' => @locations.to_h,
|
|
169
172
|
'aws' => @aws.to_h,
|
|
170
173
|
'settings' => @settings.to_h
|
|
@@ -43,6 +43,31 @@ module Appydave
|
|
|
43
43
|
path
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# Get git remote URL for a brand (with self-healing)
|
|
47
|
+
# @param brand_key [String] Brand key (e.g., 'appydave', 'voz')
|
|
48
|
+
# @return [String, nil] Git remote URL or nil if not a git repo
|
|
49
|
+
def git_remote(brand_key)
|
|
50
|
+
Appydave::Tools::Configuration::Config.configure
|
|
51
|
+
brands_config = Appydave::Tools::Configuration::Config.brands
|
|
52
|
+
brand_info = brands_config.get_brand(brand_key)
|
|
53
|
+
|
|
54
|
+
# 1. Check if git_remote is already configured
|
|
55
|
+
return brand_info.git_remote if brand_info.git_remote && !brand_info.git_remote.empty?
|
|
56
|
+
|
|
57
|
+
# 2. Try to infer from git command
|
|
58
|
+
brand_path_dir = brand_path(brand_key)
|
|
59
|
+
inferred_remote = infer_git_remote(brand_path_dir)
|
|
60
|
+
|
|
61
|
+
# 3. Auto-save if inferred successfully
|
|
62
|
+
if inferred_remote
|
|
63
|
+
brand_info.git_remote = inferred_remote
|
|
64
|
+
brands_config.set_brand(brand_info.key, brand_info)
|
|
65
|
+
brands_config.save
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
inferred_remote
|
|
69
|
+
end
|
|
70
|
+
|
|
46
71
|
# Expand brand shortcut to full brand name
|
|
47
72
|
# Reads from brands.json if available, falls back to hardcoded shortcuts
|
|
48
73
|
# @param shortcut [String] Brand shortcut (e.g., 'appydave', 'ad', 'APPYDAVE')
|
|
@@ -165,6 +190,19 @@ module Appydave
|
|
|
165
190
|
config.save
|
|
166
191
|
ERROR
|
|
167
192
|
end
|
|
193
|
+
|
|
194
|
+
# Infer git remote URL from git repository
|
|
195
|
+
# @param path [String] Path to git repository
|
|
196
|
+
# @return [String, nil] Remote URL or nil if not a git repo
|
|
197
|
+
def infer_git_remote(path)
|
|
198
|
+
return nil unless Dir.exist?(path)
|
|
199
|
+
|
|
200
|
+
# Try to get git remote URL
|
|
201
|
+
result = `git -C "#{path}" remote get-url origin 2>/dev/null`.strip
|
|
202
|
+
result.empty? ? nil : result
|
|
203
|
+
rescue StandardError
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
168
206
|
end
|
|
169
207
|
end
|
|
170
208
|
end
|
|
@@ -157,6 +157,14 @@ module Appydave
|
|
|
157
157
|
'archived'
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
+
# Check S3 staging (only if local exists)
|
|
161
|
+
s3_staging_path = File.join(local_path, 's3-staging')
|
|
162
|
+
s3_exists = local_exists && Dir.exist?(s3_staging_path)
|
|
163
|
+
|
|
164
|
+
# Check for storyline.json
|
|
165
|
+
storyline_json_path = File.join(local_path, 'data', 'storyline.json')
|
|
166
|
+
has_storyline_json = local_exists && File.exist?(storyline_json_path)
|
|
167
|
+
|
|
160
168
|
# Check SSD (try both flat and range-based structures)
|
|
161
169
|
ssd_exists = if ssd_available
|
|
162
170
|
flat_ssd_path = File.join(ssd_backup, project_id)
|
|
@@ -168,11 +176,16 @@ module Appydave
|
|
|
168
176
|
|
|
169
177
|
{
|
|
170
178
|
id: project_id,
|
|
179
|
+
type: has_storyline_json ? 'storyline-app' : 'flivideo',
|
|
180
|
+
hasStorylineJson: has_storyline_json,
|
|
171
181
|
storage: {
|
|
172
182
|
ssd: {
|
|
173
183
|
exists: ssd_exists,
|
|
174
184
|
path: ssd_exists ? project_id : nil
|
|
175
185
|
},
|
|
186
|
+
s3: {
|
|
187
|
+
exists: s3_exists
|
|
188
|
+
},
|
|
176
189
|
local: {
|
|
177
190
|
exists: local_exists,
|
|
178
191
|
structure: structure,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appydave
|
|
4
|
+
module Tools
|
|
5
|
+
module Dam
|
|
6
|
+
# Push brand repository changes
|
|
7
|
+
class RepoPush
|
|
8
|
+
attr_reader :brand, :project_id, :brand_info, :brand_path
|
|
9
|
+
|
|
10
|
+
def initialize(brand, project_id = nil)
|
|
11
|
+
@brand_info = load_brand_info(brand)
|
|
12
|
+
@brand = @brand_info.key
|
|
13
|
+
@brand_path = Config.brand_path(@brand)
|
|
14
|
+
@project_id = project_id
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Push changes
|
|
18
|
+
def push
|
|
19
|
+
puts "📤 Pushing: v-#{brand}"
|
|
20
|
+
puts ''
|
|
21
|
+
|
|
22
|
+
unless git_repo?
|
|
23
|
+
puts "❌ Not a git repository: #{brand_path}"
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# If project specified, validate it exists in manifest
|
|
28
|
+
validate_project if project_id
|
|
29
|
+
|
|
30
|
+
perform_push
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def load_brand_info(brand)
|
|
36
|
+
Appydave::Tools::Configuration::Config.configure
|
|
37
|
+
Appydave::Tools::Configuration::Config.brands.get_brand(brand)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def git_repo?
|
|
41
|
+
git_dir = File.join(brand_path, '.git')
|
|
42
|
+
Dir.exist?(git_dir)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_project
|
|
46
|
+
manifest = load_manifest
|
|
47
|
+
unless manifest
|
|
48
|
+
puts '⚠️ Warning: Manifest not found'
|
|
49
|
+
puts ' Continuing with push (manifest is optional for validation)'
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Resolve short name if needed (b65 -> b65-full-name)
|
|
54
|
+
resolved = ProjectResolver.new.resolve(brand, project_id)
|
|
55
|
+
|
|
56
|
+
project_entry = manifest[:projects].find { |p| p[:id] == resolved }
|
|
57
|
+
if project_entry
|
|
58
|
+
puts "✓ Project validated: #{resolved}"
|
|
59
|
+
else
|
|
60
|
+
puts "⚠️ Warning: Project '#{resolved}' not found in manifest"
|
|
61
|
+
puts ' Continuing with push (manifest may be outdated)'
|
|
62
|
+
end
|
|
63
|
+
puts ''
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load_manifest
|
|
67
|
+
manifest_path = File.join(brand_path, 'projects.json')
|
|
68
|
+
return nil unless File.exist?(manifest_path)
|
|
69
|
+
|
|
70
|
+
JSON.parse(File.read(manifest_path), symbolize_names: true)
|
|
71
|
+
rescue JSON::ParserError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def perform_push
|
|
76
|
+
# Check for uncommitted changes
|
|
77
|
+
if uncommitted_changes?
|
|
78
|
+
puts '⚠️ Warning: Uncommitted changes detected'
|
|
79
|
+
puts ''
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check if ahead of remote
|
|
83
|
+
ahead = commits_ahead
|
|
84
|
+
if ahead.zero?
|
|
85
|
+
puts '✓ Nothing to push (up to date with remote)'
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
puts "📤 Pushing #{ahead} commit(s)..."
|
|
90
|
+
puts ''
|
|
91
|
+
|
|
92
|
+
# Perform git push
|
|
93
|
+
output = `git -C "#{brand_path}" push 2>&1`
|
|
94
|
+
exit_code = $CHILD_STATUS.exitstatus
|
|
95
|
+
|
|
96
|
+
if exit_code.zero?
|
|
97
|
+
puts '✓ Push successful'
|
|
98
|
+
puts ''
|
|
99
|
+
show_push_summary(output)
|
|
100
|
+
else
|
|
101
|
+
puts '❌ Push failed:'
|
|
102
|
+
puts output
|
|
103
|
+
exit 1
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def uncommitted_changes?
|
|
108
|
+
output = `git -C "#{brand_path}" status --porcelain 2>/dev/null`.strip
|
|
109
|
+
!output.empty?
|
|
110
|
+
rescue StandardError
|
|
111
|
+
false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def commits_ahead
|
|
115
|
+
`git -C "#{brand_path}" rev-list --count @{upstream}..HEAD 2>/dev/null`.strip.to_i
|
|
116
|
+
rescue StandardError
|
|
117
|
+
0
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def show_push_summary(output)
|
|
121
|
+
# Extract useful info from git push output
|
|
122
|
+
lines = output.lines.map(&:strip).reject(&:empty?)
|
|
123
|
+
|
|
124
|
+
lines.each do |line|
|
|
125
|
+
puts " #{line}" if line.include?('->') || line.include?('branch')
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appydave
|
|
4
|
+
module Tools
|
|
5
|
+
module Dam
|
|
6
|
+
# Show git status for brand repositories
|
|
7
|
+
class RepoStatus
|
|
8
|
+
attr_reader :brand, :brand_info, :brand_path
|
|
9
|
+
|
|
10
|
+
def initialize(brand = nil)
|
|
11
|
+
return unless brand
|
|
12
|
+
|
|
13
|
+
@brand_info = load_brand_info(brand)
|
|
14
|
+
@brand = @brand_info.key
|
|
15
|
+
@brand_path = Config.brand_path(@brand)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Show status for single brand
|
|
19
|
+
def show
|
|
20
|
+
puts "🔍 Git Status: v-#{brand}"
|
|
21
|
+
puts ''
|
|
22
|
+
|
|
23
|
+
unless git_repo?
|
|
24
|
+
puts "❌ Not a git repository: #{brand_path}"
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
show_git_info
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Show status for all brands
|
|
32
|
+
def show_all
|
|
33
|
+
puts '🔍 Git Status - All Brands'
|
|
34
|
+
puts ''
|
|
35
|
+
|
|
36
|
+
Appydave::Tools::Configuration::Config.configure
|
|
37
|
+
brands_config = Appydave::Tools::Configuration::Config.brands
|
|
38
|
+
|
|
39
|
+
brands_config.brands.each do |brand_info|
|
|
40
|
+
@brand_info = brand_info
|
|
41
|
+
@brand = brand_info.key
|
|
42
|
+
@brand_path = Config.brand_path(@brand)
|
|
43
|
+
|
|
44
|
+
next unless git_repo?
|
|
45
|
+
|
|
46
|
+
puts "📦 v-#{brand}"
|
|
47
|
+
show_git_info(indent: ' ')
|
|
48
|
+
puts ''
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def load_brand_info(brand)
|
|
55
|
+
Appydave::Tools::Configuration::Config.configure
|
|
56
|
+
Appydave::Tools::Configuration::Config.brands.get_brand(brand)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def git_repo?
|
|
60
|
+
git_dir = File.join(brand_path, '.git')
|
|
61
|
+
Dir.exist?(git_dir)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def show_git_info(indent: '')
|
|
65
|
+
status = git_status_info
|
|
66
|
+
|
|
67
|
+
puts "#{indent}🌿 Branch: #{status[:branch]}"
|
|
68
|
+
puts "#{indent}📡 Remote: #{status[:remote]}" if status[:remote]
|
|
69
|
+
|
|
70
|
+
if status[:modified_count].positive? || status[:untracked_count].positive?
|
|
71
|
+
puts "#{indent}↕️ Changes: #{status[:modified_count]} modified, #{status[:untracked_count]} untracked"
|
|
72
|
+
else
|
|
73
|
+
puts "#{indent}✓ Working directory clean"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if status[:ahead].positive? || status[:behind].positive?
|
|
77
|
+
puts "#{indent}🔄 Sync: #{sync_status_text(status[:ahead], status[:behind])}"
|
|
78
|
+
else
|
|
79
|
+
puts "#{indent}✓ Up to date with remote"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def sync_status_text(ahead, behind)
|
|
84
|
+
parts = []
|
|
85
|
+
parts << "#{ahead} ahead" if ahead.positive?
|
|
86
|
+
parts << "#{behind} behind" if behind.positive?
|
|
87
|
+
parts.join(', ')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def git_status_info
|
|
91
|
+
{
|
|
92
|
+
branch: current_branch,
|
|
93
|
+
remote: remote_url,
|
|
94
|
+
modified_count: modified_files_count,
|
|
95
|
+
untracked_count: untracked_files_count,
|
|
96
|
+
ahead: commits_ahead,
|
|
97
|
+
behind: commits_behind
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def current_branch
|
|
102
|
+
`git -C "#{brand_path}" rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
103
|
+
rescue StandardError
|
|
104
|
+
'unknown'
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def remote_url
|
|
108
|
+
result = `git -C "#{brand_path}" remote get-url origin 2>/dev/null`.strip
|
|
109
|
+
result.empty? ? nil : result
|
|
110
|
+
rescue StandardError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def modified_files_count
|
|
115
|
+
`git -C "#{brand_path}" status --porcelain 2>/dev/null | grep -E "^.M|^M" | wc -l`.strip.to_i
|
|
116
|
+
rescue StandardError
|
|
117
|
+
0
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def untracked_files_count
|
|
121
|
+
`git -C "#{brand_path}" status --porcelain 2>/dev/null | grep -E "^\\?\\?" | wc -l`.strip.to_i
|
|
122
|
+
rescue StandardError
|
|
123
|
+
0
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def commits_ahead
|
|
127
|
+
`git -C "#{brand_path}" rev-list --count @{upstream}..HEAD 2>/dev/null`.strip.to_i
|
|
128
|
+
rescue StandardError
|
|
129
|
+
0
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def commits_behind
|
|
133
|
+
`git -C "#{brand_path}" rev-list --count HEAD..@{upstream} 2>/dev/null`.strip.to_i
|
|
134
|
+
rescue StandardError
|
|
135
|
+
0
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appydave
|
|
4
|
+
module Tools
|
|
5
|
+
module Dam
|
|
6
|
+
# Sync (git pull) brand repositories
|
|
7
|
+
class RepoSync
|
|
8
|
+
attr_reader :brand, :brand_info, :brand_path
|
|
9
|
+
|
|
10
|
+
def initialize(brand = nil)
|
|
11
|
+
return unless brand
|
|
12
|
+
|
|
13
|
+
@brand_info = load_brand_info(brand)
|
|
14
|
+
@brand = @brand_info.key
|
|
15
|
+
@brand_path = Config.brand_path(@brand)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Sync single brand
|
|
19
|
+
def sync
|
|
20
|
+
puts "🔄 Syncing: v-#{brand}"
|
|
21
|
+
puts ''
|
|
22
|
+
|
|
23
|
+
unless git_repo?
|
|
24
|
+
puts "❌ Not a git repository: #{brand_path}"
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
perform_sync
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Sync all brands
|
|
32
|
+
def sync_all
|
|
33
|
+
puts '🔄 Syncing All Brands'
|
|
34
|
+
puts ''
|
|
35
|
+
|
|
36
|
+
Appydave::Tools::Configuration::Config.configure
|
|
37
|
+
brands_config = Appydave::Tools::Configuration::Config.brands
|
|
38
|
+
|
|
39
|
+
results = []
|
|
40
|
+
|
|
41
|
+
brands_config.brands.each do |brand_info|
|
|
42
|
+
@brand_info = brand_info
|
|
43
|
+
@brand = brand_info.key
|
|
44
|
+
@brand_path = Config.brand_path(@brand)
|
|
45
|
+
|
|
46
|
+
next unless git_repo?
|
|
47
|
+
|
|
48
|
+
puts "📦 v-#{brand}"
|
|
49
|
+
result = perform_sync(indent: ' ')
|
|
50
|
+
results << result
|
|
51
|
+
puts ''
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
show_summary(results)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def load_brand_info(brand)
|
|
60
|
+
Appydave::Tools::Configuration::Config.configure
|
|
61
|
+
Appydave::Tools::Configuration::Config.brands.get_brand(brand)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def git_repo?
|
|
65
|
+
git_dir = File.join(brand_path, '.git')
|
|
66
|
+
Dir.exist?(git_dir)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def perform_sync(indent: '')
|
|
70
|
+
# Check for uncommitted changes
|
|
71
|
+
if uncommitted_changes?
|
|
72
|
+
puts "#{indent}⚠️ Uncommitted changes detected"
|
|
73
|
+
puts "#{indent} Skipping pull (would cause conflicts)"
|
|
74
|
+
return { brand: brand, status: 'skipped', reason: 'uncommitted changes' }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Perform git pull
|
|
78
|
+
output = `git -C "#{brand_path}" pull 2>&1`
|
|
79
|
+
exit_code = $CHILD_STATUS.exitstatus
|
|
80
|
+
|
|
81
|
+
if exit_code.zero?
|
|
82
|
+
if output.include?('Already up to date')
|
|
83
|
+
puts "#{indent}✓ Already up to date"
|
|
84
|
+
{ brand: brand, status: 'current' }
|
|
85
|
+
else
|
|
86
|
+
puts "#{indent}✓ Updated successfully"
|
|
87
|
+
puts "#{indent} #{output.lines.first.strip}" if output.lines.any?
|
|
88
|
+
{ brand: brand, status: 'updated' }
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
puts "#{indent}❌ Pull failed: #{output.strip}"
|
|
92
|
+
{ brand: brand, status: 'error', reason: output.strip }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def uncommitted_changes?
|
|
97
|
+
output = `git -C "#{brand_path}" status --porcelain 2>/dev/null`.strip
|
|
98
|
+
!output.empty?
|
|
99
|
+
rescue StandardError
|
|
100
|
+
false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def show_summary(results)
|
|
104
|
+
puts '=' * 60
|
|
105
|
+
puts '📊 Sync Summary:'
|
|
106
|
+
puts ''
|
|
107
|
+
|
|
108
|
+
updated = results.count { |r| r[:status] == 'updated' }
|
|
109
|
+
current = results.count { |r| r[:status] == 'current' }
|
|
110
|
+
skipped = results.count { |r| r[:status] == 'skipped' }
|
|
111
|
+
errors = results.count { |r| r[:status] == 'error' }
|
|
112
|
+
|
|
113
|
+
puts " Total repos checked: #{results.size}"
|
|
114
|
+
puts " Updated: #{updated}"
|
|
115
|
+
puts " Already current: #{current}"
|
|
116
|
+
puts " Skipped: #{skipped}" if skipped.positive?
|
|
117
|
+
puts " Errors: #{errors}" if errors.positive?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|