braid-blob 0.0.21 → 0.0.23
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.
- package/index.js +24 -42
- package/package.json +2 -2
- package/test/tests.js +258 -2
- package/.claude/settings.local.json +0 -20
package/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
var {http_server: braidify, fetch: braid_fetch} = require('braid-http'),
|
|
2
2
|
{url_file_db} = require('url-file-db'),
|
|
3
|
-
fs = require('fs'),
|
|
4
3
|
path = require('path')
|
|
5
4
|
|
|
6
5
|
function create_braid_blob() {
|
|
@@ -9,9 +8,8 @@ function create_braid_blob() {
|
|
|
9
8
|
meta_folder: './braid-blob-meta',
|
|
10
9
|
cache: {},
|
|
11
10
|
key_to_subs: {},
|
|
12
|
-
peer: null, //
|
|
13
|
-
db: null
|
|
14
|
-
meta_db: null // url-file-db instance for meta storage
|
|
11
|
+
peer: null, // will be auto-generated if not set by the user
|
|
12
|
+
db: null // url-file-db instance with integrated meta storage
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
braid_blob.init = async () => {
|
|
@@ -21,25 +19,21 @@ function create_braid_blob() {
|
|
|
21
19
|
await braid_blob.init()
|
|
22
20
|
|
|
23
21
|
async function real_init() {
|
|
24
|
-
// Create url-file-db instance
|
|
25
|
-
braid_blob.db = await url_file_db.create(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
// Create url-file-db instance with integrated meta storage
|
|
23
|
+
braid_blob.db = await url_file_db.create(
|
|
24
|
+
braid_blob.db_folder,
|
|
25
|
+
braid_blob.meta_folder,
|
|
26
|
+
async (db, key) => {
|
|
27
|
+
// File changed externally, notify subscriptions
|
|
28
|
+
// Use db parameter instead of braid_blob.db to avoid race condition
|
|
29
|
+
var body = await db.read(key)
|
|
30
|
+
await braid_blob.put(key, body, { skip_write: true })
|
|
31
|
+
}
|
|
32
|
+
)
|
|
34
33
|
|
|
35
|
-
// establish a peer id
|
|
36
|
-
if (!braid_blob.peer)
|
|
37
|
-
try {
|
|
38
|
-
braid_blob.peer = await fs.promises.readFile(`${braid_blob.meta_folder}/peer.txt`, 'utf8')
|
|
39
|
-
} catch (e) {}
|
|
34
|
+
// establish a peer id if not already set
|
|
40
35
|
if (!braid_blob.peer)
|
|
41
36
|
braid_blob.peer = Math.random().toString(36).slice(2)
|
|
42
|
-
await fs.promises.writeFile(`${braid_blob.meta_folder}/peer.txt`, braid_blob.peer)
|
|
43
37
|
}
|
|
44
38
|
}
|
|
45
39
|
|
|
@@ -69,11 +63,8 @@ function create_braid_blob() {
|
|
|
69
63
|
|
|
70
64
|
await braid_blob.init()
|
|
71
65
|
|
|
72
|
-
// Read the meta data
|
|
73
|
-
var meta = {}
|
|
74
|
-
var meta_content = await braid_blob.meta_db.read(key)
|
|
75
|
-
if (meta_content)
|
|
76
|
-
meta = JSON.parse(meta_content.toString('utf8'))
|
|
66
|
+
// Read the meta data using new meta API
|
|
67
|
+
var meta = braid_blob.db.get_meta(key) || {}
|
|
77
68
|
|
|
78
69
|
var their_e =
|
|
79
70
|
!options.version ?
|
|
@@ -93,11 +84,12 @@ function create_braid_blob() {
|
|
|
93
84
|
if (!options.skip_write)
|
|
94
85
|
await braid_blob.db.write(key, body)
|
|
95
86
|
|
|
96
|
-
//
|
|
87
|
+
// Update only the fields we want to change in metadata
|
|
88
|
+
var meta_updates = { event: their_e }
|
|
97
89
|
if (options.content_type)
|
|
98
|
-
|
|
90
|
+
meta_updates.content_type = options.content_type
|
|
99
91
|
|
|
100
|
-
await braid_blob.
|
|
92
|
+
await braid_blob.db.update_meta(key, meta_updates)
|
|
101
93
|
|
|
102
94
|
// Notify all subscriptions of the update
|
|
103
95
|
// (except the peer which made the PUT request itself)
|
|
@@ -153,11 +145,8 @@ function create_braid_blob() {
|
|
|
153
145
|
|
|
154
146
|
await braid_blob.init()
|
|
155
147
|
|
|
156
|
-
// Read the meta data
|
|
157
|
-
var meta = {}
|
|
158
|
-
var meta_content = await braid_blob.meta_db.read(key)
|
|
159
|
-
if (meta_content)
|
|
160
|
-
meta = JSON.parse(meta_content.toString('utf8'))
|
|
148
|
+
// Read the meta data using new meta API
|
|
149
|
+
var meta = braid_blob.db.get_meta(key) || {}
|
|
161
150
|
if (meta.event == null) return null
|
|
162
151
|
|
|
163
152
|
var result = {
|
|
@@ -238,12 +227,6 @@ function create_braid_blob() {
|
|
|
238
227
|
var body = req.method === 'PUT' && await slurp(req)
|
|
239
228
|
|
|
240
229
|
await within_fiber(options.key, async () => {
|
|
241
|
-
// Read the meta data from meta_db
|
|
242
|
-
var meta = {}
|
|
243
|
-
var meta_content = await braid_blob.meta_db.read(options.key)
|
|
244
|
-
if (meta_content)
|
|
245
|
-
meta = JSON.parse(meta_content.toString('utf8'))
|
|
246
|
-
|
|
247
230
|
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
248
231
|
if (!res.hasHeader("editable")) res.setHeader("Editable", "true")
|
|
249
232
|
if (!req.subscribe) res.setHeader("Accept-Subscribe", "true")
|
|
@@ -301,16 +284,15 @@ function create_braid_blob() {
|
|
|
301
284
|
}
|
|
302
285
|
} else if (req.method === 'PUT') {
|
|
303
286
|
// Handle PUT request to update binary files
|
|
304
|
-
|
|
287
|
+
var event = await braid_blob.put(options.key, body, {
|
|
305
288
|
version: req.version,
|
|
306
289
|
content_type: req.headers['content-type'],
|
|
307
290
|
peer: req.peer
|
|
308
291
|
})
|
|
309
|
-
res.setHeader("Version", version_to_header(
|
|
292
|
+
res.setHeader("Version", version_to_header(event != null ? [event] : []))
|
|
310
293
|
res.end('')
|
|
311
294
|
} else if (req.method === 'DELETE') {
|
|
312
295
|
await braid_blob.db.delete(options.key)
|
|
313
|
-
await braid_blob.meta_db.delete(options.key)
|
|
314
296
|
res.statusCode = 204 // No Content
|
|
315
297
|
res.end('')
|
|
316
298
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braid-blob",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"description": "Library for collaborative blobs over http using braid.",
|
|
5
5
|
"author": "Braid Working Group",
|
|
6
6
|
"repository": "braid-org/braid-blob",
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"braid-http": "~1.3.82",
|
|
14
|
-
"url-file-db": "^0.0.
|
|
14
|
+
"url-file-db": "^0.0.21"
|
|
15
15
|
}
|
|
16
16
|
}
|
package/test/tests.js
CHANGED
|
@@ -34,7 +34,7 @@ runTest(
|
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
runTest(
|
|
37
|
-
"test that peer is
|
|
37
|
+
"test that peer is different each time we create a new instance",
|
|
38
38
|
async () => {
|
|
39
39
|
var r1 = await braid_fetch(`/eval`, {
|
|
40
40
|
method: 'POST',
|
|
@@ -62,7 +62,7 @@ runTest(
|
|
|
62
62
|
await require('fs').promises.rm(db, { recursive: true, force: true })
|
|
63
63
|
await require('fs').promises.rm(meta, { recursive: true, force: true })
|
|
64
64
|
|
|
65
|
-
res.end('' + (bb1.peer
|
|
65
|
+
res.end('' + (bb1.peer !== bb2.peer))
|
|
66
66
|
})()`
|
|
67
67
|
})
|
|
68
68
|
return await r1.text()
|
|
@@ -100,6 +100,39 @@ runTest(
|
|
|
100
100
|
'test_peer'
|
|
101
101
|
)
|
|
102
102
|
|
|
103
|
+
runTest(
|
|
104
|
+
"test that manually set peer persists through initialization",
|
|
105
|
+
async () => {
|
|
106
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: `void (async () => {
|
|
109
|
+
var test_id = 'test-db-' + Math.random().toString(36).slice(2)
|
|
110
|
+
var db = __dirname + '/' + test_id + '-db'
|
|
111
|
+
var meta = __dirname + '/' + test_id + '-meta'
|
|
112
|
+
|
|
113
|
+
// Create instance with manually set peer
|
|
114
|
+
var bb1 = braid_blob.create_braid_blob()
|
|
115
|
+
bb1.db_folder = db
|
|
116
|
+
bb1.meta_folder = meta
|
|
117
|
+
bb1.peer = 'custom-peer-id-123'
|
|
118
|
+
|
|
119
|
+
// Initialize (should keep our custom peer)
|
|
120
|
+
await bb1.init()
|
|
121
|
+
|
|
122
|
+
var peer_after_init = bb1.peer
|
|
123
|
+
|
|
124
|
+
// Clean up
|
|
125
|
+
await require('fs').promises.rm(db, { recursive: true, force: true })
|
|
126
|
+
await require('fs').promises.rm(meta, { recursive: true, force: true })
|
|
127
|
+
|
|
128
|
+
res.end(peer_after_init === 'custom-peer-id-123' ? 'true' : 'false: ' + peer_after_init)
|
|
129
|
+
})()`
|
|
130
|
+
})
|
|
131
|
+
return await r1.text()
|
|
132
|
+
},
|
|
133
|
+
'true'
|
|
134
|
+
)
|
|
135
|
+
|
|
103
136
|
runTest(
|
|
104
137
|
"test that PUTing with shorter event id doesn't do anything.",
|
|
105
138
|
async () => {
|
|
@@ -1142,6 +1175,229 @@ runTest(
|
|
|
1142
1175
|
'309'
|
|
1143
1176
|
)
|
|
1144
1177
|
|
|
1178
|
+
runTest(
|
|
1179
|
+
"test multiple writes preserve correct mtime across restarts",
|
|
1180
|
+
async () => {
|
|
1181
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1182
|
+
method: 'POST',
|
|
1183
|
+
body: `void (async () => {
|
|
1184
|
+
var fs = require('fs').promises
|
|
1185
|
+
var test_id = 'test-multi-write-' + Math.random().toString(36).slice(2)
|
|
1186
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1187
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1188
|
+
var test_key = 'test-file'
|
|
1189
|
+
|
|
1190
|
+
try {
|
|
1191
|
+
// Create first braid_blob instance
|
|
1192
|
+
var bb1 = braid_blob.create_braid_blob()
|
|
1193
|
+
bb1.db_folder = db_folder
|
|
1194
|
+
bb1.meta_folder = meta_folder
|
|
1195
|
+
|
|
1196
|
+
// First write
|
|
1197
|
+
await bb1.put(test_key, Buffer.from('content1'), {
|
|
1198
|
+
version: ['version-1']
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
// Wait a bit to ensure different mtime
|
|
1202
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
1203
|
+
|
|
1204
|
+
// Second write to same file (this is where the bug would occur)
|
|
1205
|
+
await bb1.put(test_key, Buffer.from('content2'), {
|
|
1206
|
+
version: ['version-2']
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
var result1 = await bb1.get(test_key)
|
|
1210
|
+
|
|
1211
|
+
// Now restart and check
|
|
1212
|
+
var bb2 = braid_blob.create_braid_blob()
|
|
1213
|
+
bb2.db_folder = db_folder
|
|
1214
|
+
bb2.meta_folder = meta_folder
|
|
1215
|
+
|
|
1216
|
+
// This should NOT trigger a file change callback
|
|
1217
|
+
var result2 = await bb2.get(test_key)
|
|
1218
|
+
|
|
1219
|
+
// Version should still be version-2, not regenerated
|
|
1220
|
+
var correct_version = (result2.version[0] === 'version-2')
|
|
1221
|
+
var content_correct = (result2.body.toString() === 'content2')
|
|
1222
|
+
|
|
1223
|
+
// Clean up
|
|
1224
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1225
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1226
|
+
|
|
1227
|
+
res.end(correct_version && content_correct ? 'true' :
|
|
1228
|
+
'false: version=' + result2.version[0] + ', content=' + result2.body.toString())
|
|
1229
|
+
} catch (e) {
|
|
1230
|
+
// Clean up even on error
|
|
1231
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1232
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1233
|
+
res.end('error: ' + e.message)
|
|
1234
|
+
}
|
|
1235
|
+
})()`
|
|
1236
|
+
})
|
|
1237
|
+
return await r1.text()
|
|
1238
|
+
},
|
|
1239
|
+
'true'
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
runTest(
|
|
1243
|
+
"test that files keep same event ID across restarts when not edited",
|
|
1244
|
+
async () => {
|
|
1245
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1246
|
+
method: 'POST',
|
|
1247
|
+
body: `void (async () => {
|
|
1248
|
+
var fs = require('fs').promises
|
|
1249
|
+
var test_id = 'test-persist-event-' + Math.random().toString(36).slice(2)
|
|
1250
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1251
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1252
|
+
var test_key = 'test-file'
|
|
1253
|
+
var test_content = 'test content that should not change'
|
|
1254
|
+
|
|
1255
|
+
try {
|
|
1256
|
+
// Create first braid_blob instance
|
|
1257
|
+
var bb1 = braid_blob.create_braid_blob()
|
|
1258
|
+
bb1.db_folder = db_folder
|
|
1259
|
+
bb1.meta_folder = meta_folder
|
|
1260
|
+
|
|
1261
|
+
// Put a file with specific version
|
|
1262
|
+
var version1 = await bb1.put(test_key, Buffer.from(test_content), {
|
|
1263
|
+
version: ['test-peer-123456']
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1266
|
+
// Get the file to verify it has the expected version
|
|
1267
|
+
var result1 = await bb1.get(test_key)
|
|
1268
|
+
|
|
1269
|
+
// Check what metadata was saved
|
|
1270
|
+
var meta1 = bb1.db.get_meta(test_key)
|
|
1271
|
+
var debug_info = 'meta1: ' + JSON.stringify(meta1) + '; '
|
|
1272
|
+
|
|
1273
|
+
// Wait a bit to ensure file system has settled
|
|
1274
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
1275
|
+
|
|
1276
|
+
// Now create a second braid_blob instance with the same folders
|
|
1277
|
+
// This simulates a restart
|
|
1278
|
+
var bb2 = braid_blob.create_braid_blob()
|
|
1279
|
+
bb2.db_folder = db_folder
|
|
1280
|
+
bb2.meta_folder = meta_folder
|
|
1281
|
+
|
|
1282
|
+
// Initialize bb2 by doing a get (this triggers init)
|
|
1283
|
+
var result2 = await bb2.get(test_key)
|
|
1284
|
+
|
|
1285
|
+
// Check what metadata bb2 sees
|
|
1286
|
+
var meta2 = bb2.db.get_meta(test_key)
|
|
1287
|
+
debug_info += 'meta2: ' + JSON.stringify(meta2) + '; '
|
|
1288
|
+
|
|
1289
|
+
// The version should be the same - no new event ID generated
|
|
1290
|
+
var versions_match = (result1.version[0] === result2.version[0])
|
|
1291
|
+
var both_have_expected = (result1.version[0] === 'test-peer-123456')
|
|
1292
|
+
|
|
1293
|
+
// Clean up
|
|
1294
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1295
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1296
|
+
|
|
1297
|
+
res.end(versions_match && both_have_expected ? 'true' :
|
|
1298
|
+
'false: v1=' + result1.version[0] + ', v2=' + result2.version[0] + ' | ' + debug_info)
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
// Clean up even on error
|
|
1301
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1302
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1303
|
+
res.end('error: ' + e.message)
|
|
1304
|
+
}
|
|
1305
|
+
})()`
|
|
1306
|
+
})
|
|
1307
|
+
return await r1.text()
|
|
1308
|
+
},
|
|
1309
|
+
'true'
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
runTest(
|
|
1313
|
+
"test that callback receives db parameter for use before assignment",
|
|
1314
|
+
async () => {
|
|
1315
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1316
|
+
method: 'POST',
|
|
1317
|
+
body: `void (async () => {
|
|
1318
|
+
var fs = require('fs').promises
|
|
1319
|
+
var test_id = 'test-callback-' + Math.random().toString(36).slice(2)
|
|
1320
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1321
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1322
|
+
|
|
1323
|
+
try {
|
|
1324
|
+
// Pre-create files that will trigger callback during init
|
|
1325
|
+
await fs.mkdir(db_folder, { recursive: true })
|
|
1326
|
+
await fs.mkdir(meta_folder, { recursive: true })
|
|
1327
|
+
|
|
1328
|
+
// Write a file that exists before init
|
|
1329
|
+
await fs.writeFile(db_folder + '/pre-existing', 'old content')
|
|
1330
|
+
|
|
1331
|
+
// Create metadata for it with old timestamp to trigger callback
|
|
1332
|
+
await fs.writeFile(meta_folder + '/!pre-existing', JSON.stringify({
|
|
1333
|
+
canonical_path: '/pre-existing',
|
|
1334
|
+
event: 'old-version',
|
|
1335
|
+
last_seen: Date.now() - 10000,
|
|
1336
|
+
mtime_ns: '1000000000000000'
|
|
1337
|
+
}))
|
|
1338
|
+
|
|
1339
|
+
// Wait for files to be written
|
|
1340
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
1341
|
+
|
|
1342
|
+
var callback_error = null
|
|
1343
|
+
var callback_called = false
|
|
1344
|
+
var db_was_null = false
|
|
1345
|
+
|
|
1346
|
+
// Monkey-patch url_file_db.create to intercept callback
|
|
1347
|
+
var url_file_db_module = require('url-file-db').url_file_db
|
|
1348
|
+
var original_create = url_file_db_module.create
|
|
1349
|
+
|
|
1350
|
+
url_file_db_module.create = async function(db_dir, meta_dir, callback) {
|
|
1351
|
+
var wrapped_callback = async function(db, key) {
|
|
1352
|
+
callback_called = true
|
|
1353
|
+
// Check if bb.db is null during callback
|
|
1354
|
+
if (!bb.db) {
|
|
1355
|
+
db_was_null = true
|
|
1356
|
+
}
|
|
1357
|
+
try {
|
|
1358
|
+
await callback(db, key)
|
|
1359
|
+
} catch (e) {
|
|
1360
|
+
callback_error = e.message
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return await original_create.call(this, db_dir, meta_dir, wrapped_callback)
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
var bb = braid_blob.create_braid_blob()
|
|
1367
|
+
bb.db_folder = db_folder
|
|
1368
|
+
bb.meta_folder = meta_folder
|
|
1369
|
+
|
|
1370
|
+
// Init will trigger callback for pre-existing file
|
|
1371
|
+
// Callback tries to use braid_blob.db.read() but db not assigned yet
|
|
1372
|
+
await bb.init()
|
|
1373
|
+
|
|
1374
|
+
// Restore
|
|
1375
|
+
url_file_db_module.create = original_create
|
|
1376
|
+
|
|
1377
|
+
// Clean up
|
|
1378
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1379
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1380
|
+
|
|
1381
|
+
if (!callback_called) {
|
|
1382
|
+
res.end('callback was not called')
|
|
1383
|
+
} else if (callback_error) {
|
|
1384
|
+
res.end('callback error: ' + callback_error)
|
|
1385
|
+
} else {
|
|
1386
|
+
// Success: callback worked even if bb.db was null (using db param)
|
|
1387
|
+
res.end('true')
|
|
1388
|
+
}
|
|
1389
|
+
} catch (e) {
|
|
1390
|
+
await fs.rm(db_folder, { recursive: true, force: true }).catch(() => {})
|
|
1391
|
+
await fs.rm(meta_folder, { recursive: true, force: true }).catch(() => {})
|
|
1392
|
+
res.end('error: ' + e.message)
|
|
1393
|
+
}
|
|
1394
|
+
})()`
|
|
1395
|
+
})
|
|
1396
|
+
return await r1.text()
|
|
1397
|
+
},
|
|
1398
|
+
'true'
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1145
1401
|
}
|
|
1146
1402
|
|
|
1147
1403
|
// Export for Node.js (CommonJS)
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(node test/test.js:*)",
|
|
5
|
-
"Bash(lsof:*)",
|
|
6
|
-
"Bash(xargs kill -9)",
|
|
7
|
-
"Bash(curl:*)",
|
|
8
|
-
"Bash(cat:*)",
|
|
9
|
-
"Bash(node test.js:*)",
|
|
10
|
-
"Bash(timeout 5 node:*)",
|
|
11
|
-
"Bash(git checkout:*)",
|
|
12
|
-
"Bash(npm show:*)",
|
|
13
|
-
"Bash(npm install:*)",
|
|
14
|
-
"Bash(node:*)",
|
|
15
|
-
"Bash(git add:*)"
|
|
16
|
-
],
|
|
17
|
-
"deny": [],
|
|
18
|
-
"ask": []
|
|
19
|
-
}
|
|
20
|
-
}
|