@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.
- package/LICENSE.md +21 -0
- package/README.md +46 -0
- package/dist/env_web.js +431 -0
- package/dist/naan.min.js +73 -0
- package/frameworks/browser/browser.nlg +30 -0
- package/frameworks/browser/pouchdb/pouchdb.min.js +7 -0
- package/frameworks/browser/spark-md5/spark-md5.min.js +1 -0
- package/frameworks/browser/sworker.js +192 -0
- package/frameworks/browser/terminals.nlg +787 -0
- package/frameworks/browser/workers.nlg +386 -0
- package/frameworks/client/apiclient.nlg +183 -0
- package/frameworks/client/client.nlg +29 -0
- package/frameworks/client/psm_client.nlg +572 -0
- package/frameworks/common/common.nlg +464 -0
- package/frameworks/common/preferences.nlg +131 -0
- package/frameworks/common/repltools.nlg +100 -0
- package/frameworks/common/watching.nlg +97 -0
- package/frameworks/node/apiserver.nlg +399 -0
- package/frameworks/node/filesystem.nlg +1012 -0
- package/frameworks/node/gitter.nlg +234 -0
- package/frameworks/node/node.nlg +81 -0
- package/frameworks/node/psm_server.nlg +465 -0
- package/frameworks/node/worker.nlg +560 -0
- package/frameworks/project/build.nlg +1229 -0
- package/frameworks/project/jszip.js +30 -0
- package/frameworks/project/jszip.nlg +87 -0
- package/frameworks/project/proj_cliser.nlg +139 -0
- package/frameworks/project/proj_console.nlg +137 -0
- package/frameworks/project/proj_folder.nlg +143 -0
- package/frameworks/project/proj_lambda.nlg +137 -0
- package/frameworks/project/proj_static.nlg +138 -0
- package/frameworks/project/project.nlg +29 -0
- package/frameworks/project/projects.nlg +587 -0
- package/frameworks/project/uglifyjs.js +1 -0
- package/frameworks/project/uglifyjs.nlg +79 -0
- package/frameworks/running/debugcom.nlg +163 -0
- package/frameworks/running/debugnub.nlg +565 -0
- package/frameworks/running/debugutil.nlg +510 -0
- package/frameworks/running/executors.nlg +767 -0
- package/frameworks/running/running.nlg +151 -0
- package/frameworks/running/sourcecode.nlg +346 -0
- package/frameworks/running/taskexec.nlg +196 -0
- package/frameworks/storage/dbt_pouch.nlg +773 -0
- package/frameworks/storage/file_manager.nlg +772 -0
- package/frameworks/storage/psm.nlg +326 -0
- package/frameworks/storage/psm_dbtables.nlg +814 -0
- package/frameworks/storage/resources.nlg +445 -0
- package/frameworks/storage/storage.nlg +52 -0
- package/lib/browser/env_web.js +431 -0
- package/lib/browser/env_webworker.js +146 -0
- package/lib/browser/require.js +1653 -0
- package/lib/core/naanlib.js +73 -0
- package/lib/env_node.js +282 -0
- package/lib/env_nodeworker.js +165 -0
- package/lib/node_repl.js +25 -0
- package/lib/node_repl_init.nlg +52 -0
- package/package.json +13 -0
- package/plugins/gitClient/clientops.nlg +317 -0
- package/plugins/gitClient/gitClient.nlg +63 -0
- package/plugins/gitClient/gitutils.nlg +1331 -0
- package/plugins/gitClient/minimatch.js +945 -0
- package/plugins/gitClient/minimatch_license.txt +15 -0
- package/plugins/gitClient/minimatch_readme.md +208 -0
- package/plugins/nodeLib/nodeLib.nlg +26 -0
- package/plugins/nodeLib/node_cp.nlg +65 -0
- package/plugins/nodeLib/node_fs.nlg +137 -0
- package/plugins/nodeLib/node_https.nlg +172 -0
- package/plugins/nodeLib/node_util.nlg +180 -0
- package/plugins/serviceAws/aws/aws-sdk.min.js +88 -0
- package/plugins/serviceAws/aws/lambda_rest_index.js +122 -0
- package/plugins/serviceAws/aws/lambda_rest_init.nlg +58 -0
- package/plugins/serviceAws/aws/lambda_ws_index.js +498 -0
- package/plugins/serviceAws/aws/lambda_ws_init.nlg +49 -0
- package/plugins/serviceAws/aws_cloudwatchlogs.nlg +122 -0
- package/plugins/serviceAws/aws_cognito.nlg +76 -0
- package/plugins/serviceAws/aws_dynamo.nlg +509 -0
- package/plugins/serviceAws/aws_lambda.nlg +95 -0
- package/plugins/serviceAws/aws_s3.nlg +134 -0
- package/plugins/serviceAws/aws_utils.nlg +86 -0
- package/plugins/serviceAws/dbt_aws.nlg +769 -0
- package/plugins/serviceAws/lambda_naan_control.nlg +175 -0
- package/plugins/serviceAws/psm_aws.nlg +258 -0
- package/plugins/serviceAws/serviceAws.nlg +38 -0
- package/plugins/serviceGitHub/gitops.nlg +522 -0
- package/plugins/serviceGitHub/psm_github.nlg +205 -0
- package/plugins/serviceGitHub/serviceGitHub.nlg +65 -0
- package/plugins/serviceGitLab/gitops.nlg +528 -0
- package/plugins/serviceGitLab/psm_gitlab.nlg +205 -0
- package/plugins/serviceGitLab/serviceGitLab.nlg +65 -0
- package/test/harness.nlg +312 -0
- package/test/node_test.js +33 -0
- package/test/test_01_core.nlg +768 -0
- package/test/test_02_context.nlg +269 -0
- package/test/test_03_jsinterop.nlg +137 -0
- package/test/test_04_numerics.nlg +450 -0
- package/test/test_05_strings.nlg +63 -0
- package/test/test_06_lingoparse.nlg +500 -0
- 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
|
+
}();
|