@naanlang/naan 1.0.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.
Files changed (98) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +46 -0
  3. package/dist/env_web.js +431 -0
  4. package/dist/naan.min.js +73 -0
  5. package/frameworks/browser/browser.nlg +30 -0
  6. package/frameworks/browser/pouchdb/pouchdb.min.js +7 -0
  7. package/frameworks/browser/spark-md5/spark-md5.min.js +1 -0
  8. package/frameworks/browser/sworker.js +192 -0
  9. package/frameworks/browser/terminals.nlg +787 -0
  10. package/frameworks/browser/workers.nlg +386 -0
  11. package/frameworks/client/apiclient.nlg +183 -0
  12. package/frameworks/client/client.nlg +29 -0
  13. package/frameworks/client/psm_client.nlg +572 -0
  14. package/frameworks/common/common.nlg +464 -0
  15. package/frameworks/common/preferences.nlg +131 -0
  16. package/frameworks/common/repltools.nlg +100 -0
  17. package/frameworks/common/watching.nlg +97 -0
  18. package/frameworks/node/apiserver.nlg +399 -0
  19. package/frameworks/node/filesystem.nlg +1012 -0
  20. package/frameworks/node/gitter.nlg +234 -0
  21. package/frameworks/node/node.nlg +81 -0
  22. package/frameworks/node/psm_server.nlg +465 -0
  23. package/frameworks/node/worker.nlg +560 -0
  24. package/frameworks/project/build.nlg +1229 -0
  25. package/frameworks/project/jszip.js +30 -0
  26. package/frameworks/project/jszip.nlg +87 -0
  27. package/frameworks/project/proj_cliser.nlg +139 -0
  28. package/frameworks/project/proj_console.nlg +137 -0
  29. package/frameworks/project/proj_folder.nlg +143 -0
  30. package/frameworks/project/proj_lambda.nlg +137 -0
  31. package/frameworks/project/proj_static.nlg +138 -0
  32. package/frameworks/project/project.nlg +29 -0
  33. package/frameworks/project/projects.nlg +587 -0
  34. package/frameworks/project/uglifyjs.js +1 -0
  35. package/frameworks/project/uglifyjs.nlg +79 -0
  36. package/frameworks/running/debugcom.nlg +163 -0
  37. package/frameworks/running/debugnub.nlg +565 -0
  38. package/frameworks/running/debugutil.nlg +510 -0
  39. package/frameworks/running/executors.nlg +767 -0
  40. package/frameworks/running/running.nlg +151 -0
  41. package/frameworks/running/sourcecode.nlg +346 -0
  42. package/frameworks/running/taskexec.nlg +196 -0
  43. package/frameworks/storage/dbt_pouch.nlg +773 -0
  44. package/frameworks/storage/file_manager.nlg +772 -0
  45. package/frameworks/storage/psm.nlg +326 -0
  46. package/frameworks/storage/psm_dbtables.nlg +814 -0
  47. package/frameworks/storage/resources.nlg +445 -0
  48. package/frameworks/storage/storage.nlg +52 -0
  49. package/lib/browser/env_web.js +431 -0
  50. package/lib/browser/env_webworker.js +146 -0
  51. package/lib/browser/require.js +1653 -0
  52. package/lib/core/naanlib.js +73 -0
  53. package/lib/env_node.js +282 -0
  54. package/lib/env_nodeworker.js +165 -0
  55. package/lib/node_repl.js +25 -0
  56. package/lib/node_repl_init.nlg +52 -0
  57. package/package.json +13 -0
  58. package/plugins/gitClient/clientops.nlg +317 -0
  59. package/plugins/gitClient/gitClient.nlg +63 -0
  60. package/plugins/gitClient/gitutils.nlg +1331 -0
  61. package/plugins/gitClient/minimatch.js +945 -0
  62. package/plugins/gitClient/minimatch_license.txt +15 -0
  63. package/plugins/gitClient/minimatch_readme.md +208 -0
  64. package/plugins/nodeLib/nodeLib.nlg +26 -0
  65. package/plugins/nodeLib/node_cp.nlg +65 -0
  66. package/plugins/nodeLib/node_fs.nlg +137 -0
  67. package/plugins/nodeLib/node_https.nlg +172 -0
  68. package/plugins/nodeLib/node_util.nlg +180 -0
  69. package/plugins/serviceAws/aws/aws-sdk.min.js +88 -0
  70. package/plugins/serviceAws/aws/lambda_rest_index.js +122 -0
  71. package/plugins/serviceAws/aws/lambda_rest_init.nlg +58 -0
  72. package/plugins/serviceAws/aws/lambda_ws_index.js +498 -0
  73. package/plugins/serviceAws/aws/lambda_ws_init.nlg +49 -0
  74. package/plugins/serviceAws/aws_cloudwatchlogs.nlg +122 -0
  75. package/plugins/serviceAws/aws_cognito.nlg +76 -0
  76. package/plugins/serviceAws/aws_dynamo.nlg +509 -0
  77. package/plugins/serviceAws/aws_lambda.nlg +95 -0
  78. package/plugins/serviceAws/aws_s3.nlg +134 -0
  79. package/plugins/serviceAws/aws_utils.nlg +86 -0
  80. package/plugins/serviceAws/dbt_aws.nlg +769 -0
  81. package/plugins/serviceAws/lambda_naan_control.nlg +175 -0
  82. package/plugins/serviceAws/psm_aws.nlg +258 -0
  83. package/plugins/serviceAws/serviceAws.nlg +38 -0
  84. package/plugins/serviceGitHub/gitops.nlg +522 -0
  85. package/plugins/serviceGitHub/psm_github.nlg +205 -0
  86. package/plugins/serviceGitHub/serviceGitHub.nlg +65 -0
  87. package/plugins/serviceGitLab/gitops.nlg +528 -0
  88. package/plugins/serviceGitLab/psm_gitlab.nlg +205 -0
  89. package/plugins/serviceGitLab/serviceGitLab.nlg +65 -0
  90. package/test/harness.nlg +312 -0
  91. package/test/node_test.js +33 -0
  92. package/test/test_01_core.nlg +768 -0
  93. package/test/test_02_context.nlg +269 -0
  94. package/test/test_03_jsinterop.nlg +137 -0
  95. package/test/test_04_numerics.nlg +450 -0
  96. package/test/test_05_strings.nlg +63 -0
  97. package/test/test_06_lingoparse.nlg +500 -0
  98. package/test/test_07_lingo.nlg +623 -0
@@ -0,0 +1,1331 @@
1
+ /*
2
+ * gitutils.nlg
3
+ * gitClient
4
+ *
5
+ * Low-level utility operations for the git client.
6
+ *
7
+ * column positioning: // // !
8
+ *
9
+ * Copyright (c) 2021 by Richard C. Zulch
10
+ *
11
+ */
12
+
13
+
14
+ // testing
15
+ // fs = App.nide.locate.connect("Host", "NideFS", "/Users/richardz/temp").1;
16
+
17
+ // TO-DO items ###
18
+ // review pathsep for windows filesystem arrays
19
+ // implement revert
20
+ // implement stage and unstage
21
+ // implement gc
22
+ // check that staged files are refreshed properly
23
+ // we need a save operation to preserve the GitTree state in the .ngit folder
24
+
25
+
26
+ /*
27
+ * synctree
28
+ *
29
+ * Update existing TO tree with information in FROM tree using a callback.
30
+ *
31
+ * The callback is invoked for these cases:
32
+ *
33
+ * callback(1, newto, from, topath, frompath) - FROM node is not yet in TO, but newto will
34
+ * be inserted if the callback returns true.
35
+ *
36
+ * callback(-1, oldto, false, topath, frompath) - The oldto node is not in from, and will be
37
+ * deleted if the callback returns true.
38
+ *
39
+ * callback(0, to, from, topath, frompath) - The node exists in both trees, so update TO
40
+ * as desired. Could be folder/file swap
41
+ *
42
+ * This sorts the children into a consistent order and the original order is not restored. If a file
43
+ * is replaced with a directory or vice versa then this is detectable in callback(0), which must
44
+ * check for this case.
45
+ * When this finds a new FROM directory, it invokes the callback for the new node and then, if
46
+ * callback returns true, it invokes the callback for the existing FROM and new TO directories. This
47
+ * causes the new children to be added recursively.
48
+ * When this finds a deleted TO node, it deletes the node immediately if the callback returns
49
+ * true. Otherwise for a directory node it invokes the callback again on each of its children from
50
+ * deepest upward. If the directory becomes empty then the callback is invoked again, offering the
51
+ * opportunity to remove the directory completely.
52
+ *
53
+ */
54
+
55
+ closure synctree(to, from, callback, local fpathsep, tpathsep) {
56
+ // childcomp
57
+ //
58
+ // Compare children in tree array by name.
59
+ //
60
+ function childcomp(a, b) {
61
+ if (a.name < b.name)
62
+ -1
63
+ else if (a.name > b.name)
64
+ 1
65
+ else
66
+ 0 }
67
+
68
+ // delkids
69
+ //
70
+ // Recursively invoke the callback for the children of a deleted directory, returning true if it
71
+ // should be deleted from its parent. If called with an empty directory or anything other than a
72
+ // directory then this immediately returns false. The callback is invoked from here only if the
73
+ // directory *becomes* empty.
74
+ //
75
+ function delkids(to, local tnode, tdex) {
76
+ if !to.children.length > 0
77
+ return (false) // not a directory or already empty
78
+ tdex = 0
79
+ loop {
80
+ tnode = to.children[tdex]
81
+ if !tnode
82
+ break
83
+ if callback(-1, tnode) || delkids(tnode)
84
+ to.children.splice(tdex, 1) // remove existing node
85
+ else
86
+ ++tdex
87
+ }
88
+ if to.children.length == 0
89
+ callback(-1, to) // notify it's empty
90
+ else
91
+ false
92
+ }
93
+
94
+ // strecur
95
+ //
96
+ // Iteratively and recursively sync two directories by comparing their children by name.
97
+ //
98
+ function strecur(to, from, tpathpar, fpathpar, local fnode, tnode, fdex, tdex, fpath, tpath) {
99
+ callback(0, to, from, tpathpar, fpathpar) // update the directory pair
100
+ from.children.sort(childcomp) // ensure we are in consistent order
101
+ to.children.sort(childcomp)
102
+ fdex = tdex = 0
103
+ loop {
104
+ fnode = from.children[fdex]
105
+ tnode = to.children[tdex]
106
+ if !fnode && !tnode
107
+ break // both lists exhausted
108
+ // determine child paths
109
+ if !tnode
110
+ tpath = false
111
+ else if !tpathpar || tpathpar == ""
112
+ tpath = tnode.name
113
+ else
114
+ tpath = tpathpar.concat(tpathsep, tnode.name) // child path in to node
115
+ if !fnode
116
+ fpath = false
117
+ else if !fpathpar || fpathpar == ""
118
+ fpath = fnode.name
119
+ else
120
+ fpath = fpathpar.concat(fpathsep, fnode.name) // child path in from node
121
+ // compare to and from
122
+ if !tnode || fnode.name < tnode.name { // found new node in FROM
123
+ ++fdex // FROM advances for next time
124
+ tnode = { name: fnode.name }
125
+ if fnode.children
126
+ tnode.children = [] // empty starting point
127
+ if callback(1, tnode, fnode, tpath, fpath) {
128
+ to.children.splice(tdex++, 0, tnode) // insert new child in TO
129
+ if tnode.children
130
+ strecur(tnode, fnode, tpath, fpath) // add the children of a new directory
131
+ } }
132
+ else if !fnode || fnode.name > tnode.name { // found missing node
133
+ if callback(-1, tnode, false, tpath) || delkids(tnode)
134
+ to.children.splice(tdex, 1) // remove existing node
135
+ else
136
+ ++tdex }
137
+ else { // found matching node
138
+ strecur(tnode, fnode, tpath, fpath) // (but might be file/folder conflict)
139
+ ++fdex
140
+ ++tdex }
141
+ }
142
+ }
143
+
144
+ fpathsep = from.data.pathsep
145
+ tpathsep = to.data.pathsep
146
+ strecur(to, from, "", "")
147
+ };
148
+
149
+
150
+ /*
151
+ * GitTree
152
+ *
153
+ * Trees for the git client. This is structured like a filesystem tree with extensions so that a
154
+ * hash can be stored with each file node (by path) for the current commit, the staging area, and the
155
+ * working tree. This implements the functionality of the git index. A tree is initialized with the
156
+ * filesystem and path to the repository root within that filesystem.
157
+ * Note that a GitTree is associated with a specific filesystem, but not a specific repo. It is
158
+ * possible to fetch from one repo and commit to a different one. Also a GitTree is not a git client,
159
+ * but rather maintains the state of a particular filesystem and its relationship to commits in
160
+ * various repositories. GitTree methods are summarized here:
161
+ *
162
+ * init() - initialize the repository if needed, ensuring it has a .ngit folder
163
+ * refresh() - refresh gittree from the filesystem
164
+ * sync(repotree) - synchronize gittree from the specified commit tree, returning an
165
+ * array of obsolete file and folder paths to vacate
166
+ * vacate(obsoletes) - delete the obsolete file and folder paths from sync
167
+ * discard(filelist) - discard changes to the entire repo, or an array of paths
168
+ * fetch(filelist, readproc) - fetch any pending files using the readproc, limited to files in the
169
+ * array if provided, and without overwriting user data
170
+ * stage(filelist) - stage files in the index
171
+ * unstage(filelist) - unstage files from the index
172
+ * readstage(shastage) - read a staged file if it exists
173
+ * status(options) - report status, optionally including untracked and ignored files
174
+ * getcommit(options) - get commit requirements info without changing anything
175
+ *
176
+ --------------------------------------------------------------
177
+
178
+ WIP Notes:
179
+
180
+ 1. Essential state to save is:
181
+ - affirmatively deleted files in tree
182
+ - staged files in tree
183
+ 2. Save state after it changes (refresh/stage/unstage)
184
+ 3. Conflict resolution plan is:
185
+ - Sync with new ref/stash and fetch all that does not conflict
186
+ - Show current status vs. "repo" so user can make changes
187
+ 4. Implement gc() to remove unused files from stage folder
188
+ 5. Stash not supported directly, but can be done with:
189
+ - explicit stash branches
190
+ - cloned projects
191
+
192
+ --------------------------------------------------------------
193
+ *
194
+ * Format of the .ngit folder on the filesystem:
195
+ * tree.json - stores gittree in JSON format
196
+ * stage/
197
+ * <file-0-sha>
198
+ * <file-1-sha>
199
+ *
200
+ * The stash files contain an array of paths and hashes comprising the stashed contents.
201
+ *
202
+ */
203
+
204
+ closure GitTree(local fs, reporoot, gtree, repodir, stagedir) {
205
+ gtree = new(object, this)
206
+ repodir = ".ngit"
207
+ stagedir = "stage"
208
+
209
+ // fileHash
210
+ //
211
+ // Read a file from the filesystem, generate a hash, and then make the callback provided.
212
+ //
213
+ closure fileHash(fspath, callback) {
214
+ if !callback
215
+ return (syncAdapter(fileHash, fspath))
216
+ fs.readFile(fspath, false, function(error, strdata, local hash) {
217
+ if error
218
+ callback(error)
219
+ else
220
+ callback(false, hashSHA1("blob".concat(space, strdata.length, "\x00", strdata)))
221
+ })
222
+ }
223
+
224
+ // nodeForPathArray
225
+ //
226
+ // Return the node for the path array within a directory.
227
+ //
228
+ function nodeForPathArray(directory, paths, local node) {
229
+ if paths.length == 0
230
+ return (directory)
231
+ for node in directory.children
232
+ if node.name == paths.0
233
+ return (nodeForPathArray(node, paths.slice(1)))
234
+ false
235
+ }
236
+
237
+ // fspathForPathArray
238
+ //
239
+ // Return a filesystem path for the specified array of nodenames. This is a strange beast because
240
+ // path calculations are currently remoted (!) and this converts the array to an argument list so
241
+ // the remote is called in a way that gets forwarded. It's perhaps clever but not elegant.
242
+ //
243
+ closure fspathForPathArray(patharray) {
244
+ apply(`., list(fs.path, `join, cons(reporoot, totuple(patharray))))
245
+ }
246
+
247
+ // pathToFSpath
248
+ //
249
+ // Return the filesystem path for a tree path.
250
+ //
251
+ closure pathToFSpath(treepath) {
252
+ fspathForPathArray(treepath.split("/"))
253
+ }
254
+
255
+ // stageFSpath
256
+ //
257
+ // Return the path for the staged file having the specified hash, whether or not it exists.
258
+ //
259
+ function stageFSpath(shaStage) {
260
+ fs.path.join(gtree.repopath, stagedir, shaStage)
261
+ }
262
+
263
+ // joinode
264
+ //
265
+ // Join a tree node to a parent path using the gittree "/" path separator.
266
+ //
267
+ function joinode(parpath, node) {
268
+ if !parpath || parpath == ""
269
+ node.name
270
+ else
271
+ parpath.concat("/", node.name)
272
+ }
273
+
274
+ // fsdelnode
275
+ //
276
+ // Delete a file or directory from the filesystem, updating the node as appropriate. This returns
277
+ // an error or false. It clears out directory children on success, so it should not be used if you
278
+ // want to keep any repo-only child info. (But in that case why are you deleting it?)
279
+ //
280
+ closure delnode(node, treepath, local error, result) {
281
+ `(error, result) = fs.delete(pathToFSpath(treepath))
282
+ if !error {
283
+ if node.fsdir
284
+ node.fsdir = undefined
285
+ if node.shaFS
286
+ node.shaFS = undefined
287
+ node.children = undefined // not even repo-only children
288
+ }
289
+ error
290
+ }
291
+
292
+ // init
293
+ //
294
+ // Initialize the working tree as a repo and load any existing state information. This returns a
295
+ // standard `(error, result) tuple.
296
+ //
297
+ gtree.init = closure init(ifs, ireporoot, local error, result) {
298
+ fs = ifs
299
+ reporoot = ireporoot
300
+ gtree.ignores = GitIgnores(fs, reporoot) // manage ignores
301
+ gtree.repopath = fs.path.join(reporoot, repodir)
302
+ `(error, result) = fs.info(gtree.repopath)
303
+ if !error
304
+ `(error, result) = fs.mkdir(gtree.repopath)
305
+ if !error
306
+ `(error, result) = fs.mkdir(fs.path.join(gtree.repopath, stagedir))
307
+ if error // could not create repo folder
308
+ return (list(Error("Could not initialize local git repo folder", error)))
309
+ // ### load saved state here
310
+ list(false, { ok: true })
311
+ }
312
+
313
+ // refresh
314
+ //
315
+ // Refresh the gittree from the filesystem, returning a standard `(error, result) tuple. This
316
+ // obtains the current filesystem tree and updates the gittree from it. The cases and actions
317
+ // are as follows:
318
+ // 1. New file found. The file is added to the tree and hash update scheduled.
319
+ // 2. Modified file found. The file information is updated and hash update scheduled.
320
+ // 3. Missing file noticed. The missing file is marked missing but not removed if still in
321
+ // the repository (i.e. has a repo hash.)
322
+ // 4. Directory found with existing file. A repo hash in the existing file is copied to the
323
+ // new directory in that position, or otherwise the existing file is forgotten.
324
+ // 5. File found with existing directory. The filesystem hash in the new file is copied to
325
+ // the existing directory if in the repo, or replaces the directory otherwise.
326
+ // New and missing directories are handled recursively like new and missing files. Ignores are
327
+ // processed by the separate GitIgnores helper class.
328
+ //
329
+ gtree.refresh = closure refresh(local error, fstree, errors) {
330
+
331
+ // restage
332
+ //
333
+ // The staging area is tracked in the tree under <repodir> / <stagedir>, and a dictionary
334
+ // of stage hashes is maintained in gtree.index. This enumerates the staged files, indexes
335
+ // their hashes, and ensures that all files marked staged in the tree actually refer to a
336
+ // valid index entry.
337
+ //
338
+ function restage(directory, local stage, node) {
339
+ gtree.index = { }
340
+ stage = nodeForPathArray(gtree.gittree, [repodir, stagedir])
341
+ for node in stage.children
342
+ if node.shaFS && node.name == node.shaFS { // has a hash that matches the name
343
+ gtree.index[node.shaFS] = node // indexed
344
+ node.shaStage = node.shaFS } // used in stage()
345
+ function restager(directory, node) {
346
+ for node in directory.children
347
+ if node.children
348
+ restager(node)
349
+ else if node.shaStage && node.shaStage != "" && !gtree.index[node.shaStage]
350
+ node.shaStage = undefined // stage storage not valid
351
+ } (directory)
352
+ }
353
+
354
+ // dirRepoChildCount
355
+ // Count the transitive repo child of the specified directory node.
356
+ //
357
+ function dirRepoChildCount(directory, local count, node) {
358
+ count = 0
359
+ for node in directory.children {
360
+ if node.shaRepo
361
+ ++count
362
+ if node.children
363
+ count += dirRepoChildCount(node)
364
+ }
365
+ count
366
+ }
367
+
368
+ // updateFile
369
+ // Update a file node's hash and hash time, if needed.
370
+ //
371
+ closure updateFile(node, fspath, local error, hash, info) {
372
+ fspath = fs.path.join(reporoot, fspath)
373
+ `(error, info) = fs.info(fspath)
374
+ if error {
375
+ errors.push(error)
376
+ return (false) } // don't add the new node
377
+ if info.type == "directory" {
378
+ errors.push(Error("directory found where file expected", fspath))
379
+ return (false) } // don't add the new node
380
+ if node.shaFS_mtimeMs == info.mtimeMs
381
+ return (true) // no need to update
382
+ `(error, hash) = fileHash(fspath)
383
+ if error {
384
+ errors.push(error)
385
+ return (false) }
386
+ node.shaFS = hash
387
+ node.shaFS_mtimeMs = info.mtimeMs
388
+ node.size = info.length
389
+ return (true)
390
+ }
391
+
392
+ `(error, fstree) = fs.tree(reporoot)
393
+ if error
394
+ return (list(Error("Cannot access local repository", error)))
395
+ if !gtree.gittree
396
+ gtree.gittree = { // add an empty gittree
397
+ name: fstree.name
398
+ data: { platform: "ngit", semantics: "git", pathsep: "/" }
399
+ parents: fstree.parents
400
+ children: []
401
+ }
402
+ errors = gtree.ignores.update(fstree)
403
+ synctree(gtree.gittree, fstree, closure(change, tnode, fnode, tpath, fpath) {
404
+
405
+ if change > 0 { // new node found
406
+ if tnode.children {
407
+ tnode.fsdir = true
408
+ true } // add the new directory (which calls us back)
409
+ else
410
+ updateFile(tnode, fpath)
411
+ } else if change < 0 { // existing node deleted
412
+ if tnode.children {
413
+ tnode.fsdir = false
414
+ tnode.children.length == 0 } // remove a missing directory if empty
415
+ else if tnode.shaRepo {
416
+ if tnode.shaFS {
417
+ tnode.shaFS = "" // affirmatively deleted file
418
+ tnode.shaFS_mtimeMs = undefined // re-hash if it comes back
419
+ tnode.size = undefined }
420
+ false } // false to keep in tree
421
+ else
422
+ true // true to delete
423
+ } else { // update node
424
+ if fnode.children && !tnode.children { // file matched with filesystem directory
425
+ tnode.children = [] // (will be called back for FROM children)
426
+ tnode.fsdir = true
427
+ tnode.shaFS = undefined }
428
+ else if !fnode.children && tnode.children { // directory matched with filesystem file
429
+ if dirRepoChildCount(tnode) == 0
430
+ tnode.children = undefined // directory not in repo or filesystem
431
+ tnode.fsdir = undefined
432
+ updateFile(tnode, fpath) }
433
+ else if !fnode.children && !tnode.children { // existing file still present
434
+ updateFile(tnode, fpath) }
435
+ else {
436
+ tnode.fsdir = true } // existing directory still present
437
+ }
438
+ })
439
+ restage(gtree.gittree) // update stage state
440
+ if errors.length > 0
441
+ list(Errors("Errors during GitTree.refresh:", reporoot, errors))
442
+ else
443
+ list(false, { ok: true })
444
+ }
445
+
446
+ // sync
447
+ //
448
+ // Sync the gittree from the specified repo tree without changing the filesystem, returning a
449
+ // list of obsolete files since the last repo tree that are still in the filesystem. This copies
450
+ // node information from the repo tree (representing a commit) to the gittree and removes any
451
+ // gittree nodes that are no longer present in either the filesystem or the repo tree.
452
+ // Cases and actions are as follows:
453
+ // 1. New repo file found. It is added to the tree with the repo hash.
454
+ // 2. Existing repo file found. The repo hash is updated.
455
+ // 3. File only in filesystem. If the file was previously in the repo and unchanged, then
456
+ // add to the obsolete list. Otherwise remove any repo hash.
457
+ // 4. File not in filesystem or repo. Remove the tree entry completely.
458
+ // 5. Add newly-empty directories to the obsolete list.
459
+ // 6. Repo file would replace filesystem folder. Directory node marked with repo hash and
460
+ // thus repo file will be considered missing.
461
+ // 7. Repo folder would replace filesystem file. Convert node to directory if needed but
462
+ // keep filesystem hash present, and add repo children (which will appear missing.)
463
+ // Files reported in the obsoletes array are those that were previously fetched from a repo but
464
+ // were unchanged on the filesystem, or are an empty directory that is not in the repo.
465
+ //
466
+ gtree.sync = closure sync(repotree, local obsoletes) {
467
+ obsoletes = []
468
+ synctree(gtree.gittree, repotree, function(change, tnode, fnode, tpath, fpath) {
469
+ if change > 0 { // new repo file found
470
+ if tnode.children
471
+ true // add a repo directory (which calls us back)
472
+ else {
473
+ tnode.shaRepo = fnode.hash
474
+ tnode.repoLength = fnode.size
475
+ // ### convert mode to type
476
+ true } }
477
+ else if change < 0 { // existing node not in repo
478
+ if tnode.children {
479
+ if tpath == repodir
480
+ return (false) // ignore and keep
481
+ if tnode.children.length == 0 {
482
+ obsoletes.push(tpath) // remove empty directories from tree
483
+ return (true) }
484
+ return (false) } // must have a child file we want
485
+ if tnode.shaRepo { // file previously in repo
486
+ if tnode.shaRepo == tnode.shaFS
487
+ obsoletes.push(tpath) // unchanged previously-fetched file
488
+ tnode.shaRepo = undefined
489
+ false // in repo, keep in tree
490
+ } else if tnode.shaFS
491
+ false // in filesystem, so keep in tree
492
+ else
493
+ true } // not in filesystem or repo, so remove
494
+ else { // still in repo
495
+ if tnode.children {
496
+ if fnode.children
497
+ tnode.repodir = true
498
+ else // trying to replace folder with file
499
+ debuglog("GitTree.sync: cannot replace folder with file", tpath, "<-", fpath)
500
+ }
501
+ else if fnode.children { // trying to replace file with folder
502
+ if tnode.shaRepo == tnode.shaFS
503
+ obsoletes.push(tpath) // unchanged previously-fetched file
504
+ else
505
+ debuglog("GitTree.sync: cannot replace file with folder", tpath, "<-", fpath)
506
+ }
507
+ else { // files correspond (may be changed)
508
+ tnode.shaRepo = fnode.hash
509
+ tnode.repoLength = fnode.size
510
+ }
511
+ }
512
+ })
513
+ obsoletes
514
+ }
515
+
516
+ // vacate
517
+ //
518
+ // Delete from the filesystem a list of obsolete files and directories as returned by sync above.
519
+ // Each item is a pathname, representing either a file or a directory. This will try to delete
520
+ // all items in the list even if errors occur, in which case an array of the errors is returned
521
+ // as an error.
522
+ // The sync method has already removed the obsolete directory entries from gittree, so we do
523
+ // not need to do so here. If a node fails to delete then it will be discovered and restored on
524
+ // the next refresh.
525
+ // This does not work in parallel because deleting children before parents would be complex,
526
+ // and this really should be checking for directories and deleting them deeply instead. That
527
+ // would require trimming the paths to remove any stray children that should not actually be
528
+ // there, but then it could use asyncArray and process the deletions in parallel. However I have
529
+ // decided to punt these improvements for now instead.
530
+ //
531
+ gtree.vacate = closure vacate(obsoletes, local path, errors) {
532
+ errors = []
533
+ obsoletes.sort().reverse() // ensure children precede parents
534
+ for path in obsoletes
535
+ if error = delnode(path)
536
+ errors.push(error)
537
+ if errors.length > 0
538
+ list(Error("Errors during GitTree.vacate:", reporoot, errors))
539
+ else
540
+ list(false, { ok: true })
541
+ }
542
+
543
+ // discard
544
+ //
545
+ // Discard changed files and unneeded directories in the filesystem relative to the last synced
546
+ // repo. If a filelist array is provided then only the specified paths are discarded, and only
547
+ // if needed. If tostate is true then this only discards files that are staged. Otherwise this
548
+ // disards any files different from the repo. This returns a `(error, tofetch) tuple where the
549
+ // tofetch value is an array of files to be subsequently fetched.
550
+ // Use this after a refresh and sync, and before a fetch, to revert a set of files back to
551
+ // their state in the repo. This will never delete untracked files, but it will discard changes
552
+ // made to the filesystem in tracked files.
553
+ // The caller should call refresh before discarding to ensure that all file information is
554
+ // updated. Note that this does not replace the discarded files; that is done by calling fetch
555
+ // below. To restore a staged file instead of a repo call the readstage(hash) method in the
556
+ // fetch callback.
557
+ //
558
+ gtree.discard = closure discard(filelist, tostage, local tofetch, errors, indexFilelistPaths) {
559
+ tofetch = []
560
+ errors = []
561
+
562
+ // inFilelist
563
+ // Report true if the specified path is in the filelist or there is no filelist
564
+ function inFilelist(path) {
565
+ if !filelist
566
+ return // true
567
+ if !indexFilelistPaths {
568
+ indexFilelistPaths = { }
569
+ for item in filelist // make an index of file paths to include
570
+ indexFilelistPaths[item] = true
571
+ }
572
+ indexFilelistPaths[path]
573
+ }
574
+
575
+ // testrep
576
+ // True iff the node should be replaced.
577
+ function testrep(node) {
578
+ if node.children
579
+ tostage && node.shaStage || !tostage && node.shaRepo
580
+ else
581
+ tostage && node.shaStage && node.shaStage != node.shaFS || !tostage && node.shaRepo && node.shaRepo != node.shaFS
582
+ }
583
+
584
+ // delrecur
585
+ // Recursively delete files and folders that we neeed to overwrite, returning true to keep
586
+ // the directory.
587
+ function delrecur(directory, parpath, local node, path, error, keeparent) {
588
+ for node in directory.children {
589
+ path = joinode(parpath, node)
590
+ if node.children { // is a directory
591
+ if !delrecur(node, path) || testrep(node) && inFilelist(path) {
592
+ if node.fsdir // not needed or being replaced by file in filelist
593
+ if error = delnode(node, path)
594
+ errors.push(error)
595
+ tofetch.push(path) }
596
+ else
597
+ keeparent = true } // needed, and either not being replaced or not in filelist
598
+ else {
599
+ keeparent = true
600
+ if testrep(node) && inFilelist(path) { // tracked and different
601
+ if node.shaFS == ""
602
+ node.shaFS = undefined // was intentionally deleted, now missing
603
+ else if error = delnode(node, path) // remove file to be overwritten
604
+ errors.push(error)
605
+ tofetch.push(path) } }
606
+ }
607
+ keeparent
608
+ }
609
+
610
+ delrecur(gtree.gittree, "")
611
+ if errors.length > 0
612
+ list(Error("Errors during GitTree.discard:", reporoot, errors))
613
+ else
614
+ list(false, tofetch)
615
+ }
616
+
617
+ // fetch
618
+ //
619
+ // Fetch files using the specified readproc, returning a standard `(error, result) tuple. This
620
+ // traverses the tree and looks for files that are not in the working tree and not marked
621
+ // intentionally deleted. New directories are created as needed before the files are downloaded.
622
+ // This prefers to use local data instead of fetching from a repo. If a filelist is provided then
623
+ // it's an array of pathnames that limit the files actually fetched. The readproc is called as
624
+ // follows:
625
+ //
626
+ // tree.fetch(paths[], closure(shaRepo, shaStage, treepath) { // closure: will be called async
627
+ // repo.showfile(shaRepo, ref) // returns `(error, content)
628
+ // }
629
+ //
630
+ // Note that fetch does not replace existing files or directories on the filesystem! It only
631
+ // fetches files that should be there and can be copied without overwriting anything. Call the
632
+ // discard method if you want the filesytem to be overwritten to reflect the repo contents.
633
+ // The plumbing is a bit complex so here is an overview. This first marks each directory in
634
+ // the gittree as "needed" if they may not exist but contain at least one transitive child to be
635
+ // fetched. Next this traverses the tree to add work items to create directories or fetch files.
636
+ // This creates a nonce for each directory being created so that children can wait for that
637
+ // before proceeding with their own work. Files that are already in the filesystem are indexed
638
+ // so that any needed duplicates can find them. Files that should be fetched are stored as work
639
+ // items and the first instance of each hash is indexed.
640
+ // Next the fetch items in the worklist are updated to preferably copy a file from a prior
641
+ // work item, else copy from the filesystem or fetch from the repo. When a prior work item is
642
+ // found then it is linked as the source and given a nonce to indicate when it's ready. When an
643
+ // existing filesystem file is located then it's given a filesystem path to read.
644
+ // Finally the list of workitems is executed asynchronously. For each item, if a directory
645
+ // parent is referenced then it waits for completion. Then either the directory is created or the
646
+ // the file is read from the filesystem or -- after waiting until it's ready -- the content is
647
+ // copied from the source. After the content is avaialble it is written to the destination and
648
+ // any waiting work items signaled. Nonces are signaled true on success or false to indicate an
649
+ // error occurred, in which case dependent work items are abandoned without error reports.
650
+ //
651
+ // Work item fields:
652
+ // {
653
+ // path - tree path for directory to create or file to write
654
+ // node - node being fetched
655
+ // mkdir - true for make a directory, otherwise fetch a blob
656
+ // ready - if present, a nonce to signal success (true) or error (false)
657
+ // dirwait - if present, pointer to nonce of parent directory being created
658
+ // source - if present, the first work item having the same sha
659
+ // fscopy - if present, the fs path of a file with the same sha
660
+ // }
661
+ //
662
+
663
+ gtree.fetch = closure fetch(filelist, readproc, local errors, worklist, indexFS, indexRepo, indexDirs, indexFilelistPaths) {
664
+ errors = []
665
+ worklist = [] // list of blobs to fetch
666
+ indexFS = { } // index of available files on filesystem by SHA
667
+ indexRepo = { } // first fetch of a blob in worklist
668
+ indexDirs = { } // directories being created
669
+
670
+ // inFilelist
671
+ // Report true if the specified path is in the filelist or there is no filelist
672
+ function inFilelist(path) {
673
+ if !filelist
674
+ return // true
675
+ if !indexFilelistPaths {
676
+ indexFilelistPaths = { }
677
+ for item in filelist // make an index of file paths to include
678
+ indexFilelistPaths[item] = true
679
+ }
680
+ indexFilelistPaths[path]
681
+ }
682
+
683
+ // needrecur
684
+ // Recursively mark directories that are needed on the filesystem because they contain files
685
+ // that we will (attempt to) fetch.
686
+ function needrecur(directory, parpath, local path, needed, have) {
687
+ for node in directory.children {
688
+ path = joinode(parpath, node)
689
+ if node.children {
690
+ if node.fsdir
691
+ have = true // was found on disk
692
+ if needrecur(node, path)
693
+ needed = true }
694
+ else if node.shaFS
695
+ have = true // we have a file child, so we must already exist
696
+ else if node.shaRepo && node.shaFS != "" && inFilelist(path)
697
+ needed = true
698
+ }
699
+ directory.needed = needed && !have
700
+ }
701
+
702
+ // fetchrecur
703
+ // Recursive fetch planner. This visits our gittree and creates a work item for any files
704
+ // that need to be fetched.
705
+ function fetchrecur(directory, parpath, local parwork, dirwork, node, path, filework) {
706
+ parwork = indexDirs[parpath] // non-false if directory being created
707
+ for node in directory.children {
708
+ path = joinode(parpath, node)
709
+ if node.children {
710
+ if node.needed { // directory needed for children
711
+ node.needed = false
712
+ dirwork = {
713
+ mkdir: true
714
+ path: path
715
+ ready: new(nonce)
716
+ }
717
+ if parwork
718
+ dirwork.dirwait = parwork.ready // wait for parent to be created
719
+ indexDirs[path] = dirwork
720
+ worklist.push(dirwork)
721
+ }
722
+ fetchrecur(node, path)
723
+ }
724
+ else {
725
+ if node.shaFS
726
+ indexFS[node.shaFS] = path // where to get this file, in case someone cares
727
+ else if node.shaRepo && node.shaFS != "" && inFilelist(path) {
728
+ filework = {
729
+ path: path // where to write this file
730
+ node: node
731
+ }
732
+ if parwork
733
+ filework.dirwait = parwork.ready // must wait for directory creation
734
+ if !indexRepo[node.shaRepo]
735
+ indexRepo[node.shaRepo] = filework // first work item for this hash
736
+ worklist.push(filework)
737
+ }
738
+ }
739
+ }
740
+ }
741
+
742
+ let (item, source) {
743
+ needrecur(gtree.gittre, "") // mark needed directories
744
+ fetchrecur(gtree.gittree, "") // generate worklist
745
+ for item in worklist {
746
+ if item.mkdir
747
+ continue
748
+ source = indexRepo[item.node.shaRepo]
749
+ if source != item {
750
+ if !source.ready
751
+ source.ready = new(nonce) // signaled when complete
752
+ item.source = source
753
+ } else if indexFS[item.node.shaRepo]
754
+ item.fscopy = indexFS[item.node.shaRepo] // get the file from the filesystem
755
+ }
756
+ asyncArray(worklist, 10, closure workitem(item, local error, result, content) {
757
+ if item.dirwait && !item.dirwait.wait()
758
+ return // waited for parent directory creation, but it failed so give up
759
+ if item.mkdir
760
+ `(error, result) = fs.mkdir(pathToFSpath(item.path)) // create the parent directory
761
+ else {
762
+ if item.fscopy // get local copy
763
+ `(error, content) = fs.readFile(pathToFSpath(item.fscopy))
764
+ else if item.source {
765
+ if !item.source.ready.wait()
766
+ return // waited for source to fetch, but it failed so give up
767
+ error = false
768
+ content = source.content }
769
+ else // otherwise fetch blob content
770
+ `(error, content) = readproc(item.node.shaRepo, item.node.shaStage, item.path)
771
+ if !error {
772
+ if item.ready
773
+ item.content = content // keep content as source for others
774
+ `(error, result) = fs.writeFile(pathToFSpath(item.path), content) // write blob content to filesystem
775
+ }
776
+ }
777
+ if item.ready
778
+ item.ready.signal(!error) // true iff successful
779
+ if error
780
+ errors.push(error)
781
+ else
782
+ item.node.shaFS = item.node.shaRepo // now in working tree
783
+ }).wait()
784
+ if errors.length > 0
785
+ list(Error("Errors during GitTree.fetch:", reporoot, errors))
786
+ else
787
+ list(false, { ok: true, worklist: worklist })
788
+ } ()
789
+ }
790
+
791
+ // stage
792
+ //
793
+ // Stage the specified files by ensuring they exist in the staging area and adding a shaStage
794
+ // hash to their gittree entries. This accepts an array of pathnames with "/" path separators
795
+ // and returns a `(error, result) tuple.
796
+ // A tree node tracks staging status in shaStage, which is undefined for not staged, a hash
797
+ // for staged added/modified, and "" for staged removal. Ignored files are never added, even if
798
+ // explicitly listed.
799
+ // The caller should call refresh before staging to ensure that all file information is
800
+ // updated, and at some point afterward to capture all the new staging files. This checks if a
801
+ // newly staged file has the last hash recorded, and if not then this reports an error.
802
+ //
803
+ gtree.stage = closure stage(filelist, local errors, worklist, hashes, path, patharray, stage, item) {
804
+ //
805
+ // phase 1 = create list of files to copy to staging area
806
+ //
807
+ errors = []
808
+ worklist = []
809
+ hashes = new(gtree.index)
810
+ for path in filelist {
811
+ patharray = path.split("/")
812
+ if patharray && (stage = nodeForPathArray(gtree.gittree, patharray)) {
813
+ if stage.shaFS {
814
+ item = { // staged copy
815
+ path: path
816
+ patharray: patharray
817
+ stage: stage
818
+ exists: hashes[stage.shaFS]
819
+ }
820
+ if !item.exists
821
+ hashes[stage.shaFS] = stage // file already in staging area, or will be
822
+ worklist.push(item) }
823
+ else
824
+ stage.shaStage = "" } // staged removal
825
+ else
826
+ errors.push(Error("path not found:", path))
827
+ }
828
+ //
829
+ // phase 2 - copy all the files and verify they ended with correct hash
830
+ //
831
+ asyncArray(worklist, 10, closure stagecopy(item, local srcpath, destpath, error, result) {
832
+ if item.exists
833
+ return
834
+ srcpath = fspathForPathArray(item.patharray)
835
+ destpath = stageFSpath(item.stage.shaFS)
836
+ `(error, result) = fs.copy(srcpath, destpath)
837
+ if !error
838
+ `(error, result) = fileHash(destpath) // check if the source file changed
839
+ if !error && result != item.stage.shaFS {
840
+ `(error, result) = list(Error("file changing:", srcpath))
841
+ fs.delete(destpath) // try to eliminate the wrongly-named file
842
+ }
843
+ if error
844
+ errors.push(error)
845
+ else
846
+ item.stage.shaStage = item.stage.shaFS
847
+ }).wait()
848
+ //
849
+ // phase 3 - update stage status for already-copied files
850
+ //
851
+ for item in worklist
852
+ if item.exists {
853
+ if item.exists.shaStage
854
+ item.stage.shaStage = item.exists.shaStage // either already staged or we copied successfully
855
+ else
856
+ errors.push(Error("file missing from staging area", item.path))
857
+ }
858
+ //
859
+ // phase 4 - return results
860
+ //
861
+ if errors.length > 0
862
+ list(Error("Errors during GitTree.stage:", reporoot, errors))
863
+ else
864
+ list(false, { ok: true })
865
+ }
866
+
867
+ // unstage
868
+ //
869
+ // Unstage the specified files by removing their tree entry pointing to the staging area. The
870
+ // files in the staging area may have multiple references and are removed by garbage collection
871
+ // below. This accepts an array of pathnames with "/" path separators and returns a standard
872
+ // `(error, result) tuple without performing i/o. It is not an error to unstage a file that is
873
+ // not actually staged.
874
+ //
875
+ gtree.unstage = closure unstage(filelist, local path, patharray, stage) {
876
+ for path in filelist {
877
+ patharray = path.split("/")
878
+ if patharray && (stage = nodeForPathArray(gtree.gittree, patharray))
879
+ stage.shaStage = undefined
880
+ }
881
+ list(false, { ok: true })
882
+ }
883
+
884
+ // readstage
885
+ //
886
+ // Read a staged file with the specified hash, returning a standard `(error, content) tuple.
887
+ //
888
+ gtree.readstage = closure readstage(shaStage) {
889
+ fs.readFile(stageFSpath(shaStage))
890
+ }
891
+
892
+ // status
893
+ //
894
+ // Report the status of the working tree with respect to the last filesystem refresh and last
895
+ // synced commit. This returns an array of files having git status. Each element in the array has
896
+ // the following structure:
897
+ // {
898
+ // status: <status>, // " M", "AM", "??", etc.
899
+ // path: <path>
900
+ // }
901
+ // The path is relative to the reporoot specified when the GitTree was created. The actual
902
+ // status values are two letters for the staging area and the working tree respectively:
903
+ // " " - unmodified
904
+ // ? - untracked
905
+ // ! - ignored
906
+ // M - modified
907
+ // A - added
908
+ // D - deleted
909
+ // H - same as head (only for working tree when modification staged)
910
+ //
911
+ // Options available are (with default values):
912
+ // {
913
+ // unchanged: false // show status of unchanged files
914
+ // ignored: false // show status of ignored files and directories
915
+ // untracked: true // show status of untracked files
916
+ // }
917
+ //
918
+ gtree.status = function status(options, local statuslist) {
919
+ if !options
920
+ options = { untracked: true }
921
+ statuslist = []
922
+
923
+ // status1
924
+ // determine a status by comparing sha values
925
+ function status1(shaGit, shaOther, shaHead) {
926
+ if shaGit == shaOther
927
+ " " // unchanged
928
+ else if shaOther == "" || !shaOther
929
+ "D" // deleted
930
+ else if shaGit {
931
+ if shaHead && shaOther == shaHead
932
+ "H" // same as head
933
+ else
934
+ "M" } // modified
935
+ else
936
+ "A" // added
937
+ }
938
+
939
+ // statusrecur
940
+ // recursively examine tree for status
941
+ function statusrecur(directory, parpath, local node, statusLeft, statusRight) {
942
+ for node in directory.children {
943
+ statusLeft = statusRight = false // default is not listed
944
+ if node.ignored {
945
+ if options.ignored
946
+ statusLeft = statusRight = "!" // directory or file ignored
947
+ }
948
+ else if node.children
949
+ statusrecur(node.children) // if not ignored directory is unlisted
950
+ else {
951
+ // left compares repo with stage
952
+ if node.shaStage
953
+ statusLeft = status1(node.shaRepo, node.shaStage)
954
+ else if node.shaRepo
955
+ statusLeft = " " // not staged but in repo
956
+ else if options.untracked && node.shaFS
957
+ statusLeft = "?"
958
+ // right compares (stage or repo) with filesystem
959
+ if node.shaStage
960
+ statusRight = status1(node.shaStage, node.shaFS, node.shaRepo)
961
+ else if node.shaRepo
962
+ statusRight = status1(node.shaRepo, node.shaFS)
963
+ else if options.untracked && node.shaFS
964
+ statusRight = "?"
965
+ }
966
+ if statusLeft && statusRight && (options.unchanged || statusLeft != " " || statusRight != " ")
967
+ statuslist.push({ // status is reportable
968
+ path: joinode(parpath, node)
969
+ status: strcat(statusLeft, statusRight)
970
+ })
971
+ }
972
+ }
973
+ statusrecur(gtree.gittree, "")
974
+ statuslist
975
+ }
976
+
977
+ // getcommit
978
+ //
979
+ // Determine requirements for a commit of the current state of the gittree relative to the last
980
+ // sync with a repo, i.e. what files are to be added/deleted/modified/moved and what the new
981
+ // repo tree will look like.
982
+ // This is difficult functionality to generalize because the various git-based APIs out in
983
+ // the world all work a bit differently. GitHub wants either a new tree or a list of files to
984
+ // add or change. GitLab wants a list of changed files with actions. AWS wants a list of changes
985
+ // in each action category like put or delete. The solution chosen here is to return both a new
986
+ // tree representing the commit and a list of actions describing the changes since the current
987
+ // head. Here is the layout of the return data structure:
988
+ //
989
+ // {
990
+ // tree: {
991
+ // name: <repo name>
992
+ // data: { semantics: "git", pathsep: "/" [, platform: "GitHub"] }
993
+ // children: [{
994
+ // name: <file-or-folder name>
995
+ // // files:
996
+ // mode: <file mode>
997
+ // hash: <git SHA1 hash of the content>
998
+ // // directories:
999
+ // children: <array of child nodes>
1000
+ // } ...]
1001
+ // }
1002
+ // changes: [{
1003
+ // action: <name of action: ADD | DELETE | MODIFY | MOVE>
1004
+ // path: <path within repo>
1005
+ // prevpath: <previous path within repo, only for MOVE>
1006
+ // hash: <hash of content, except for DELETE>
1007
+ // source: <path to read file from fs, except for DELETE>
1008
+ // } ...]
1009
+ // }
1010
+ //
1011
+ // Options controls operation as follows:
1012
+ //
1013
+ // {
1014
+ // staged: <true to commit just staged changes, otherwise just filesystem changes>
1015
+ // moveok: <true iff MOVE is an allowed action, otherwise ADD and DELETE are used>
1016
+ // }
1017
+ //
1018
+ //
1019
+ gtree.getcommit = function getcommit(options, local output) {
1020
+ if !options
1021
+ options = {
1022
+ staged: true
1023
+ moveok: true
1024
+ }
1025
+
1026
+ // comrecur
1027
+ // Recursively copy portions of tree that should be in commit.
1028
+ function comrecur(input, parpath, chout, local node, nodout, path, action) {
1029
+ for node in input {
1030
+ nodout = { name: node.name }
1031
+ path = joinode(parpath, node)
1032
+ if node.children { // add any children below
1033
+ nodout.children = []
1034
+ comrecur(node, nodout.children)
1035
+ }
1036
+ else if staged { // take staged changes
1037
+ if node.shaStage { // in staging area
1038
+ if node.shaStage == "" // staged removal
1039
+ output.actions.push({
1040
+ verb: "DELETE"
1041
+ path: path
1042
+ hash: node.shaRepo
1043
+ })
1044
+ else if node.shaRepo != node.shaStage { // stage is different from repo
1045
+ nodout.hash = node.shaStage
1046
+ output.actions.push({
1047
+ action: if node.shaRepo "MODIFY" else "ADD"
1048
+ path: path
1049
+ hash: node.shaStage
1050
+ source: stageFSpath(node.shaStage) })
1051
+ } else if node.shaRepo
1052
+ nodout.hash = node.shaRepo // in repo but stage doesn't have change
1053
+ } else if node.shaRepo
1054
+ nodout.hash = node.shaRepo // in repo but not staged
1055
+ }
1056
+ else if node.shaRepo { // take filesystem changes (can't ADD)
1057
+ if node.shaFS { // still on filesystem
1058
+ nodout.hash = node.shaFS
1059
+ if node.shaRepo != node.shaFS
1060
+ output.actions.push({
1061
+ verb: "MODIFY"
1062
+ path: path
1063
+ hash: node.shaStage
1064
+ source: path
1065
+ })
1066
+ }
1067
+ else // not on filesystem
1068
+ output.actions.push({
1069
+ verb: "DELETE"
1070
+ path: path
1071
+ hash: node.shaRepo
1072
+ })
1073
+ }
1074
+ if nodout.hash || nodout.children.length > 0
1075
+ chout.push(nodout) // node still in commit
1076
+ }
1077
+ }
1078
+
1079
+ // matchToMove
1080
+ // Change Add/Delete pairs to Move actions.
1081
+ function matchToMove(local indexAction, action, sha, actions, mdex) {
1082
+ indexAction = { } // arrays of actions on same SHA, keyed by SHA value
1083
+ for action in output.actions
1084
+ if indexAction[action.hash]
1085
+ indexAction[action.hash].push(action)
1086
+ else
1087
+ indexAction[action.hash] = [action]
1088
+ for sha in indexAction {
1089
+ actions = indexAction[sha]
1090
+ for action in actions {
1091
+ if action.verb == "ADD" {
1092
+ mdex = actions.findIndex(function(action) {
1093
+ action.verb == "DELETE" // look for a matching source
1094
+ })
1095
+ if mdex >= 0 {
1096
+ action.verb = "MOVE"
1097
+ action.prevpath = actions[mdex].path
1098
+ actions[mdex].verb = "DEAD" // don't use this again
1099
+ }
1100
+ }
1101
+ }
1102
+ }
1103
+ output.actions = output.actions.filter(function(action) {
1104
+ action.verb != "DEAD" // remove the dead items
1105
+ })
1106
+ }
1107
+
1108
+ // perform the analysis
1109
+ output = {
1110
+ tree: {
1111
+ name: gtree.gittree.name
1112
+ data: {
1113
+ semantics: "git"
1114
+ pathsep: "/" }
1115
+ children: [] }
1116
+ changes: []
1117
+ }
1118
+ comrecur(gtree.gittree, parpath, output.tree.children)
1119
+ if options.moveok
1120
+ matchToMove()
1121
+ output
1122
+ }
1123
+
1124
+ // gc
1125
+ //
1126
+ // Garbage-collect the staging area, removing any extra files no longer referenced from gittree.
1127
+ // This returns an `(error, result) tuple.
1128
+ //
1129
+ gtree.gc = closure gc() {
1130
+ // ### not implemented
1131
+ }
1132
+
1133
+ // finis
1134
+
1135
+ gtree
1136
+ };
1137
+
1138
+
1139
+ /*
1140
+ * GitIgnores
1141
+ *
1142
+ * column positioning: // // !
1143
+ * Maintain ignore pattern lists for a directory tree. The update method searches a tree for
1144
+ * .gitignore files, reads any it sees, loads the rules, and applies them in priority order to set
1145
+ * ignore flags in the tree nodes. A file cache preserves the patterns to optimize processing, so
1146
+ * different tree instances for the same directory structure can be used without penalty.
1147
+ * Rules apply to the directory in which their .gitignore file appears, including all sub-
1148
+ * directories. Thus any given directory has a set of applicable ignore rules, comprised of the rules
1149
+ * from the current and parent directories in deepest-first order. Directories are visited top-down,
1150
+ * with the ruleset adjusted for any local additions. Each child of the directory is tested against
1151
+ * the ruleset, in last-first, bottom-to-top order. Rules can be normal or negated, and as soon as a
1152
+ * rule is found that matches the child, then processing is stopped and the child is ignored or not.
1153
+ * After all children are processed then non-ignored directory children are processed recursively,
1154
+ * possibly with revised rulesets based on ignore files deeper in the tree.
1155
+ * The rules cache operates by storing the rules defined by each file, along with the file path
1156
+ * and modify date. When an ignore file is found then the cache is used if it exists with the same
1157
+ * modify timestamp. Otherwise the ignore file is re-read, the rules rebuilt, and the result stored
1158
+ * in the cache. Each time an ignore file is encounterd a found flag is marked. After the tree is
1159
+ * processed, any cached rulesets without the found flag are removed.
1160
+ *
1161
+ */
1162
+
1163
+ closure GitIgnores(fs, reporoot, local ignores, pathsep) {
1164
+ ignores = new(object, this)
1165
+ ignores.fileCache = { } // maps paths to rulesets
1166
+
1167
+ // clearCacheFlags
1168
+ // Clear the found flags from all cached ignore files.
1169
+ //
1170
+ function clearCacheFlags(local path, file) {
1171
+ for `(path, file) in ignores.fileCache
1172
+ file.found = false
1173
+ }
1174
+
1175
+ // reclaimCacheEntries
1176
+ // Reclaim space by removing unfound cache entries.
1177
+ //
1178
+ function reclaimCacheEntries(local path, file) {
1179
+ for `(path, file) in ignores.fileCache
1180
+ if !file.found
1181
+ ignores.fileCache[path] = undefined
1182
+ }
1183
+
1184
+ // findRulefiles
1185
+ // Find any rule files in the tree and queue them for asynchronous processing.
1186
+ function findRulefiles(directory, queue, parpath, local node, nodepath) {
1187
+ for node in directory.children {
1188
+ nodepath = parpath.concat(pathsep, node.name) // path to this node
1189
+ if node.children
1190
+ findRulefiles(node, queue, nodepath)
1191
+ else if node.name == ".gitignore"
1192
+ queue.push({ // this one needs to be processed
1193
+ node: node
1194
+ path: nodepath
1195
+ parent: directory
1196
+ })
1197
+ }
1198
+ }
1199
+
1200
+ // compileRules
1201
+ // Compile the specified array of rule file lines into an array of minimatch rules, and store
1202
+ // them in the specified destination dictionary. A possibly-empty array of errors is returned.
1203
+ //
1204
+ function compileRules(lines, dest, path, local errors, rules, rule) {
1205
+ errors = []
1206
+ rules = []
1207
+ for `(line, ldex) in lines {
1208
+ line = line.trim()
1209
+ if line == "" || line.startsWith("#")
1210
+ continue
1211
+ rule = xnew(js.w.MinimatchLib.Minimatch, line, { matchBase: true, flipNegate: true })
1212
+ if rule.makeRe
1213
+ rules.push(rule)
1214
+ else
1215
+ errors.push(Error("invalid regular expression: ".concat(path, ":", ldex+1), {
1216
+ path: path
1217
+ line: ldex+1
1218
+ pattern: line
1219
+ }))
1220
+ }
1221
+ dest.rules = rules
1222
+ errors
1223
+ }
1224
+
1225
+ // updateRulefile
1226
+ // Update an ignore file's ruleset in the cache, return a possibly-empty error array.
1227
+ closure updateRulefile(node, path, parent, local ruleFile, error, info, lines) {
1228
+ path = fs.path.join(reporoot, path)
1229
+ `(error, info) = fs.info(path)
1230
+ if error || info.type == "directory"
1231
+ return (error) // not valid
1232
+ if !(ruleFile = ignores.fileCache[path])
1233
+ ruleFile = ignores.fileCache[path] = { }
1234
+ ruleFile.found = true
1235
+ if ruleFile.mtimeMs == info.mtimeMs
1236
+ return (false) // no need to update
1237
+ ruleFile.mtimeMs = info.mtimeMs // don't retry on error
1238
+ `(error, lines) = fs.readLines(path)
1239
+ if error
1240
+ return (error)
1241
+ compileRules(lines, ruleFile, path)
1242
+ }
1243
+
1244
+ // ignoredByRule
1245
+ //
1246
+ // True iff the pathname is ignored by the rules. What's tricky here is that rule.match() returns
1247
+ // true if the pattern matches, which short-circuits rule execution. But if the rule is negated
1248
+ // then the match says keep the file, which returns false.
1249
+ //
1250
+ function ignoredByRule(path, rules, local rdex, rule) {
1251
+ rdex = rules.length
1252
+ while --rdex >= 0 {
1253
+ rule = rules[rdex]
1254
+ if rule.match(path)
1255
+ return (!rule.negate) // it matches, and negate says to keep
1256
+ }
1257
+ return (false)
1258
+ }
1259
+
1260
+ // applyRules
1261
+ // Apply the updated rules to the tree in priority order.
1262
+ function applyRules(directory, rules, parpath, local node, nodepath, noderules) {
1263
+ for node in directory.children { // first pass: set ignore flags in our children
1264
+ nodepath = parpath.concat(pathsep, node.name)
1265
+ if ignoredByRule(nodepath, rules)
1266
+ node.ignored = true
1267
+ else
1268
+ node.ignored = undefined
1269
+ }
1270
+ for node in directory.children // second pass: process subdirectories
1271
+ if node.children {
1272
+ if node.ignored
1273
+ function ignorekids(directory, local node) { // ignored directories are always ignored deep
1274
+ for node in directory.children {
1275
+ node.ignored = true
1276
+ if node.children
1277
+ ignorekids(node)
1278
+ }
1279
+ } (node)
1280
+ else {
1281
+ nodepath = parpath.concat(pathsep, node.name) // path to this node
1282
+ noderules = ignores.fileCache[nodepath].rules
1283
+ if noderules
1284
+ noderules = rules.concat(noderules)
1285
+ else
1286
+ noderules = rules
1287
+ applyRules(node, noderules, nodepath)
1288
+ }
1289
+ }
1290
+ }
1291
+
1292
+ // update
1293
+ //
1294
+ // Update our ignore state, including reading ignore files, generating rules, setting flags, and
1295
+ // and updating the cache as required. This returns a possibly-empty array of errors.
1296
+ //
1297
+ ignores.update = closure update(tree, local errors, queue) {
1298
+ pathsep = from.data.pathsep
1299
+ errors = []
1300
+ queue = []
1301
+ findRulefiles(tree, queue, "")
1302
+ asyncArray(queue, 10, closure(file, local errlist) {
1303
+ errlist = updateRulefile(file.node, file.path, file.parent)
1304
+ if errlist
1305
+ errors = errors.concat(errlist)
1306
+ }).wait()
1307
+ applyRules(tree, ignores.fileCache[""].rules, "")
1308
+ errors
1309
+ }
1310
+
1311
+ // finis
1312
+ ignores
1313
+ };
1314
+
1315
+
1316
+ /*
1317
+ * gcutInit
1318
+ *
1319
+ * Initialize the gitops module.
1320
+ *
1321
+ */
1322
+
1323
+ function gcutInit(local manifest) {
1324
+
1325
+ manifest = `(synctree, GitTree, GitIgnores, gcutInit)
1326
+
1327
+ Naan.module.build(module.id, "gitutils", function(modobj, compobj) {
1328
+ compobj.manifest = manifest
1329
+ })
1330
+
1331
+ }();