carson 3.24.0 → 3.27.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/API.md +26 -8
- data/MANUAL.md +51 -22
- data/README.md +9 -16
- data/RELEASE.md +16 -1
- data/VERSION +1 -1
- data/carson.gemspec +0 -1
- data/hooks/command-guard +60 -16
- data/lib/carson/cli.rb +116 -5
- data/lib/carson/config.rb +3 -8
- data/lib/carson/delivery.rb +17 -9
- data/lib/carson/ledger.rb +242 -222
- data/lib/carson/repository.rb +2 -4
- data/lib/carson/revision.rb +2 -4
- data/lib/carson/runtime/abandon.rb +238 -0
- data/lib/carson/runtime/audit.rb +12 -2
- data/lib/carson/runtime/deliver.rb +162 -15
- data/lib/carson/runtime/govern.rb +48 -21
- data/lib/carson/runtime/housekeep.rb +189 -153
- data/lib/carson/runtime/local/onboard.rb +4 -3
- data/lib/carson/runtime/local/prune.rb +6 -11
- data/lib/carson/runtime/local/sync.rb +9 -0
- data/lib/carson/runtime/local/worktree.rb +166 -0
- data/lib/carson/runtime/recover.rb +418 -0
- data/lib/carson/runtime/setup.rb +11 -7
- data/lib/carson/runtime/status.rb +39 -28
- data/lib/carson/runtime.rb +3 -1
- data/lib/carson/worktree.rb +128 -53
- metadata +4 -22
data/lib/carson/ledger.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# JSON file-backed ledger for Carson's deliveries and revisions.
|
|
2
2
|
require "fileutils"
|
|
3
|
-
require "
|
|
3
|
+
require "json"
|
|
4
4
|
require "time"
|
|
5
5
|
|
|
6
6
|
module Carson
|
|
@@ -10,140 +10,93 @@ module Carson
|
|
|
10
10
|
|
|
11
11
|
def initialize( path: )
|
|
12
12
|
@path = File.expand_path( path )
|
|
13
|
-
|
|
13
|
+
FileUtils.mkdir_p( File.dirname( @path ) )
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
attr_reader :path
|
|
17
17
|
|
|
18
|
-
# Ensures the SQLite schema exists before Carson uses the ledger.
|
|
19
|
-
def prepare!
|
|
20
|
-
FileUtils.mkdir_p( File.dirname( path ) )
|
|
21
|
-
|
|
22
|
-
with_database do |database|
|
|
23
|
-
database.execute_batch( <<~SQL )
|
|
24
|
-
CREATE TABLE IF NOT EXISTS deliveries (
|
|
25
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
-
repo_path TEXT NOT NULL,
|
|
27
|
-
branch_name TEXT NOT NULL,
|
|
28
|
-
head TEXT NOT NULL,
|
|
29
|
-
worktree_path TEXT,
|
|
30
|
-
authority TEXT NOT NULL,
|
|
31
|
-
status TEXT NOT NULL,
|
|
32
|
-
pr_number INTEGER,
|
|
33
|
-
pr_url TEXT,
|
|
34
|
-
revision_count INTEGER NOT NULL DEFAULT 0,
|
|
35
|
-
cause TEXT,
|
|
36
|
-
summary TEXT,
|
|
37
|
-
created_at TEXT NOT NULL,
|
|
38
|
-
updated_at TEXT NOT NULL,
|
|
39
|
-
integrated_at TEXT,
|
|
40
|
-
superseded_at TEXT
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
CREATE UNIQUE INDEX IF NOT EXISTS index_deliveries_on_identity
|
|
44
|
-
ON deliveries ( repo_path, branch_name, head );
|
|
45
|
-
|
|
46
|
-
CREATE INDEX IF NOT EXISTS index_deliveries_on_state
|
|
47
|
-
ON deliveries ( repo_path, status, created_at );
|
|
48
|
-
|
|
49
|
-
CREATE TABLE IF NOT EXISTS revisions (
|
|
50
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
51
|
-
delivery_id INTEGER NOT NULL,
|
|
52
|
-
number INTEGER NOT NULL,
|
|
53
|
-
cause TEXT NOT NULL,
|
|
54
|
-
provider TEXT NOT NULL,
|
|
55
|
-
status TEXT NOT NULL,
|
|
56
|
-
started_at TEXT NOT NULL,
|
|
57
|
-
finished_at TEXT,
|
|
58
|
-
summary TEXT,
|
|
59
|
-
FOREIGN KEY ( delivery_id ) REFERENCES deliveries ( id )
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
CREATE UNIQUE INDEX IF NOT EXISTS index_revisions_on_delivery_number
|
|
63
|
-
ON revisions ( delivery_id, number );
|
|
64
|
-
SQL
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
18
|
# Creates or refreshes a delivery for the same branch head.
|
|
69
|
-
def upsert_delivery( repository:, branch_name:, head:, worktree_path:,
|
|
19
|
+
def upsert_delivery( repository:, branch_name:, head:, worktree_path:, pr_number:, pr_url:, status:, summary:, cause: )
|
|
70
20
|
timestamp = now_utc
|
|
71
21
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
[ worktree_path, authority, status, pr_number, pr_url, cause, summary, timestamp, row.fetch( "id" ) ]
|
|
87
|
-
)
|
|
88
|
-
return fetch_delivery( database: database, id: row.fetch( "id" ), repository: repository )
|
|
22
|
+
with_state do |state|
|
|
23
|
+
key = delivery_key( repo_path: repository.path, branch_name: branch_name, head: head )
|
|
24
|
+
existing = state[ "deliveries" ][ key ]
|
|
25
|
+
|
|
26
|
+
if existing
|
|
27
|
+
existing[ "repo_path" ] = repository.path
|
|
28
|
+
existing[ "worktree_path" ] = worktree_path
|
|
29
|
+
existing[ "status" ] = status
|
|
30
|
+
existing[ "pr_number" ] = pr_number
|
|
31
|
+
existing[ "pr_url" ] = pr_url
|
|
32
|
+
existing[ "cause" ] = cause
|
|
33
|
+
existing[ "summary" ] = summary
|
|
34
|
+
existing[ "updated_at" ] = timestamp
|
|
35
|
+
return build_delivery( key: key, data: existing, repository: repository )
|
|
89
36
|
end
|
|
90
37
|
|
|
91
|
-
supersede_branch!(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
38
|
+
supersede_branch!( state: state, repo_path: repository.path, branch_name: branch_name, timestamp: timestamp )
|
|
39
|
+
state[ "deliveries" ][ key ] = {
|
|
40
|
+
"repo_path" => repository.path,
|
|
41
|
+
"branch_name" => branch_name,
|
|
42
|
+
"head" => head,
|
|
43
|
+
"worktree_path" => worktree_path,
|
|
44
|
+
"status" => status,
|
|
45
|
+
"pr_number" => pr_number,
|
|
46
|
+
"pr_url" => pr_url,
|
|
47
|
+
"cause" => cause,
|
|
48
|
+
"summary" => summary,
|
|
49
|
+
"created_at" => timestamp,
|
|
50
|
+
"updated_at" => timestamp,
|
|
51
|
+
"integrated_at" => nil,
|
|
52
|
+
"superseded_at" => nil,
|
|
53
|
+
"revisions" => []
|
|
54
|
+
}
|
|
55
|
+
build_delivery( key: key, data: state[ "deliveries" ][ key ], repository: repository )
|
|
105
56
|
end
|
|
106
57
|
end
|
|
107
58
|
|
|
108
59
|
# Looks up the active delivery for a branch, if one exists.
|
|
109
60
|
def active_delivery( repo_path:, branch_name: )
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
SQL
|
|
118
|
-
[ repo_path, branch_name, *ACTIVE_DELIVERY_STATES ]
|
|
119
|
-
)
|
|
120
|
-
build_delivery( row: row ) if row
|
|
61
|
+
state = load_state
|
|
62
|
+
repo_paths = repo_identity_paths( repo_path: repo_path )
|
|
63
|
+
|
|
64
|
+
candidates = state[ "deliveries" ].select do |_key, data|
|
|
65
|
+
repo_paths.include?( data[ "repo_path" ] ) &&
|
|
66
|
+
data[ "branch_name" ] == branch_name &&
|
|
67
|
+
ACTIVE_DELIVERY_STATES.include?( data[ "status" ] )
|
|
121
68
|
end
|
|
69
|
+
|
|
70
|
+
return nil if candidates.empty?
|
|
71
|
+
|
|
72
|
+
key, data = candidates.max_by { |k, d| [ d[ "updated_at" ].to_s, k ] }
|
|
73
|
+
build_delivery( key: key, data: data )
|
|
122
74
|
end
|
|
123
75
|
|
|
124
76
|
# Lists active deliveries for a repository in creation order.
|
|
125
77
|
def active_deliveries( repo_path: )
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
[ repo_path, *ACTIVE_DELIVERY_STATES ]
|
|
134
|
-
)
|
|
135
|
-
rows.map { |row| build_delivery( row: row ) }
|
|
136
|
-
end
|
|
78
|
+
state = load_state
|
|
79
|
+
repo_paths = repo_identity_paths( repo_path: repo_path )
|
|
80
|
+
|
|
81
|
+
state[ "deliveries" ]
|
|
82
|
+
.select { |_key, data| repo_paths.include?( data[ "repo_path" ] ) && ACTIVE_DELIVERY_STATES.include?( data[ "status" ] ) }
|
|
83
|
+
.sort_by { |key, data| [ data[ "created_at" ].to_s, key ] }
|
|
84
|
+
.map { |key, data| build_delivery( key: key, data: data ) }
|
|
137
85
|
end
|
|
138
86
|
|
|
139
|
-
# Lists
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
87
|
+
# Lists integrated deliveries that still retain a worktree path.
|
|
88
|
+
def integrated_deliveries( repo_path: )
|
|
89
|
+
state = load_state
|
|
90
|
+
repo_paths = repo_identity_paths( repo_path: repo_path )
|
|
91
|
+
|
|
92
|
+
state[ "deliveries" ]
|
|
93
|
+
.select do |_key, data|
|
|
94
|
+
repo_paths.include?( data[ "repo_path" ] ) &&
|
|
95
|
+
data[ "status" ] == "integrated" &&
|
|
96
|
+
!data[ "worktree_path" ].to_s.strip.empty?
|
|
97
|
+
end
|
|
98
|
+
.sort_by { |key, data| [ data[ "integrated_at" ].to_s, data[ "updated_at" ].to_s, key ] }
|
|
99
|
+
.map { |key, data| build_delivery( key: key, data: data ) }
|
|
147
100
|
end
|
|
148
101
|
|
|
149
102
|
# Updates a delivery record in place.
|
|
@@ -155,151 +108,218 @@ module Carson
|
|
|
155
108
|
cause: UNSET,
|
|
156
109
|
summary: UNSET,
|
|
157
110
|
worktree_path: UNSET,
|
|
158
|
-
revision_count: UNSET,
|
|
159
111
|
integrated_at: UNSET,
|
|
160
112
|
superseded_at: UNSET
|
|
161
113
|
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"UPDATE deliveries SET #{assignments} WHERE id = ?",
|
|
178
|
-
updates.values + [ delivery.id ]
|
|
179
|
-
)
|
|
180
|
-
fetch_delivery( database: database, id: delivery.id, repository: delivery.repository )
|
|
114
|
+
with_state do |state|
|
|
115
|
+
data = state[ "deliveries" ][ delivery.key ]
|
|
116
|
+
raise "delivery not found: #{delivery.key}" unless data
|
|
117
|
+
|
|
118
|
+
data[ "status" ] = status unless status.equal?( UNSET )
|
|
119
|
+
data[ "pr_number" ] = pr_number unless pr_number.equal?( UNSET )
|
|
120
|
+
data[ "pr_url" ] = pr_url unless pr_url.equal?( UNSET )
|
|
121
|
+
data[ "cause" ] = cause unless cause.equal?( UNSET )
|
|
122
|
+
data[ "summary" ] = summary unless summary.equal?( UNSET )
|
|
123
|
+
data[ "worktree_path" ] = worktree_path unless worktree_path.equal?( UNSET )
|
|
124
|
+
data[ "integrated_at" ] = integrated_at unless integrated_at.equal?( UNSET )
|
|
125
|
+
data[ "superseded_at" ] = superseded_at unless superseded_at.equal?( UNSET )
|
|
126
|
+
data[ "updated_at" ] = now_utc
|
|
127
|
+
|
|
128
|
+
build_delivery( key: delivery.key, data: data, repository: delivery.repository )
|
|
181
129
|
end
|
|
182
130
|
end
|
|
183
131
|
|
|
184
|
-
# Records one revision cycle against a delivery
|
|
132
|
+
# Records one revision cycle against a delivery.
|
|
185
133
|
def record_revision( delivery:, cause:, provider:, status:, summary: )
|
|
186
134
|
timestamp = now_utc
|
|
187
135
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
build_revision(
|
|
209
|
-
row: database.get_first_row( "SELECT * FROM revisions WHERE id = ?", [ database.last_insert_row_id ] )
|
|
210
|
-
)
|
|
136
|
+
with_state do |state|
|
|
137
|
+
data = state[ "deliveries" ][ delivery.key ]
|
|
138
|
+
raise "delivery not found: #{delivery.key}" unless data
|
|
139
|
+
|
|
140
|
+
revisions = data[ "revisions" ] ||= []
|
|
141
|
+
next_number = ( revisions.map { |r| r[ "number" ].to_i }.max || 0 ) + 1
|
|
142
|
+
finished = %w[completed failed stalled].include?( status ) ? timestamp : nil
|
|
143
|
+
|
|
144
|
+
revision_data = {
|
|
145
|
+
"number" => next_number,
|
|
146
|
+
"cause" => cause,
|
|
147
|
+
"provider" => provider,
|
|
148
|
+
"status" => status,
|
|
149
|
+
"started_at" => timestamp,
|
|
150
|
+
"finished_at" => finished,
|
|
151
|
+
"summary" => summary
|
|
152
|
+
}
|
|
153
|
+
revisions << revision_data
|
|
154
|
+
data[ "updated_at" ] = timestamp
|
|
155
|
+
|
|
156
|
+
build_revision( data: revision_data )
|
|
211
157
|
end
|
|
212
158
|
end
|
|
213
159
|
|
|
214
|
-
#
|
|
215
|
-
def revisions_for_delivery(
|
|
216
|
-
|
|
217
|
-
database.execute(
|
|
218
|
-
"SELECT * FROM revisions WHERE delivery_id = ? ORDER BY number ASC, id ASC",
|
|
219
|
-
[ delivery_id ]
|
|
220
|
-
).map { |row| build_revision( row: row ) }
|
|
221
|
-
end
|
|
160
|
+
# Returns revisions for a delivery in ascending order.
|
|
161
|
+
def revisions_for_delivery( delivery: )
|
|
162
|
+
delivery.revisions.sort_by( &:number )
|
|
222
163
|
end
|
|
223
164
|
|
|
224
165
|
private
|
|
225
166
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
167
|
+
# Acquires file lock, loads state, yields for mutation, saves atomically, releases lock.
|
|
168
|
+
def with_state
|
|
169
|
+
lock_path = "#{path}.lock"
|
|
170
|
+
FileUtils.mkdir_p( File.dirname( lock_path ) )
|
|
171
|
+
FileUtils.touch( lock_path )
|
|
172
|
+
|
|
173
|
+
File.open( lock_path, File::RDWR | File::CREAT ) do |lock_file|
|
|
174
|
+
lock_file.flock( File::LOCK_EX )
|
|
175
|
+
state = load_state
|
|
176
|
+
result = yield state
|
|
177
|
+
save_state!( state )
|
|
178
|
+
result
|
|
179
|
+
end
|
|
234
180
|
end
|
|
235
181
|
|
|
236
|
-
def
|
|
237
|
-
|
|
238
|
-
|
|
182
|
+
def load_state
|
|
183
|
+
return { "deliveries" => {}, "recovery_events" => [] } unless File.exist?( path )
|
|
184
|
+
|
|
185
|
+
raw = File.read( path )
|
|
186
|
+
return { "deliveries" => {}, "recovery_events" => [] } if raw.strip.empty?
|
|
187
|
+
|
|
188
|
+
parsed = JSON.parse( raw )
|
|
189
|
+
raise "state file must contain a JSON object at #{path}" unless parsed.is_a?( Hash )
|
|
190
|
+
parsed[ "deliveries" ] ||= {}
|
|
191
|
+
parsed[ "recovery_events" ] ||= []
|
|
192
|
+
parsed
|
|
193
|
+
rescue JSON::ParserError => exception
|
|
194
|
+
raise "invalid JSON in state file #{path}: #{exception.message}"
|
|
239
195
|
end
|
|
240
196
|
|
|
241
|
-
def
|
|
242
|
-
|
|
197
|
+
def save_state!( state )
|
|
198
|
+
tmp_path = "#{path}.tmp"
|
|
199
|
+
File.write( tmp_path, JSON.pretty_generate( state ) + "\n" )
|
|
200
|
+
File.rename( tmp_path, path )
|
|
201
|
+
end
|
|
243
202
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
203
|
+
def delivery_key( repo_path:, branch_name:, head: )
|
|
204
|
+
"#{repo_path}:#{branch_name}:#{head}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def build_delivery( key:, data:, repository: nil )
|
|
208
|
+
return nil unless data
|
|
209
|
+
|
|
210
|
+
revisions = Array( data[ "revisions" ] ).map { |r| build_revision( data: r ) }
|
|
249
211
|
|
|
250
212
|
Delivery.new(
|
|
251
|
-
|
|
213
|
+
repo_path: data.fetch( "repo_path" ),
|
|
252
214
|
repository: repository,
|
|
253
|
-
branch:
|
|
254
|
-
head:
|
|
255
|
-
worktree_path:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
superseded_at: row.fetch( "superseded_at" )
|
|
215
|
+
branch: data.fetch( "branch_name" ),
|
|
216
|
+
head: data.fetch( "head" ),
|
|
217
|
+
worktree_path: data[ "worktree_path" ],
|
|
218
|
+
status: data.fetch( "status" ),
|
|
219
|
+
pull_request_number: data[ "pr_number" ],
|
|
220
|
+
pull_request_url: data[ "pr_url" ],
|
|
221
|
+
revisions: revisions,
|
|
222
|
+
cause: data[ "cause" ],
|
|
223
|
+
summary: data[ "summary" ],
|
|
224
|
+
created_at: data.fetch( "created_at" ),
|
|
225
|
+
updated_at: data.fetch( "updated_at" ),
|
|
226
|
+
integrated_at: data[ "integrated_at" ],
|
|
227
|
+
superseded_at: data[ "superseded_at" ]
|
|
267
228
|
)
|
|
268
229
|
end
|
|
269
230
|
|
|
270
|
-
def build_revision(
|
|
271
|
-
return nil unless
|
|
231
|
+
def build_revision( data: )
|
|
232
|
+
return nil unless data
|
|
272
233
|
|
|
273
234
|
Revision.new(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
finished_at: row.fetch( "finished_at" ),
|
|
282
|
-
summary: row.fetch( "summary" )
|
|
235
|
+
number: data.fetch( "number" ).to_i,
|
|
236
|
+
cause: data.fetch( "cause" ),
|
|
237
|
+
provider: data.fetch( "provider" ),
|
|
238
|
+
status: data.fetch( "status" ),
|
|
239
|
+
started_at: data.fetch( "started_at" ),
|
|
240
|
+
finished_at: data[ "finished_at" ],
|
|
241
|
+
summary: data[ "summary" ]
|
|
283
242
|
)
|
|
284
243
|
end
|
|
285
244
|
|
|
286
|
-
def supersede_branch!(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
[ "
|
|
294
|
-
|
|
245
|
+
def supersede_branch!( state:, repo_path:, branch_name:, timestamp: )
|
|
246
|
+
repo_paths = repo_identity_paths( repo_path: repo_path )
|
|
247
|
+
state[ "deliveries" ].each do |_key, data|
|
|
248
|
+
next unless repo_paths.include?( data[ "repo_path" ] )
|
|
249
|
+
next unless data[ "branch_name" ] == branch_name
|
|
250
|
+
next unless ACTIVE_DELIVERY_STATES.include?( data[ "status" ] )
|
|
251
|
+
|
|
252
|
+
data[ "status" ] = "superseded"
|
|
253
|
+
data[ "superseded_at" ] = timestamp
|
|
254
|
+
data[ "updated_at" ] = timestamp
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def repo_identity_paths( repo_path: )
|
|
259
|
+
canonical_path = File.expand_path( repo_path )
|
|
260
|
+
canonical_realpath = realpath_or_nil( path: canonical_path )
|
|
261
|
+
worktree_gitdirs = Dir.glob( File.join( canonical_path, ".git", "worktrees", "*", "gitdir" ) )
|
|
262
|
+
paths = worktree_gitdirs.each_with_object( path_aliases( path: canonical_path ) ) do |gitdir_path, identities|
|
|
263
|
+
worktree_git_path = File.read( gitdir_path ).to_s.strip
|
|
264
|
+
next if worktree_git_path.empty?
|
|
265
|
+
|
|
266
|
+
worktree_path = File.dirname( File.expand_path( worktree_git_path, File.dirname( gitdir_path ) ) )
|
|
267
|
+
identities.concat( path_aliases( path: worktree_path ) )
|
|
268
|
+
|
|
269
|
+
worktree_realpath = realpath_or_nil( path: worktree_path )
|
|
270
|
+
next unless canonical_realpath && worktree_realpath
|
|
271
|
+
|
|
272
|
+
canonical_prefix = File.join( canonical_realpath, "" )
|
|
273
|
+
next unless worktree_realpath.start_with?( canonical_prefix )
|
|
274
|
+
|
|
275
|
+
relative_path = worktree_realpath.delete_prefix( canonical_prefix )
|
|
276
|
+
identities << File.join( canonical_path, relative_path ) unless relative_path.empty?
|
|
277
|
+
end
|
|
278
|
+
paths.uniq
|
|
279
|
+
rescue StandardError
|
|
280
|
+
[ File.expand_path( repo_path ) ]
|
|
295
281
|
end
|
|
296
282
|
|
|
297
|
-
def
|
|
298
|
-
|
|
283
|
+
def path_aliases( path: )
|
|
284
|
+
expanded_path = File.expand_path( path )
|
|
285
|
+
aliases = [ expanded_path, realpath_or_nil( path: expanded_path ) ]
|
|
286
|
+
aliases << expanded_path.delete_prefix( "/private" ) if expanded_path.start_with?( "/private/" )
|
|
287
|
+
aliases.compact.uniq
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def realpath_or_nil( path: )
|
|
291
|
+
File.realpath( path )
|
|
292
|
+
rescue StandardError
|
|
293
|
+
nil
|
|
299
294
|
end
|
|
300
295
|
|
|
301
296
|
def now_utc
|
|
302
297
|
Time.now.utc.iso8601
|
|
303
298
|
end
|
|
299
|
+
|
|
300
|
+
def record_recovery_event( repository:, branch_name:, pr_number:, pr_url:, check_name:, default_branch:, default_branch_sha:, pr_sha:, actor:, merge_method:, status:, summary: )
|
|
301
|
+
timestamp = now_utc
|
|
302
|
+
|
|
303
|
+
with_state do |state|
|
|
304
|
+
state[ "recovery_events" ] ||= []
|
|
305
|
+
event = {
|
|
306
|
+
"repository" => repository.path,
|
|
307
|
+
"branch_name" => branch_name,
|
|
308
|
+
"pr_number" => pr_number,
|
|
309
|
+
"pr_url" => pr_url,
|
|
310
|
+
"check_name" => check_name,
|
|
311
|
+
"default_branch" => default_branch,
|
|
312
|
+
"default_branch_sha" => default_branch_sha,
|
|
313
|
+
"pr_sha" => pr_sha,
|
|
314
|
+
"actor" => actor,
|
|
315
|
+
"merge_method" => merge_method,
|
|
316
|
+
"status" => status,
|
|
317
|
+
"summary" => summary,
|
|
318
|
+
"recorded_at" => timestamp
|
|
319
|
+
}
|
|
320
|
+
state[ "recovery_events" ] << event
|
|
321
|
+
event
|
|
322
|
+
end
|
|
323
|
+
end
|
|
304
324
|
end
|
|
305
325
|
end
|
data/lib/carson/repository.rb
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# Passive repository record reconstructed from git state and Carson's ledger.
|
|
2
2
|
module Carson
|
|
3
3
|
class Repository
|
|
4
|
-
attr_reader :path
|
|
4
|
+
attr_reader :path
|
|
5
5
|
|
|
6
|
-
def initialize( path:,
|
|
6
|
+
def initialize( path:, runtime: )
|
|
7
7
|
@path = File.expand_path( path )
|
|
8
|
-
@authority = authority
|
|
9
8
|
@runtime = runtime
|
|
10
9
|
end
|
|
11
10
|
|
|
@@ -35,7 +34,6 @@ module Carson
|
|
|
35
34
|
{
|
|
36
35
|
name: name,
|
|
37
36
|
path: path,
|
|
38
|
-
authority: authority,
|
|
39
37
|
branches: runtime.ledger.active_deliveries( repo_path: path ).map { |delivery| delivery.branch }
|
|
40
38
|
}
|
|
41
39
|
end
|
data/lib/carson/revision.rb
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# Passive ledger record for one feedback-driven revision cycle.
|
|
2
2
|
module Carson
|
|
3
3
|
class Revision
|
|
4
|
-
attr_reader :
|
|
4
|
+
attr_reader :number, :cause, :provider, :status, :started_at, :finished_at, :summary
|
|
5
5
|
|
|
6
|
-
def initialize(
|
|
7
|
-
@id = id
|
|
8
|
-
@delivery_id = delivery_id
|
|
6
|
+
def initialize( number:, cause:, provider:, status:, started_at:, finished_at:, summary: )
|
|
9
7
|
@number = number
|
|
10
8
|
@cause = cause
|
|
11
9
|
@provider = provider
|