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.
data/lib/carson/ledger.rb CHANGED
@@ -1,6 +1,6 @@
1
- # SQLite-backed ledger for Carson's deliveries and revisions.
1
+ # JSON file-backed ledger for Carson's deliveries and revisions.
2
2
  require "fileutils"
3
- require "sqlite3"
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
- prepare!
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:, authority:, pr_number:, pr_url:, status:, summary:, cause: )
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
- with_database do |database|
73
- row = database.get_first_row(
74
- "SELECT * FROM deliveries WHERE repo_path = ? AND branch_name = ? AND head = ? LIMIT 1",
75
- [ repository.path, branch_name, head ]
76
- )
77
-
78
- if row
79
- database.execute(
80
- <<~SQL,
81
- UPDATE deliveries
82
- SET worktree_path = ?, authority = ?, status = ?, pr_number = ?, pr_url = ?,
83
- cause = ?, summary = ?, updated_at = ?
84
- WHERE id = ?
85
- SQL
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!( database: database, repository: repository, branch_name: branch_name, timestamp: timestamp )
92
- database.execute(
93
- <<~SQL,
94
- INSERT INTO deliveries (
95
- repo_path, branch_name, head, worktree_path, authority, status,
96
- pr_number, pr_url, revision_count, cause, summary, created_at, updated_at
97
- ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ? )
98
- SQL
99
- [
100
- repository.path, branch_name, head, worktree_path, authority, status,
101
- pr_number, pr_url, cause, summary, timestamp, timestamp
102
- ]
103
- )
104
- fetch_delivery( database: database, id: database.last_insert_row_id, repository: repository )
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
- with_database do |database|
111
- row = database.get_first_row(
112
- <<~SQL,
113
- SELECT * FROM deliveries
114
- WHERE repo_path = ? AND branch_name = ? AND status IN ( #{active_state_placeholders} )
115
- ORDER BY updated_at DESC
116
- LIMIT 1
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
- with_database do |database|
127
- rows = database.execute(
128
- <<~SQL,
129
- SELECT * FROM deliveries
130
- WHERE repo_path = ? AND status IN ( #{active_state_placeholders} )
131
- ORDER BY created_at ASC, id ASC
132
- SQL
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 queued deliveries ready for integration.
140
- def queued_deliveries( repo_path: )
141
- with_database do |database|
142
- database.execute(
143
- "SELECT * FROM deliveries WHERE repo_path = ? AND status = ? ORDER BY created_at ASC, id ASC",
144
- [ repo_path, "queued" ]
145
- ).map { |row| build_delivery( row: row ) }
146
- end
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
- updates = {}
163
- updates[ "status" ] = status unless status.equal?( UNSET )
164
- updates[ "pr_number" ] = pr_number unless pr_number.equal?( UNSET )
165
- updates[ "pr_url" ] = pr_url unless pr_url.equal?( UNSET )
166
- updates[ "cause" ] = cause unless cause.equal?( UNSET )
167
- updates[ "summary" ] = summary unless summary.equal?( UNSET )
168
- updates[ "worktree_path" ] = worktree_path unless worktree_path.equal?( UNSET )
169
- updates[ "revision_count" ] = revision_count unless revision_count.equal?( UNSET )
170
- updates[ "integrated_at" ] = integrated_at unless integrated_at.equal?( UNSET )
171
- updates[ "superseded_at" ] = superseded_at unless superseded_at.equal?( UNSET )
172
- updates[ "updated_at" ] = now_utc
173
-
174
- with_database do |database|
175
- assignments = updates.keys.map { |key| "#{key} = ?" }.join( ", " )
176
- database.execute(
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 and bumps the delivery counter.
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
- with_database do |database|
189
- next_number = database.get_first_value(
190
- "SELECT COALESCE( MAX(number), 0 ) + 1 FROM revisions WHERE delivery_id = ?",
191
- [ delivery.id ]
192
- ).to_i
193
- database.execute(
194
- <<~SQL,
195
- INSERT INTO revisions ( delivery_id, number, cause, provider, status, started_at, finished_at, summary )
196
- VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
197
- SQL
198
- [
199
- delivery.id, next_number, cause, provider, status, timestamp,
200
- ( status == "completed" || status == "failed" || status == "stalled" ) ? timestamp : nil,
201
- summary
202
- ]
203
- )
204
- database.execute(
205
- "UPDATE deliveries SET revision_count = ?, updated_at = ? WHERE id = ?",
206
- [ next_number, timestamp, delivery.id ]
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
- # Lists revisions for a delivery in ascending order.
215
- def revisions_for_delivery( delivery_id: )
216
- with_database do |database|
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
- def with_database
227
- database = SQLite3::Database.new( path )
228
- database.results_as_hash = true
229
- database.busy_timeout = 5_000
230
- database.execute( "PRAGMA journal_mode = WAL" )
231
- yield database
232
- ensure
233
- database&.close
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 fetch_delivery( database:, id:, repository: nil )
237
- row = database.get_first_row( "SELECT * FROM deliveries WHERE id = ?", [ id ] )
238
- build_delivery( row: row, repository: repository )
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 build_delivery( row:, repository: nil )
242
- return nil unless row
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
- repository ||= Repository.new(
245
- path: row.fetch( "repo_path" ),
246
- authority: row.fetch( "authority" ),
247
- runtime: nil
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
- id: row.fetch( "id" ),
213
+ repo_path: data.fetch( "repo_path" ),
252
214
  repository: repository,
253
- branch: row.fetch( "branch_name" ),
254
- head: row.fetch( "head" ),
255
- worktree_path: row.fetch( "worktree_path" ),
256
- authority: row.fetch( "authority" ),
257
- status: row.fetch( "status" ),
258
- pull_request_number: row.fetch( "pr_number" ),
259
- pull_request_url: row.fetch( "pr_url" ),
260
- revision_count: row.fetch( "revision_count" ).to_i,
261
- cause: row.fetch( "cause" ),
262
- summary: row.fetch( "summary" ),
263
- created_at: row.fetch( "created_at" ),
264
- updated_at: row.fetch( "updated_at" ),
265
- integrated_at: row.fetch( "integrated_at" ),
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( row: )
271
- return nil unless row
231
+ def build_revision( data: )
232
+ return nil unless data
272
233
 
273
234
  Revision.new(
274
- id: row.fetch( "id" ),
275
- delivery_id: row.fetch( "delivery_id" ),
276
- number: row.fetch( "number" ).to_i,
277
- cause: row.fetch( "cause" ),
278
- provider: row.fetch( "provider" ),
279
- status: row.fetch( "status" ),
280
- started_at: row.fetch( "started_at" ),
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!( database:, repository:, branch_name:, timestamp: )
287
- database.execute(
288
- <<~SQL,
289
- UPDATE deliveries
290
- SET status = ?, superseded_at = ?, updated_at = ?
291
- WHERE repo_path = ? AND branch_name = ? AND status IN ( #{active_state_placeholders} )
292
- SQL
293
- [ "superseded", timestamp, timestamp, repository.path, branch_name, *ACTIVE_DELIVERY_STATES ]
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 active_state_placeholders
298
- ACTIVE_DELIVERY_STATES.map { "?" }.join( ", " )
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
@@ -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, :authority
4
+ attr_reader :path
5
5
 
6
- def initialize( path:, authority:, runtime: )
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
@@ -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 :id, :delivery_id, :number, :cause, :provider, :status, :started_at, :finished_at, :summary
4
+ attr_reader :number, :cause, :provider, :status, :started_at, :finished_at, :summary
5
5
 
6
- def initialize( id:, delivery_id:, number:, cause:, provider:, status:, started_at:, finished_at:, summary: )
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