зеркало из https://github.com/Azure/git-rest-api.git
Feature: Repo indexing + repo state machine to manage concurrent requests correctly (#51)
This commit is contained in:
Родитель
ed0c3ef077
Коммит
d73350e3f6
|
@ -772,8 +772,7 @@
|
|||
"ansi-regex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
|
@ -783,6 +782,11 @@
|
|||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
|
||||
|
@ -793,6 +797,11 @@
|
|||
"normalize-path": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"app-root-path": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz",
|
||||
"integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA=="
|
||||
},
|
||||
"append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
|
@ -816,7 +825,6 @@
|
|||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
|
@ -1989,6 +1997,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
|
||||
"integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw=="
|
||||
},
|
||||
"basic-auth": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||
|
@ -2126,6 +2139,15 @@
|
|||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==",
|
||||
"requires": {
|
||||
"base64-js": "^1.0.2",
|
||||
"ieee754": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"buffer-alloc": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
|
||||
|
@ -2313,6 +2335,82 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"cli-highlight": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.1.tgz",
|
||||
"integrity": "sha512-0y0VlNmdD99GXZHYnvrQcmHxP8Bi6T00qucGgBgGv4kJ0RyDthNnnFPupHV7PYv/OXSVk+azFbOeaW6+vGmx9A==",
|
||||
"requires": {
|
||||
"chalk": "^2.3.0",
|
||||
"highlight.js": "^9.6.0",
|
||||
"mz": "^2.4.0",
|
||||
"parse5": "^4.0.0",
|
||||
"yargs": "^13.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"cliui": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
|
||||
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
|
||||
"requires": {
|
||||
"string-width": "^3.1.0",
|
||||
"strip-ansi": "^5.2.0",
|
||||
"wrap-ansi": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
|
||||
"requires": {
|
||||
"emoji-regex": "^7.0.1",
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
|
||||
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.0",
|
||||
"string-width": "^3.0.0",
|
||||
"strip-ansi": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"yargs": {
|
||||
"version": "13.2.4",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz",
|
||||
"integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==",
|
||||
"requires": {
|
||||
"cliui": "^5.0.0",
|
||||
"find-up": "^3.0.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"os-locale": "^3.1.0",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^3.0.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^13.1.0"
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
|
||||
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
|
||||
|
@ -2787,7 +2885,6 @@
|
|||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nice-try": "^1.0.4",
|
||||
"path-key": "^2.0.1",
|
||||
|
@ -3095,6 +3192,11 @@
|
|||
"resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz",
|
||||
"integrity": "sha1-WTKJDcn04vGeXrAqIAJuXl78j1g="
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz",
|
||||
"integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w=="
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
|
@ -3117,6 +3219,11 @@
|
|||
"shimmer": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
|
||||
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
|
||||
},
|
||||
"enabled": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz",
|
||||
|
@ -3305,7 +3412,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^6.0.0",
|
||||
"get-stream": "^4.0.0",
|
||||
|
@ -3562,6 +3668,11 @@
|
|||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
|
||||
"integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg=="
|
||||
},
|
||||
"figlet": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.2.3.tgz",
|
||||
"integrity": "sha512-+F5zdvZ66j77b8x2KCPvWUHC0UCKUMWrewxmewgPlagp3wmDpcrHMbyv/ygq/6xoxBPGQA+UJU3SMoBzKoROQQ=="
|
||||
},
|
||||
"file-stream-rotator": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.4.1.tgz",
|
||||
|
@ -3617,7 +3728,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
|
||||
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"locate-path": "^3.0.0"
|
||||
}
|
||||
|
@ -4325,7 +4435,6 @@
|
|||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
|
@ -4418,7 +4527,6 @@
|
|||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
|
||||
"integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
},
|
||||
|
@ -4426,8 +4534,7 @@
|
|||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -4529,6 +4636,11 @@
|
|||
"resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.0.0.tgz",
|
||||
"integrity": "sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys="
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "9.15.8",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.8.tgz",
|
||||
"integrity": "sha512-RrapkKQWwE+wKdF73VsOa2RQdIoO3mxwJ4P8mhbI6KYJUraUHRKM5w5zQQKXNk0xNL4UVRdulV9SBJcmzJNzVA=="
|
||||
},
|
||||
"home-or-tmp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
|
||||
|
@ -4612,6 +4724,11 @@
|
|||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
|
||||
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
|
||||
},
|
||||
"ienoopen": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.1.0.tgz",
|
||||
|
@ -4672,8 +4789,7 @@
|
|||
"invert-kv": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
|
||||
"integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA=="
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.9.0",
|
||||
|
@ -5475,7 +5591,6 @@
|
|||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
|
||||
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
|
@ -5484,8 +5599,7 @@
|
|||
"esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5606,7 +5720,6 @@
|
|||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
|
||||
"integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"invert-kv": "^2.0.0"
|
||||
}
|
||||
|
@ -5655,7 +5768,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
||||
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-locate": "^3.0.0",
|
||||
"path-exists": "^3.0.0"
|
||||
|
@ -5750,7 +5862,6 @@
|
|||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
|
||||
"integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-defer": "^1.0.0"
|
||||
}
|
||||
|
@ -5779,7 +5890,6 @@
|
|||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
|
||||
"integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"map-age-cleaner": "^0.1.1",
|
||||
"mimic-fn": "^2.0.0",
|
||||
|
@ -5862,8 +5972,7 @@
|
|||
"mimic-fn": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
|
@ -5956,6 +6065,16 @@
|
|||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"requires": {
|
||||
"any-promise": "^1.0.0",
|
||||
"object-assign": "^4.0.1",
|
||||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
|
||||
|
@ -6030,8 +6149,7 @@
|
|||
"nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"nocache": {
|
||||
"version": "2.1.0",
|
||||
|
@ -6229,7 +6347,6 @@
|
|||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-key": "^2.0.0"
|
||||
}
|
||||
|
@ -6411,7 +6528,6 @@
|
|||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
|
||||
"integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"execa": "^1.0.0",
|
||||
"lcid": "^2.0.0",
|
||||
|
@ -6435,8 +6551,7 @@
|
|||
"p-defer": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
|
||||
"integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
|
||||
"dev": true
|
||||
"integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww="
|
||||
},
|
||||
"p-each-series": {
|
||||
"version": "1.0.0",
|
||||
|
@ -6450,20 +6565,17 @@
|
|||
"p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
|
||||
"dev": true
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"p-is-promise": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
|
||||
"integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg=="
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz",
|
||||
"integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-try": "^2.0.0"
|
||||
}
|
||||
|
@ -6472,7 +6584,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
|
||||
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-limit": "^2.0.0"
|
||||
}
|
||||
|
@ -6486,8 +6597,12 @@
|
|||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
|
||||
},
|
||||
"parent-require": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz",
|
||||
"integrity": "sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc="
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "4.0.0",
|
||||
|
@ -6502,8 +6617,7 @@
|
|||
"parse5": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
|
||||
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.3",
|
||||
|
@ -6519,8 +6633,7 @@
|
|||
"path-exists": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
|
||||
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
|
||||
"dev": true
|
||||
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
|
@ -6530,8 +6643,7 @@
|
|||
"path-key": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
|
||||
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
|
||||
"dev": true
|
||||
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
|
||||
},
|
||||
"path-parse": {
|
||||
"version": "1.0.6",
|
||||
|
@ -6676,7 +6788,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
|
@ -6942,14 +7053,12 @@
|
|||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
|
||||
"dev": true
|
||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
|
||||
},
|
||||
"require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.10.1",
|
||||
|
@ -7130,7 +7239,6 @@
|
|||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"shebang-regex": "^1.0.0"
|
||||
}
|
||||
|
@ -7138,8 +7246,7 @@
|
|||
"shebang-regex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
|
||||
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
|
||||
"dev": true
|
||||
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
|
||||
},
|
||||
"shellwords": {
|
||||
"version": "0.1.1",
|
||||
|
@ -7382,8 +7489,17 @@
|
|||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
||||
"dev": true
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"sqlite3": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.9.tgz",
|
||||
"integrity": "sha512-IkvzjmsWQl9BuBiM4xKpl5X8WCR4w0AeJHRdobCdXZ8dT/lNc1XS6WqvY35N6+YzIIgzSBeY5prdFObID9F9tA==",
|
||||
"requires": {
|
||||
"nan": "^2.12.1",
|
||||
"node-pre-gyp": "^0.11.0",
|
||||
"request": "^2.87.0"
|
||||
}
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.16.1",
|
||||
|
@ -7516,7 +7632,6 @@
|
|||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
||||
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^4.1.0"
|
||||
}
|
||||
|
@ -7530,8 +7645,7 @@
|
|||
"strip-eof": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
|
||||
},
|
||||
"strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
|
@ -7634,6 +7748,22 @@
|
|||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
|
||||
},
|
||||
"thenify": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz",
|
||||
"integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=",
|
||||
"requires": {
|
||||
"any-promise": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"thenify-all": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||
"integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=",
|
||||
"requires": {
|
||||
"thenify": ">= 3.1.0 < 4"
|
||||
}
|
||||
},
|
||||
"throat": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz",
|
||||
|
@ -7880,6 +8010,104 @@
|
|||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"typeorm": {
|
||||
"version": "0.2.18",
|
||||
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.18.tgz",
|
||||
"integrity": "sha512-S553GwtG5ab268+VmaLCN7gKDqFPIzUw0eGMTobJ9yr0Np62Ojfx8j1Oa9bIeh5p7Pz1/kmGabAHoP1MYK05pA==",
|
||||
"requires": {
|
||||
"app-root-path": "^2.0.1",
|
||||
"buffer": "^5.1.0",
|
||||
"chalk": "^2.4.2",
|
||||
"cli-highlight": "^2.0.0",
|
||||
"debug": "^4.1.1",
|
||||
"dotenv": "^6.2.0",
|
||||
"glob": "^7.1.2",
|
||||
"js-yaml": "^3.13.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"tslib": "^1.9.0",
|
||||
"xml2js": "^0.4.17",
|
||||
"yargonaut": "^1.1.2",
|
||||
"yargs": "^13.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"cliui": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
|
||||
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
|
||||
"requires": {
|
||||
"string-width": "^3.1.0",
|
||||
"strip-ansi": "^5.2.0",
|
||||
"wrap-ansi": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
|
||||
"requires": {
|
||||
"emoji-regex": "^7.0.1",
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
|
||||
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.0",
|
||||
"string-width": "^3.0.0",
|
||||
"strip-ansi": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"yargs": {
|
||||
"version": "13.2.4",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz",
|
||||
"integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==",
|
||||
"requires": {
|
||||
"cliui": "^5.0.0",
|
||||
"find-up": "^3.0.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"os-locale": "^3.1.0",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^3.0.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^13.1.0"
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
|
||||
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.1.tgz",
|
||||
|
@ -8128,8 +8356,7 @@
|
|||
"which-module": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
|
||||
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
|
||||
"dev": true
|
||||
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
|
||||
},
|
||||
"wide-align": {
|
||||
"version": "1.1.3",
|
||||
|
@ -8313,6 +8540,20 @@
|
|||
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.19",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
|
||||
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~9.0.1"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "9.0.7",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
|
||||
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
||||
|
@ -8321,14 +8562,60 @@
|
|||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
|
||||
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
|
||||
},
|
||||
"yargonaut": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz",
|
||||
"integrity": "sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==",
|
||||
"requires": {
|
||||
"chalk": "^1.1.1",
|
||||
"figlet": "^1.1.1",
|
||||
"parent-require": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
|
||||
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
|
||||
},
|
||||
"chalk": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
|
||||
"requires": {
|
||||
"ansi-styles": "^2.2.1",
|
||||
"escape-string-regexp": "^1.0.2",
|
||||
"has-ansi": "^2.0.0",
|
||||
"strip-ansi": "^3.0.0",
|
||||
"supports-color": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
|
||||
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
|
||||
}
|
||||
}
|
||||
},
|
||||
"yargs": {
|
||||
"version": "12.0.5",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
|
||||
|
|
|
@ -49,7 +49,6 @@
|
|||
"jest": "^24.8.0",
|
||||
"jest-junit": "^6.4.0",
|
||||
"prettier": "^1.18.2",
|
||||
"rimraf": "^2.6.3",
|
||||
"swagger-ui-express": "^4.0.6",
|
||||
"ts-jest": "^24.0.2",
|
||||
"tslint": "^5.17.0",
|
||||
|
@ -76,8 +75,11 @@
|
|||
"node-fetch": "^2.6.0",
|
||||
"nodegit": "^0.24.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^2.6.3",
|
||||
"rxjs": "^6.5.2",
|
||||
"sqlite3": "^4.0.9",
|
||||
"triple-beam": "^1.3.0",
|
||||
"typeorm": "^0.2.18",
|
||||
"uuid": "^3.3.2",
|
||||
"winston": "^3.2.1",
|
||||
"winston-daily-rotate-file": "^3.9.0"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
||||
import { APP_INTERCEPTOR } from "@nestjs/core";
|
||||
import { Connection } from "typeorm";
|
||||
|
||||
import { Configuration } from "./config";
|
||||
import {
|
||||
|
@ -19,12 +20,14 @@ import {
|
|||
ContentService,
|
||||
DiskUsageService,
|
||||
FSService,
|
||||
GitFetchService,
|
||||
HttpService,
|
||||
PermissionCacheService,
|
||||
PermissionService,
|
||||
RepoCleanupService,
|
||||
RepoService,
|
||||
createDBConnection,
|
||||
} from "./services";
|
||||
import { RepoIndexService } from "./services/repo-index";
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
|
@ -36,13 +39,14 @@ import {
|
|||
FSService,
|
||||
BranchService,
|
||||
PermissionService,
|
||||
GitFetchService,
|
||||
PermissionCacheService,
|
||||
HttpService,
|
||||
Configuration,
|
||||
CommitService,
|
||||
ContentService,
|
||||
DiskUsageService,
|
||||
RepoIndexService,
|
||||
RepoCleanupService,
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: LoggingInterceptor,
|
||||
|
@ -52,12 +56,19 @@ import {
|
|||
useFactory: (config: Configuration) => createTelemetry(config),
|
||||
inject: [Configuration],
|
||||
},
|
||||
{
|
||||
provide: Connection,
|
||||
useFactory: (config: Configuration) => createDBConnection(config),
|
||||
inject: [Configuration],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
constructor(private diskUsage: DiskUsageService) {}
|
||||
constructor(private diskUsage: DiskUsageService, private repoCleanupService: RepoCleanupService) {}
|
||||
public configure(consumer: MiddlewareConsumer) {
|
||||
this.diskUsage.startCollection();
|
||||
this.repoCleanupService.start();
|
||||
|
||||
consumer.apply(ContextMiddleware).forRoutes("*");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { delay } from "../../utils";
|
|||
|
||||
describe("Test branch controller", () => {
|
||||
it("doesn't conflict when getting the same repo twice at the same time", async () => {
|
||||
await delay(100);
|
||||
await delay(2000); // Need to wait for the server to release locks on the repo from previous tests. As e2e test are run in squence this should be a fixed amount of time
|
||||
await deleteLocalRepo(UNENCODED_TEST_REPO);
|
||||
await delay(100);
|
||||
const responses = await Promise.all([
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
export * from "./repo-auth";
|
||||
export * from "./logger";
|
||||
export * from "./pagination";
|
||||
export * from "./repo";
|
||||
export * from "./telemetry";
|
||||
export * from "./models";
|
||||
export * from "./context";
|
||||
|
|
|
@ -7,10 +7,14 @@ export interface LogMetadata {
|
|||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
private logger: winston.Logger;
|
||||
type Class = new (...args: any[]) => unknown;
|
||||
|
||||
constructor(private context: string) {
|
||||
export class Logger {
|
||||
private readonly logger: winston.Logger;
|
||||
private readonly context: string;
|
||||
|
||||
constructor(context: string | Class) {
|
||||
this.context = typeof context === "string" ? context : context.constructor.name;
|
||||
this.logger = WINSTON_LOGGER;
|
||||
}
|
||||
|
||||
|
@ -23,7 +27,7 @@ export class Logger {
|
|||
}
|
||||
|
||||
public warning(message: string, meta?: LogMetadata) {
|
||||
this.logger.warning(message, this.processMetadata(meta));
|
||||
this.logger.warn(message, this.processMetadata(meta));
|
||||
}
|
||||
|
||||
public error(message: string | Error, meta?: LogMetadata) {
|
||||
|
|
|
@ -59,9 +59,12 @@ if (config.nodeEnv === "development") {
|
|||
);
|
||||
}
|
||||
|
||||
export const WINSTON_LOGGER_OPTIONS: LoggerOptions = {
|
||||
transports: [
|
||||
new winston.transports.Console(consoleTransport),
|
||||
const transports: any[] = [new winston.transports.Console(consoleTransport)];
|
||||
|
||||
if (config.nodeEnv === "test") {
|
||||
consoleTransport.silent = true;
|
||||
} else {
|
||||
transports.push(
|
||||
new winstonDailyFile({
|
||||
filename: `%DATE%.log`,
|
||||
datePattern: "YYYY-MM-DD-HH",
|
||||
|
@ -69,7 +72,11 @@ export const WINSTON_LOGGER_OPTIONS: LoggerOptions = {
|
|||
dirname: "logs",
|
||||
handleExceptions: true,
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
export const WINSTON_LOGGER_OPTIONS: LoggerOptions = {
|
||||
transports,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import { Repository } from "nodegit";
|
||||
|
||||
import { RepositoryDestroyedError } from "./repo-destroyed-error";
|
||||
|
||||
export class GCRepo {
|
||||
public path: string;
|
||||
private references = 1;
|
||||
private destroyed = false;
|
||||
|
||||
constructor(private repo: Repository) {
|
||||
this.path = this.repo.path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access to the repository
|
||||
*/
|
||||
public get instance() {
|
||||
// Guard to make sure you are not using the repo after it was destroyed which would cause a segmentation fault and crash the server.
|
||||
// This will just throw an error which will result in a 500 which can be diagnoistied much better
|
||||
if (this.destroyed) {
|
||||
throw new RepositoryDestroyedError(this.path);
|
||||
}
|
||||
return this.repo;
|
||||
}
|
||||
|
||||
public lock() {
|
||||
this.references++;
|
||||
}
|
||||
|
||||
public unlock() {
|
||||
this.references--;
|
||||
if (this.references === 0) {
|
||||
this.destroyed = true;
|
||||
this.repo.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./gc-repo";
|
||||
export * from "./repo-destroyed-error";
|
|
@ -1,8 +0,0 @@
|
|||
export class RepositoryDestroyedError extends Error {
|
||||
constructor(public path: string) {
|
||||
super(
|
||||
`Cannot access repository that was destroyed. There must be an issue.` +
|
||||
` Make sure to call .lock() Path: ${path}`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./repo-reference";
|
|
@ -1,8 +0,0 @@
|
|||
import { Repository } from "nodegit";
|
||||
|
||||
import { GitRemotePermission } from "../services";
|
||||
|
||||
export interface LocalRepo {
|
||||
repo: Repository;
|
||||
permission: GitRemotePermission;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity({ name: "repo_references" })
|
||||
export class RepoReferenceRecord {
|
||||
@PrimaryColumn()
|
||||
public path!: string;
|
||||
|
||||
@Column("bigint")
|
||||
public lastUse!: number;
|
||||
|
||||
@Column("bigint", { nullable: true })
|
||||
public lastFetch?: number;
|
||||
}
|
|
@ -72,9 +72,7 @@ export class CommitService {
|
|||
if (ref) {
|
||||
return this.getCommit(repo, ref);
|
||||
} else {
|
||||
const branch = await repo.getCurrentBranch();
|
||||
const name = branch.shorthand();
|
||||
return repo.getReferenceCommit(`origin/${name}`);
|
||||
return repo.getReferenceCommit(`origin/master`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Commit, ConvenientPatch, Diff, Merge, Oid, Repository } from "nodegit";
|
||||
|
||||
import { Logger } from "../../core";
|
||||
import { GitFileDiff, PatchStatus } from "../../dtos";
|
||||
import { GitDiff } from "../../dtos/git-diff";
|
||||
import { GitUtils, notUndefined } from "../../utils";
|
||||
|
@ -12,6 +13,8 @@ const MAX_FILES_PER_DIFF = 300;
|
|||
|
||||
@Injectable()
|
||||
export class CompareService {
|
||||
private logger = new Logger(CompareService);
|
||||
|
||||
constructor(private repoService: RepoService, private commitService: CommitService) {}
|
||||
|
||||
public async compare(
|
||||
|
@ -20,8 +23,8 @@ export class CompareService {
|
|||
head: string,
|
||||
options: GitBaseOptions = {},
|
||||
): Promise<GitDiff | NotFoundException> {
|
||||
const compareRepo = await this.getCompareRepo(remote, base, head, options);
|
||||
return this.repoService.using(compareRepo.repo, async repo => {
|
||||
return this.useCompareRepo(remote, base, head, options, async compareRepo => {
|
||||
const repo = compareRepo.repo;
|
||||
const [baseCommit, headCommit] = await Promise.all([
|
||||
this.commitService.getCommit(repo, compareRepo.baseRef),
|
||||
this.commitService.getCommit(repo, compareRepo.headRef),
|
||||
|
@ -38,8 +41,13 @@ export class CompareService {
|
|||
}
|
||||
|
||||
public async getMergeBase(repo: Repository, base: Oid, head: Oid): Promise<Commit | undefined> {
|
||||
const mergeBaseSha = await Merge.base(repo, base, head);
|
||||
return this.commitService.getCommit(repo, mergeBaseSha.toString());
|
||||
try {
|
||||
const mergeBaseSha = await Merge.base(repo, base, head);
|
||||
return this.commitService.getCommit(repo, mergeBaseSha.toString());
|
||||
} catch (error) {
|
||||
this.logger.info("Merge base was not found", { error });
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async getComparison(
|
||||
|
@ -100,7 +108,13 @@ export class CompareService {
|
|||
return patches.map(x => toFileDiff(x)).slice(0, MAX_FILES_PER_DIFF);
|
||||
}
|
||||
|
||||
private async getCompareRepo(remote: string, base: string, head: string, options: GitBaseOptions) {
|
||||
private async useCompareRepo<T>(
|
||||
remote: string,
|
||||
base: string,
|
||||
head: string,
|
||||
options: GitBaseOptions,
|
||||
action: (p: any) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const baseRef = GitUtils.parseRemoteReference(base, remote);
|
||||
const headRef = GitUtils.parseRemoteReference(head, remote);
|
||||
|
||||
|
@ -108,7 +122,7 @@ export class CompareService {
|
|||
const headRemote = headRef.remote;
|
||||
|
||||
if (baseRemote !== headRemote) {
|
||||
const repo = await this.repoService.createForCompare(
|
||||
return this.repoService.useForCompare(
|
||||
{
|
||||
name: "baser",
|
||||
remote: baseRemote,
|
||||
|
@ -118,11 +132,13 @@ export class CompareService {
|
|||
remote: headRemote,
|
||||
},
|
||||
options,
|
||||
repo =>
|
||||
action({ repo, baseRef: `refs/remotes/baser/${baseRef.ref}`, headRef: `refs/remotes/headr/${headRef.ref}` }),
|
||||
);
|
||||
return { repo, baseRef: `refs/remotes/baser/${baseRef.ref}`, headRef: `refs/remotes/headr/${headRef.ref}` };
|
||||
} else {
|
||||
const repo = await this.repoService.get(headRemote, options);
|
||||
return { repo, baseRef: baseRef.ref, headRef: headRef.ref };
|
||||
return this.repoService.use(headRemote, options, repo =>
|
||||
action({ repo, baseRef: baseRef.ref, headRef: headRef.ref }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { createConnection } from "typeorm";
|
||||
|
||||
import { Configuration } from "../../config";
|
||||
import { RepoReferenceRecord } from "../../models";
|
||||
|
||||
/**
|
||||
* Create a DB Connection to sqlite and sync the schema.
|
||||
* Can be used as NestJS async factory and then have the Connection as a injectable for other service.
|
||||
*/
|
||||
export async function createDBConnection(configuration: Configuration) {
|
||||
const connection = await createConnection({
|
||||
type: "sqlite",
|
||||
database: `${configuration.dataDir}/data.sqlite`,
|
||||
entities: [RepoReferenceRecord],
|
||||
});
|
||||
await connection.synchronize();
|
||||
return connection;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./db.service";
|
|
@ -1,5 +1,9 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import fs from "fs";
|
||||
import rimraf from "rimraf";
|
||||
import { promisify } from "util";
|
||||
|
||||
const rm = promisify(rimraf);
|
||||
|
||||
@Injectable()
|
||||
export class FSService {
|
||||
|
@ -16,4 +20,8 @@ export class FSService {
|
|||
await fs.promises.mkdir(path, { recursive: true });
|
||||
return path;
|
||||
}
|
||||
|
||||
public async rm(path: string): Promise<void> {
|
||||
await rm(path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Clone, Cred, Fetch, FetchOptions, Repository } from "nodegit";
|
||||
import path from "path";
|
||||
|
||||
import { Configuration } from "../../config";
|
||||
import { FSService } from "../fs";
|
||||
import { GitBaseOptions } from "../repo";
|
||||
|
||||
export function credentialsCallback(options: GitBaseOptions): () => Cred {
|
||||
return () => {
|
||||
if (options.auth) {
|
||||
return options.auth.toCreds();
|
||||
}
|
||||
return Cred.defaultNew();
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultFetchOptions: FetchOptions = {
|
||||
downloadTags: 0,
|
||||
prune: Fetch.PRUNE.GIT_FETCH_PRUNE,
|
||||
};
|
||||
|
||||
const FETCH_TIMEOUT = 30_000; // 30s;
|
||||
|
||||
@Injectable()
|
||||
export class GitFetchService {
|
||||
public readonly repoCacheFolder = path.join(this.config.dataDir, "repos");
|
||||
|
||||
private currentFetches = new Map<string, Promise<Repository>>();
|
||||
private lastFetch = new Map<string, number>();
|
||||
|
||||
private cacheReady: Promise<string>;
|
||||
|
||||
constructor(fs: FSService, private config: Configuration) {
|
||||
this.cacheReady = fs.mkdir(this.repoCacheFolder);
|
||||
}
|
||||
|
||||
public async fetch(id: string, repo: Repository, options: GitBaseOptions): Promise<Repository> {
|
||||
if (await this.needToFetch(id)) {
|
||||
return this.ensureSingleFetch(id, () => this.fetchAll(id, repo, options).then(() => repo));
|
||||
}
|
||||
return repo;
|
||||
}
|
||||
|
||||
private ensureSingleFetch(id: string, callback: () => Promise<Repository>): Promise<Repository> {
|
||||
let promise = this.currentFetches.get(id);
|
||||
|
||||
if (!promise) {
|
||||
promise = callback().then(repo => {
|
||||
this.currentFetches.delete(id);
|
||||
return repo;
|
||||
});
|
||||
this.currentFetches.set(id, promise);
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
public async clone(remote: string, repoPath: string, options: GitBaseOptions): Promise<Repository> {
|
||||
await this.cacheReady;
|
||||
try {
|
||||
return await Clone.clone(`https://${remote}`, repoPath, {
|
||||
fetchOpts: {
|
||||
...defaultFetchOptions,
|
||||
callbacks: {
|
||||
credentials: credentialsCallback(options),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchAll(remote: string, repo: Repository, options: GitBaseOptions) {
|
||||
try {
|
||||
await repo.fetchAll({
|
||||
...defaultFetchOptions,
|
||||
callbacks: {
|
||||
credentials: credentialsCallback(options),
|
||||
},
|
||||
});
|
||||
this.lastFetch.set(remote, new Date().getTime());
|
||||
} catch {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we need to fetch the given remote
|
||||
*/
|
||||
private async needToFetch(remote: string): Promise<boolean> {
|
||||
const lastFetch = this.lastFetch.get(remote);
|
||||
if (!lastFetch) {
|
||||
return true;
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
return now - lastFetch > FETCH_TIMEOUT;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./git-fetch.service";
|
|
@ -6,6 +6,8 @@ export * from "./compare";
|
|||
export * from "./disk-usage";
|
||||
export * from "./fs";
|
||||
export * from "./repo";
|
||||
export * from "./repo-cleanup";
|
||||
export * from "./repo-index";
|
||||
export * from "./permission";
|
||||
export * from "./git-fetch";
|
||||
export * from "./http";
|
||||
export * from "./db";
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from "./repo-cleanup.service";
|
|
@ -0,0 +1,104 @@
|
|||
import { BehaviorSubject, Subscription } from "rxjs";
|
||||
|
||||
import { DiskUsage } from "../disk-usage";
|
||||
import { RepoCleanupService } from "./repo-cleanup.service";
|
||||
|
||||
const defaultDiskUsage: DiskUsage = {
|
||||
total: 10_000,
|
||||
available: 8_000,
|
||||
used: 2_000,
|
||||
};
|
||||
|
||||
describe("RepoCleanupService", () => {
|
||||
let service: RepoCleanupService;
|
||||
|
||||
const indexServiceSpy = {
|
||||
size: 20,
|
||||
getLeastUsedRepos: jest.fn(() => ["foo-1", "foo-2"]),
|
||||
};
|
||||
|
||||
const repoServiceSpy = {
|
||||
deleteLocalRepo: jest.fn(),
|
||||
};
|
||||
|
||||
const diskUsageService = {
|
||||
dataDiskUsage: new BehaviorSubject<DiskUsage>(defaultDiskUsage),
|
||||
};
|
||||
|
||||
let sub: Subscription;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers();
|
||||
diskUsageService.dataDiskUsage.next(defaultDiskUsage);
|
||||
service = new RepoCleanupService(indexServiceSpy as any, repoServiceSpy as any, diskUsageService as any);
|
||||
sub = service.start();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sub.unsubscribe();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("Shouldn't do anything if there is enough disk available", () => {
|
||||
expect(indexServiceSpy.getLeastUsedRepos).not.toHaveBeenCalled();
|
||||
diskUsageService.dataDiskUsage.next({
|
||||
total: 10_000,
|
||||
available: 5_000,
|
||||
used: 5_000,
|
||||
});
|
||||
expect(indexServiceSpy.getLeastUsedRepos).not.toHaveBeenCalled();
|
||||
diskUsageService.dataDiskUsage.next({
|
||||
total: 10_000,
|
||||
available: 4_000,
|
||||
used: 4_000,
|
||||
});
|
||||
expect(indexServiceSpy.getLeastUsedRepos).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Should try to delete repos if there is not enough space", () => {
|
||||
expect(indexServiceSpy.getLeastUsedRepos).not.toHaveBeenCalled();
|
||||
diskUsageService.dataDiskUsage.next({
|
||||
total: 10_000,
|
||||
available: 500,
|
||||
used: 9_500,
|
||||
});
|
||||
expect(indexServiceSpy.getLeastUsedRepos).toHaveBeenCalledTimes(1);
|
||||
expect(indexServiceSpy.getLeastUsedRepos).toHaveBeenCalledWith(1);
|
||||
|
||||
jest.runAllTicks();
|
||||
expect(repoServiceSpy.deleteLocalRepo).toHaveBeenCalledTimes(2);
|
||||
expect(repoServiceSpy.deleteLocalRepo).toHaveBeenCalledWith("foo-1");
|
||||
expect(repoServiceSpy.deleteLocalRepo).toHaveBeenCalledWith("foo-2");
|
||||
});
|
||||
|
||||
it("Should wait for the bathc of deletion to complete before checking the disk usage again", () => {
|
||||
expect(indexServiceSpy.getLeastUsedRepos).not.toHaveBeenCalled();
|
||||
diskUsageService.dataDiskUsage.next({
|
||||
total: 10_000,
|
||||
available: 500,
|
||||
used: 9_500,
|
||||
});
|
||||
expect(indexServiceSpy.getLeastUsedRepos).toHaveBeenCalledTimes(1);
|
||||
|
||||
diskUsageService.dataDiskUsage.next({
|
||||
total: 10_000,
|
||||
available: 400,
|
||||
used: 9_600,
|
||||
});
|
||||
|
||||
expect(indexServiceSpy.getLeastUsedRepos).toHaveBeenCalledTimes(1);
|
||||
diskUsageService.dataDiskUsage.next({
|
||||
total: 10_000,
|
||||
available: 300,
|
||||
used: 9_700,
|
||||
});
|
||||
|
||||
expect(indexServiceSpy.getLeastUsedRepos).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.runAllTicks();
|
||||
expect(repoServiceSpy.deleteLocalRepo).toHaveBeenCalledTimes(2);
|
||||
expect(repoServiceSpy.deleteLocalRepo).toHaveBeenCalledWith("foo-1");
|
||||
expect(repoServiceSpy.deleteLocalRepo).toHaveBeenCalledWith("foo-2");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { from, of } from "rxjs";
|
||||
import { catchError, delay, exhaustMap, filter } from "rxjs/operators";
|
||||
|
||||
import { Logger } from "../../core";
|
||||
import { DiskUsageService } from "../disk-usage";
|
||||
import { RepoService } from "../repo";
|
||||
import { RepoIndexService } from "../repo-index";
|
||||
|
||||
/**
|
||||
* Service handling cleanup of the disk where repos are cloned when available space is getting low.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RepoCleanupService {
|
||||
private logger = new Logger(RepoCleanupService);
|
||||
|
||||
constructor(
|
||||
private repoIndexService: RepoIndexService,
|
||||
private repoService: RepoService,
|
||||
private diskUsageService: DiskUsageService,
|
||||
) {}
|
||||
|
||||
public start() {
|
||||
return this.diskUsageService.dataDiskUsage
|
||||
.pipe(
|
||||
filter(x => {
|
||||
const freeRatio = x.available / x.total;
|
||||
return freeRatio < 0.1;
|
||||
}),
|
||||
exhaustMap(() => {
|
||||
const count = this.getNumberOfReposToRemove();
|
||||
if (this.repoIndexService.size === 0) {
|
||||
this.logger.error("There isn't any repo cached on disk. Space is most likely used by something else.");
|
||||
}
|
||||
const total = this.repoIndexService.size;
|
||||
this.logger.warning(
|
||||
`Disk availability is low. Removing least recently used repos. Total repos: ${total}, Removing: ${count}`,
|
||||
);
|
||||
const repos = this.repoIndexService.getLeastUsedRepos(count);
|
||||
return from(Promise.all(repos.map(x => this.repoService.deleteLocalRepo(x)))).pipe(
|
||||
delay(2000),
|
||||
catchError(error => {
|
||||
this.logger.error("Error occured when deleting repos", { error });
|
||||
return of(undefined);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
error: error => {
|
||||
this.logger.error("Error occured in repo cleanup", { error });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private getNumberOfReposToRemove() {
|
||||
const count = Math.ceil(this.repoIndexService.size / 100);
|
||||
return Math.max(Math.min(count, 10), 1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./repo-index.service";
|
|
@ -0,0 +1,65 @@
|
|||
import { RepoReferenceRecord } from "../../models";
|
||||
import { RepoIndexService } from "./repo-index.service";
|
||||
|
||||
describe("RepoIndexService", () => {
|
||||
let service: RepoIndexService;
|
||||
let now: number;
|
||||
const nowSpy = jest.fn(() => now);
|
||||
let originalNow: typeof Date.now;
|
||||
|
||||
const dbRepoSpy = {};
|
||||
const connectionSpy = {
|
||||
getRepository: jest.fn(() => dbRepoSpy),
|
||||
};
|
||||
beforeEach(() => {
|
||||
originalNow = Date.now;
|
||||
now = Date.now();
|
||||
Date.now = nowSpy;
|
||||
service = new RepoIndexService(connectionSpy as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date.now = originalNow;
|
||||
});
|
||||
|
||||
it("Calls the repo", () => {
|
||||
expect(connectionSpy.getRepository).toHaveBeenCalledTimes(1);
|
||||
expect(connectionSpy.getRepository).toHaveBeenCalledWith(RepoReferenceRecord);
|
||||
});
|
||||
|
||||
it("get the least recently used repos", () => {
|
||||
service.markRepoAsOpened("foo-1");
|
||||
now += 100;
|
||||
service.markRepoAsFetched("foo-2");
|
||||
now += 100;
|
||||
service.markRepoAsFetched("foo-3");
|
||||
now += 100;
|
||||
service.markRepoAsOpened("foo-4");
|
||||
now -= 10000;
|
||||
service.markRepoAsOpened("foo-0");
|
||||
|
||||
expect(service.getLeastUsedRepos(3)).toEqual(["foo-0", "foo-1", "foo-2"]);
|
||||
});
|
||||
|
||||
it("needs to fetch if the repo was never opened", () => {
|
||||
expect(service.needToFetch("foo-1")).toBe(true);
|
||||
});
|
||||
|
||||
it("needs to fetch if the repo was never fetched", () => {
|
||||
service.markRepoAsOpened("foo-1");
|
||||
|
||||
expect(service.needToFetch("foo-1")).toBe(true);
|
||||
});
|
||||
|
||||
it("needs to fetch only after the cache timeout expire", () => {
|
||||
service.markRepoAsFetched("foo-1");
|
||||
expect(service.needToFetch("foo-1")).toBe(false);
|
||||
|
||||
now += 29_999;
|
||||
expect(service.needToFetch("foo-1")).toBe(false);
|
||||
now += 2;
|
||||
expect(service.needToFetch("foo-1")).toBe(true);
|
||||
now += 10_000;
|
||||
expect(service.needToFetch("foo-1")).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { Connection, Repository } from "typeorm";
|
||||
|
||||
import { Logger } from "../../core";
|
||||
import { RepoReferenceRecord } from "../../models";
|
||||
|
||||
export interface RepoReference {
|
||||
readonly path: string;
|
||||
readonly lastUse: number;
|
||||
readonly lastFetch?: number;
|
||||
}
|
||||
|
||||
const FETCH_CACHE_EXPIRY = 30_000; // 30s;
|
||||
|
||||
/**
|
||||
* Service that keep an index on all repository cached. It presisted across restart of the server.
|
||||
* It keeps track of a few details related to the repository such as the last time
|
||||
* it was open/fetched to find least recently used repos or if a fetch is due
|
||||
*/
|
||||
@Injectable()
|
||||
export class RepoIndexService {
|
||||
private logger = new Logger(RepoIndexService);
|
||||
private readonly repos = new Map<string, RepoReference>();
|
||||
private repository: Repository<RepoReferenceRecord>;
|
||||
|
||||
constructor(connection: Connection) {
|
||||
this.repository = connection.getRepository(RepoReferenceRecord);
|
||||
this.init().catch(e => {
|
||||
this.logger.error("Failed to load data from database", e);
|
||||
});
|
||||
}
|
||||
|
||||
public get size() {
|
||||
return this.repos.size;
|
||||
}
|
||||
|
||||
private async update(ref: RepoReference) {
|
||||
this.repos.set(ref.path, ref);
|
||||
|
||||
try {
|
||||
await this.repository.insert(ref);
|
||||
} catch (e) {
|
||||
await this.repository.update({ path: ref.path }, ref);
|
||||
}
|
||||
}
|
||||
|
||||
public getLeastUsedRepos(count = 1): string[] {
|
||||
return [...this.repos.values()]
|
||||
.sort((a, b) => a.lastUse - b.lastUse)
|
||||
.slice(0, count)
|
||||
.map(x => x.path);
|
||||
}
|
||||
|
||||
public needToFetch(repoId: string): boolean {
|
||||
const repo = this.repos.get(repoId);
|
||||
if (!repo || !repo.lastFetch) {
|
||||
return true;
|
||||
}
|
||||
const now = Date.now();
|
||||
return now - repo.lastFetch > FETCH_CACHE_EXPIRY;
|
||||
}
|
||||
|
||||
public markRepoAsOpened(repoId: string) {
|
||||
const now = Date.now();
|
||||
const existing = this.repos.get(repoId);
|
||||
this.tryAndLog(() => this.update({ ...existing, path: repoId, lastUse: now }));
|
||||
}
|
||||
|
||||
public markRepoAsFetched(repoId: string) {
|
||||
const now = Date.now();
|
||||
const existing = this.repos.get(repoId);
|
||||
this.tryAndLog(() => this.update({ ...existing, path: repoId, lastFetch: now, lastUse: now }));
|
||||
}
|
||||
|
||||
public markRepoAsRemoved(repoId: string) {
|
||||
this.repos.delete(repoId);
|
||||
this.tryAndLog(() => this.repository.delete({ path: repoId }));
|
||||
}
|
||||
|
||||
private async init() {
|
||||
const repos = await this.repository.find();
|
||||
for (const repo of repos) {
|
||||
this.repos.set(repo.path, { ...repo });
|
||||
}
|
||||
}
|
||||
|
||||
private tryAndLog(f: () => Promise<unknown>) {
|
||||
f().catch(error => {
|
||||
this.logger.error("Error occured", error);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./local-repo";
|
|
@ -0,0 +1,253 @@
|
|||
import nodegit from "nodegit";
|
||||
|
||||
import { RepoAuth } from "../../../core";
|
||||
import { Deferred, delay } from "../../../utils";
|
||||
import { LocalRepo } from "./local-repo";
|
||||
|
||||
const origin = {
|
||||
name: "origin",
|
||||
remote: "example.com/git-rest-api.git",
|
||||
};
|
||||
|
||||
describe("LocalRepo", () => {
|
||||
let repo: LocalRepo;
|
||||
|
||||
const fsSpy = {
|
||||
exists: jest.fn(),
|
||||
rm: jest.fn(),
|
||||
};
|
||||
const repoIndexSpy = {
|
||||
markRepoAsOpened: jest.fn(),
|
||||
markRepoAsFetched: jest.fn(),
|
||||
};
|
||||
|
||||
const onDestroy = jest.fn();
|
||||
|
||||
const repoSpy = {
|
||||
cleanup: jest.fn(),
|
||||
fetchAll: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
const MockRepository = {
|
||||
open: jest.fn(() => Promise.resolve(repoSpy)),
|
||||
init: jest.fn(() => Promise.resolve(repoSpy)),
|
||||
};
|
||||
|
||||
const MockRemote = {
|
||||
create: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
nodegit.Repository = MockRepository as any;
|
||||
nodegit.Remote = MockRemote as any;
|
||||
|
||||
repo = new LocalRepo("foo", fsSpy as any, repoIndexSpy as any);
|
||||
repo.onDestroy.subscribe(onDestroy);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (repo) {
|
||||
repo.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
describe("init the repo", () => {
|
||||
it("opens the existing one if the path exists", async () => {
|
||||
fsSpy.exists.mockResolvedValue(true);
|
||||
await repo.init([origin]);
|
||||
expect(MockRepository.open).toHaveBeenCalledTimes(1);
|
||||
expect(MockRepository.open).toHaveBeenCalledWith("foo");
|
||||
expect(MockRepository.init).not.toHaveBeenCalled();
|
||||
|
||||
expect(fsSpy.rm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("init the repo if the path doesn't exists", async () => {
|
||||
fsSpy.exists.mockResolvedValue(false);
|
||||
await repo.init([origin]);
|
||||
expect(MockRepository.init).toHaveBeenCalledTimes(1);
|
||||
expect(MockRepository.init).toHaveBeenCalledWith("foo", 1);
|
||||
expect(MockRepository.open).not.toHaveBeenCalled();
|
||||
|
||||
expect(MockRemote.create).toHaveBeenCalledTimes(1);
|
||||
expect(MockRemote.create).toHaveBeenCalledWith(repoSpy, "origin", "https://example.com/git-rest-api.git");
|
||||
});
|
||||
|
||||
it("delete then reinit the repo if it fails to open when the path exists", async () => {
|
||||
fsSpy.exists.mockResolvedValue(true);
|
||||
MockRepository.open.mockRejectedValueOnce(new Error("Failed to open repo"));
|
||||
await repo.init([origin]);
|
||||
expect(MockRepository.open).toHaveBeenCalledTimes(1);
|
||||
expect(MockRepository.open).toHaveBeenCalledWith("foo");
|
||||
|
||||
expect(fsSpy.rm).toHaveBeenCalledTimes(1);
|
||||
expect(fsSpy.rm).toHaveBeenCalledWith("foo");
|
||||
|
||||
expect(MockRepository.init).toHaveBeenCalledTimes(1);
|
||||
expect(MockRepository.init).toHaveBeenCalledWith("foo", 1);
|
||||
|
||||
expect(MockRemote.create).toHaveBeenCalledTimes(1);
|
||||
expect(MockRemote.create).toHaveBeenCalledWith(repoSpy, "origin", "https://example.com/git-rest-api.git");
|
||||
});
|
||||
|
||||
it("calling init again doesn't do anything", async () => {
|
||||
fsSpy.exists.mockResolvedValue(true);
|
||||
const init1 = await repo.init([origin]);
|
||||
await delay();
|
||||
const init2 = repo.init([origin]);
|
||||
await init1;
|
||||
await init2;
|
||||
expect(MockRepository.open).toHaveBeenCalledTimes(1);
|
||||
expect(MockRepository.init).not.toHaveBeenCalled();
|
||||
|
||||
await repo.init([origin]);
|
||||
expect(MockRepository.open).toHaveBeenCalledTimes(1);
|
||||
expect(MockRepository.init).not.toHaveBeenCalled();
|
||||
expect(fsSpy.rm).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Update and use", () => {
|
||||
beforeEach(async () => {
|
||||
fsSpy.exists.mockResolvedValue(true);
|
||||
await repo.init([origin]);
|
||||
});
|
||||
|
||||
it("use the repo", async () => {
|
||||
const response = await repo.use(async r => {
|
||||
expect(r).toBe(repoSpy);
|
||||
return "My-result";
|
||||
});
|
||||
|
||||
expect(response).toEqual("My-result");
|
||||
});
|
||||
|
||||
it("update the repo", async () => {
|
||||
const options = {
|
||||
auth: new RepoAuth(),
|
||||
};
|
||||
await repo.update(options);
|
||||
expect(repoSpy.fetchAll).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("doesn't trigger multiple updates if one is already in progress", async () => {
|
||||
let resolve: () => void;
|
||||
const fetchPromise = new Promise<void>(r => (resolve = r));
|
||||
repoSpy.fetchAll.mockImplementation(() => fetchPromise);
|
||||
const update1 = repo.update();
|
||||
const update2 = repo.update();
|
||||
await delay();
|
||||
const update3 = repo.update();
|
||||
resolve!();
|
||||
await Promise.all([update1, update2, update3]);
|
||||
expect(repoSpy.fetchAll).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should wait for uses to complete before updating", async () => {
|
||||
const use1Deferer = new Deferred();
|
||||
const use2Deferer = new Deferred();
|
||||
const use3Deferer = new Deferred();
|
||||
|
||||
const use1 = repo.use(() => use1Deferer.promise);
|
||||
const use2 = repo.use(() => use2Deferer.promise);
|
||||
|
||||
const update = repo.update();
|
||||
|
||||
const use3 = repo.use(() => use3Deferer.promise);
|
||||
|
||||
expect(repoSpy.fetchAll).not.toHaveBeenCalled();
|
||||
|
||||
use1Deferer.resolve();
|
||||
use2Deferer.resolve();
|
||||
await use1;
|
||||
await use2;
|
||||
await update;
|
||||
expect(repoSpy.fetchAll).toHaveBeenCalledTimes(1);
|
||||
use3Deferer.resolve();
|
||||
await use3;
|
||||
});
|
||||
});
|
||||
|
||||
describe("dispose of the repo when nothing is using it", () => {
|
||||
beforeEach(() => {
|
||||
repo.dispose();
|
||||
jest.useFakeTimers();
|
||||
jest.clearAllMocks();
|
||||
|
||||
repo = new LocalRepo("foo", fsSpy as any, repoIndexSpy as any);
|
||||
repo.onDestroy.subscribe(onDestroy);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("does nothing if the repo wasn't opened", () => {
|
||||
expect(onDestroy).not.toHaveBeenCalled();
|
||||
repo.unref();
|
||||
expect(onDestroy).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(repoSpy.cleanup).not.toHaveBeenCalled();
|
||||
expect(onDestroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("destroy the repo if it was opened", async () => {
|
||||
fsSpy.exists.mockResolvedValue(true);
|
||||
expect(onDestroy).not.toHaveBeenCalled();
|
||||
await repo.init([origin]);
|
||||
repo.unref();
|
||||
expect(onDestroy).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(repoSpy.cleanup).toHaveBeenCalledTimes(1);
|
||||
expect(onDestroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("counts the refs", async () => {
|
||||
fsSpy.exists.mockResolvedValue(true);
|
||||
expect(onDestroy).not.toHaveBeenCalled();
|
||||
repo.ref();
|
||||
repo.ref();
|
||||
await repo.init([origin]);
|
||||
|
||||
// Remove ref 1/3
|
||||
repo.unref();
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(onDestroy).not.toHaveBeenCalled();
|
||||
|
||||
// Remove ref 2/3
|
||||
repo.unref();
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(onDestroy).not.toHaveBeenCalled();
|
||||
|
||||
// Remove ref 3/3
|
||||
repo.unref();
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(repoSpy.cleanup).toHaveBeenCalledTimes(1);
|
||||
expect(onDestroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("shouldn't delete if repo is reused within the timeout period", async () => {
|
||||
fsSpy.exists.mockResolvedValue(true);
|
||||
expect(onDestroy).not.toHaveBeenCalled();
|
||||
await repo.init([origin]);
|
||||
|
||||
// Remove ref
|
||||
repo.unref();
|
||||
jest.advanceTimersByTime(50);
|
||||
expect(onDestroy).not.toHaveBeenCalled();
|
||||
|
||||
// Other ref coming in
|
||||
repo.ref();
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(onDestroy).not.toHaveBeenCalled();
|
||||
|
||||
// Remove other ref
|
||||
repo.unref();
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(repoSpy.cleanup).toHaveBeenCalledTimes(1);
|
||||
expect(onDestroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,164 @@
|
|||
import { NotFoundException } from "@nestjs/common";
|
||||
import { Cred, Fetch, FetchOptions, Remote, Repository } from "nodegit";
|
||||
import { BehaviorSubject, Subject } from "rxjs";
|
||||
import { debounceTime, filter } from "rxjs/operators";
|
||||
|
||||
import { Logger } from "../../../core";
|
||||
import { FSService } from "../../fs";
|
||||
import { RepoIndexService } from "../../repo-index/index";
|
||||
import { StateMutex } from "../mutex";
|
||||
import { GitBaseOptions } from "../repo.service";
|
||||
|
||||
export function credentialsCallback(options: GitBaseOptions): () => Cred {
|
||||
return () => {
|
||||
if (options.auth) {
|
||||
return options.auth.toCreds();
|
||||
}
|
||||
return Cred.defaultNew();
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultFetchOptions: FetchOptions = {
|
||||
downloadTags: 0,
|
||||
prune: Fetch.PRUNE.GIT_FETCH_PRUNE,
|
||||
};
|
||||
|
||||
export enum LocalRepoStatus {
|
||||
Initializing = "initializing",
|
||||
Updating = "updating",
|
||||
Deleting = "deleting",
|
||||
Idle = "idle",
|
||||
Reading = "reading",
|
||||
}
|
||||
|
||||
export interface RemoteDef {
|
||||
name: string;
|
||||
remote: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class referencing a local repo to manage concorrent actions and garbage collection.
|
||||
* **DO NOT USE outside of the `RepoService`. There must be only on instance of a LocalRepo mapping to the same repository to work correctly**
|
||||
* Use `RepoService` to access a local repo.
|
||||
*/
|
||||
export class LocalRepo {
|
||||
public onDestroy = new Subject();
|
||||
|
||||
private repo?: Repository;
|
||||
private currentUpdate?: Promise<void>;
|
||||
|
||||
private mutex = new StateMutex<LocalRepoStatus, LocalRepoStatus.Reading>(
|
||||
LocalRepoStatus.Idle,
|
||||
LocalRepoStatus.Initializing,
|
||||
);
|
||||
|
||||
private logger = new Logger("LocalRepo");
|
||||
private refs = new BehaviorSubject(1); // Automatically start with a reference
|
||||
|
||||
constructor(public readonly path: string, private fs: FSService, private repoIndex: RepoIndexService) {
|
||||
this.refs
|
||||
.pipe(
|
||||
debounceTime(100), // Give a 100ms timeout before closing
|
||||
filter(x => x === 0),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
public ref() {
|
||||
const refs = this.refs.value;
|
||||
this.refs.next(refs + 1);
|
||||
}
|
||||
|
||||
public unref() {
|
||||
this.refs.next(this.refs.value - 1);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
if (this.repo) {
|
||||
this.repo.cleanup();
|
||||
}
|
||||
if (!this.refs.closed) {
|
||||
this.refs.complete();
|
||||
}
|
||||
if (!this.onDestroy.closed) {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
}
|
||||
|
||||
public async init(remotes: RemoteDef[]): Promise<void> {
|
||||
const lock = await this.mutex.lock(LocalRepoStatus.Initializing, { exclusive: true });
|
||||
if (!this.repo) {
|
||||
this.repo = await this.loadRepo(remotes);
|
||||
}
|
||||
lock.release();
|
||||
}
|
||||
|
||||
public async update(options: GitBaseOptions = {}) {
|
||||
if (!this.currentUpdate) {
|
||||
this.currentUpdate = this.lockAndUpdate(options).then(() => {
|
||||
this.currentUpdate = undefined;
|
||||
});
|
||||
}
|
||||
return this.currentUpdate;
|
||||
}
|
||||
|
||||
public async use<T>(action: (repo: Repository) => Promise<T>): Promise<T> {
|
||||
const lock = await this.mutex.lock(LocalRepoStatus.Reading);
|
||||
|
||||
if (!this.repo) {
|
||||
throw new Error("Repo should have been loaded. Was init called");
|
||||
}
|
||||
|
||||
try {
|
||||
return await action(this.repo);
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadRepo(remotes: RemoteDef[]) {
|
||||
if (await this.fs.exists(this.path)) {
|
||||
try {
|
||||
return await Repository.open(this.path);
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to open repository. Deleting it");
|
||||
await this.fs.rm(this.path);
|
||||
}
|
||||
}
|
||||
|
||||
const repo = await Repository.init(this.path, 1);
|
||||
for (const { name, remote: value } of remotes) {
|
||||
await Remote.create(repo, name, `https://${value}`);
|
||||
}
|
||||
return repo;
|
||||
}
|
||||
|
||||
private async lockAndUpdate(options: GitBaseOptions = {}) {
|
||||
const lock = await this.mutex.lock(LocalRepoStatus.Updating, { exclusive: true });
|
||||
if (!this.repo) {
|
||||
throw new Error("Repo should have been loaded. Was init called");
|
||||
}
|
||||
try {
|
||||
await this.updateRepo(this.repo, options);
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
private async updateRepo(repo: Repository, options: GitBaseOptions) {
|
||||
try {
|
||||
await repo.fetchAll({
|
||||
...defaultFetchOptions,
|
||||
callbacks: {
|
||||
credentials: credentialsCallback(options),
|
||||
},
|
||||
});
|
||||
this.repoIndex.markRepoAsFetched(this.path);
|
||||
} catch {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./mutex";
|
||||
export * from "./state-mutex";
|
|
@ -0,0 +1,72 @@
|
|||
import { delay } from "../../../utils";
|
||||
import { Mutex } from "./mutex";
|
||||
|
||||
describe("Mutex", () => {
|
||||
let mutex: Mutex;
|
||||
|
||||
beforeEach(() => {
|
||||
mutex = new Mutex();
|
||||
});
|
||||
|
||||
it("acquire multiple read locks", async () => {
|
||||
const lock1 = await mutex.lock();
|
||||
const lock2 = await mutex.lock();
|
||||
expect(lock1.id).not.toEqual(lock2.id);
|
||||
lock1.release();
|
||||
lock2.release();
|
||||
});
|
||||
|
||||
it("acquire exclusive locks sequentially", async () => {
|
||||
const lock1Spy = jest.fn();
|
||||
const lock2Spy = jest.fn();
|
||||
const lock1Promise = mutex.lock({ exclusive: true }).then(x => {
|
||||
lock1Spy();
|
||||
return x;
|
||||
});
|
||||
const lock2Promise = mutex.lock({ exclusive: true }).then(x => {
|
||||
lock2Spy();
|
||||
return x;
|
||||
});
|
||||
|
||||
await delay();
|
||||
|
||||
expect(lock1Spy).toHaveBeenCalledTimes(1);
|
||||
expect(lock2Spy).not.toHaveBeenCalled();
|
||||
|
||||
const lock1 = await lock1Promise;
|
||||
lock1.release();
|
||||
await delay();
|
||||
|
||||
expect(lock2Spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const lock2 = await lock2Promise;
|
||||
lock2.release();
|
||||
});
|
||||
|
||||
it("acquire exclusive wait on shared lock", async () => {
|
||||
const lock1Spy = jest.fn();
|
||||
const lock2Spy = jest.fn();
|
||||
const lock1Promise = mutex.lock().then(x => {
|
||||
lock1Spy();
|
||||
return x;
|
||||
});
|
||||
const lock2Promise = mutex.lock({ exclusive: true }).then(x => {
|
||||
lock2Spy();
|
||||
return x;
|
||||
});
|
||||
|
||||
await delay();
|
||||
|
||||
expect(lock1Spy).toHaveBeenCalledTimes(1);
|
||||
expect(lock2Spy).not.toHaveBeenCalled();
|
||||
|
||||
const lock1 = await lock1Promise;
|
||||
lock1.release();
|
||||
await delay();
|
||||
|
||||
expect(lock2Spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const lock2 = await lock2Promise;
|
||||
lock2.release();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
import uuid from "uuid/v4";
|
||||
|
||||
export interface Lock {
|
||||
readonly id: string;
|
||||
readonly release: () => void;
|
||||
}
|
||||
|
||||
export interface LockOptions {
|
||||
exclusive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutex supporting shared and exclusive locks.
|
||||
* Exclusive locks have priority: If one is requested, following shared lock request will have to wait for the exclusive lock to be acquited and released
|
||||
*/
|
||||
export class Mutex {
|
||||
public get pending() {
|
||||
return !(
|
||||
this.sharedLocks.size === 0 &&
|
||||
this.exclusiveLocks.size === 0 &&
|
||||
this.sharedQueue.length === 0 &&
|
||||
this.exclusiveQueue.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
private readonly sharedLocks = new Set<string>();
|
||||
private readonly exclusiveLocks = new Set<string>();
|
||||
private readonly exclusiveQueue: Array<(lock: Lock) => void> = [];
|
||||
private readonly sharedQueue: Array<(lock: Lock) => void> = [];
|
||||
|
||||
public lock({ exclusive }: LockOptions = { exclusive: false }): Promise<Lock> {
|
||||
const promise = new Promise<Lock>(resolve =>
|
||||
exclusive ? this.exclusiveQueue.push(resolve) : this.sharedQueue.push(resolve),
|
||||
);
|
||||
this._dispatchNext();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
private _dispatchNext() {
|
||||
if (this.exclusiveQueue.length > 0) {
|
||||
if (this.sharedLocks.size === 0 && this.exclusiveLocks.size === 0) {
|
||||
const resolve = this.exclusiveQueue.shift();
|
||||
if (resolve) {
|
||||
const lock = this.createLock(this.exclusiveLocks);
|
||||
resolve(lock);
|
||||
}
|
||||
}
|
||||
} else if (this.sharedQueue.length > 0) {
|
||||
if (this.exclusiveLocks.size === 0) {
|
||||
const resolve = this.sharedQueue.shift();
|
||||
if (resolve) {
|
||||
const lock = this.createLock(this.sharedLocks);
|
||||
resolve(lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createLock(set: Set<string>) {
|
||||
const id = uuid();
|
||||
set.add(id);
|
||||
|
||||
return {
|
||||
id,
|
||||
release: () => {
|
||||
set.delete(id);
|
||||
this._dispatchNext();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { Lock, LockOptions, Mutex } from "./mutex";
|
||||
|
||||
/**
|
||||
* Mutex that also keeps a state depending on the lock
|
||||
*/
|
||||
export class StateMutex<State, SharedState extends State> {
|
||||
public state: State;
|
||||
|
||||
private mutex = new Mutex();
|
||||
|
||||
constructor(private readonly idleState: State, initialState: State) {
|
||||
this.state = initialState;
|
||||
}
|
||||
|
||||
public lock(state: SharedState, options?: { exclusive: false }): Promise<Lock>;
|
||||
public lock<T extends State>(state: T, options: { exclusive: true }): Promise<Lock>;
|
||||
public async lock(state: State, options?: LockOptions): Promise<Lock> {
|
||||
const lock = await this.mutex.lock(options);
|
||||
this.state = state;
|
||||
return {
|
||||
id: lock.id,
|
||||
release: () => {
|
||||
lock.release();
|
||||
if (!this.mutex.pending) {
|
||||
this.state = this.idleState;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,128 +1,113 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Remote, Repository } from "nodegit";
|
||||
import { Repository } from "nodegit";
|
||||
import path from "path";
|
||||
|
||||
import { GCRepo, RepoAuth } from "../../core";
|
||||
import { Configuration } from "../../config";
|
||||
import { Logger, RepoAuth } from "../../core";
|
||||
import { FSService } from "../fs";
|
||||
import { GitFetchService } from "../git-fetch";
|
||||
import { GitRemotePermission, PermissionService } from "../permission";
|
||||
import { RepoIndexService } from "../repo-index";
|
||||
import { LocalRepo, RemoteDef } from "./local-repo/local-repo";
|
||||
|
||||
export interface GitBaseOptions {
|
||||
auth?: RepoAuth;
|
||||
}
|
||||
|
||||
export interface RemoteDef {
|
||||
remote: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RepoService {
|
||||
public readonly repoCacheFolder = path.join(this.config.dataDir, "repos");
|
||||
|
||||
/**
|
||||
* Map that contains a key and promise when cloning a given repo
|
||||
*/
|
||||
private cloningRepos = new Map<string, Promise<GCRepo>>();
|
||||
private openedRepos = new Map<string, LocalRepo>();
|
||||
private deletingRepos = new Map<string, Promise<boolean>>();
|
||||
|
||||
private logger = new Logger(RepoService);
|
||||
|
||||
constructor(
|
||||
private config: Configuration,
|
||||
private fs: FSService,
|
||||
private fetchService: GitFetchService,
|
||||
private repoIndexService: RepoIndexService,
|
||||
private permissionService: PermissionService,
|
||||
) {}
|
||||
|
||||
public async use<T>(remote: string, options: GitBaseOptions, action: (repo: Repository) => Promise<T>): Promise<T> {
|
||||
const repo = await this.get(remote, options);
|
||||
return this.using(repo, action);
|
||||
}
|
||||
|
||||
public async using<T>(repo: GCRepo, action: (repo: Repository) => Promise<T>): Promise<T> {
|
||||
try {
|
||||
const response = await action(repo.instance);
|
||||
repo.unlock();
|
||||
return response;
|
||||
} catch (error) {
|
||||
repo.unlock();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Be carfull with using this one. Repository object needs to be clenup. Make sure its with `using` to ensure it gets cleanup after
|
||||
*/
|
||||
public async get(remote: string, options: GitBaseOptions = {}): Promise<GCRepo> {
|
||||
await this.validatePermissions([remote], options);
|
||||
const repoPath = this.getRepoMainPath(remote);
|
||||
|
||||
return this.loadRepo({
|
||||
repoPath,
|
||||
fetch: repo => this.fetchService.fetch(remote, repo, options),
|
||||
clone: () => this.fetchService.clone(remote, repoPath, options),
|
||||
});
|
||||
const origin = {
|
||||
name: "origin",
|
||||
remote,
|
||||
};
|
||||
return this.useWithRemotes(repoPath, options, [origin], action);
|
||||
}
|
||||
|
||||
public async createForCompare(base: RemoteDef, head: RemoteDef, options: GitBaseOptions = {}): Promise<GCRepo> {
|
||||
public async useForCompare<T>(
|
||||
base: RemoteDef,
|
||||
head: RemoteDef,
|
||||
options: GitBaseOptions = {},
|
||||
action: (repo: Repository) => Promise<T>,
|
||||
): Promise<T> {
|
||||
await this.validatePermissions([base.remote, head.remote], options);
|
||||
const localName = `${base.remote}-${head.remote}`;
|
||||
const repoPath = this.getRepoMainPath(localName, "compare");
|
||||
|
||||
const fetch = (repo: Repository) => this.fetchService.fetch(localName, repo, options);
|
||||
return this.loadRepo({
|
||||
repoPath,
|
||||
fetch,
|
||||
clone: async () => {
|
||||
const repo = await this.cloneWithMultiRemote(repoPath, [head, base]);
|
||||
await fetch(repo);
|
||||
return repo;
|
||||
},
|
||||
});
|
||||
return this.useWithRemotes(repoPath, options, [base, head], action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic repo loader that manage concurrency issue with cloning/fetching a repo
|
||||
*/
|
||||
private async loadRepo(config: {
|
||||
repoPath: string;
|
||||
fetch: (repo: Repository) => Promise<unknown>;
|
||||
clone: () => Promise<Repository>;
|
||||
}) {
|
||||
const repoPath = config.repoPath;
|
||||
const cloningRepo = this.cloningRepos.get(repoPath);
|
||||
if (cloningRepo) {
|
||||
return cloningRepo.then(x => {
|
||||
x.lock(); // Need to lock the repo as this object can be shared between requests
|
||||
return x;
|
||||
});
|
||||
public async deleteLocalRepo(repoPath: string): Promise<boolean> {
|
||||
const existingDeletion = this.deletingRepos.get(repoPath);
|
||||
if (existingDeletion) {
|
||||
return existingDeletion;
|
||||
}
|
||||
const exists = await this.fs.exists(repoPath);
|
||||
const isCloningRepo = this.cloningRepos.get(repoPath);
|
||||
if (isCloningRepo) {
|
||||
return isCloningRepo.then(x => {
|
||||
x.lock(); // Need to lock the repo as this object can be shared between requests
|
||||
return x;
|
||||
});
|
||||
if (this.openedRepos.has(repoPath)) {
|
||||
this.logger.info("Can't delete this repo has its opened");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
const repo = await Repository.open(repoPath);
|
||||
await config.fetch(repo);
|
||||
return new GCRepo(repo);
|
||||
const promise = this.fs
|
||||
.rm(repoPath)
|
||||
.then(() => true)
|
||||
.finally(() => {
|
||||
this.deletingRepos.delete(repoPath);
|
||||
this.repoIndexService.markRepoAsRemoved(repoPath);
|
||||
});
|
||||
|
||||
this.deletingRepos.set(repoPath, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
private async useWithRemotes<T>(
|
||||
repoPath: string,
|
||||
options: GitBaseOptions,
|
||||
remotes: RemoteDef[],
|
||||
action: (repo: Repository) => Promise<T>,
|
||||
): Promise<T> {
|
||||
this.repoIndexService.markRepoAsOpened(repoPath);
|
||||
let repo = this.openedRepos.get(repoPath);
|
||||
|
||||
// If repo is deleting wait for it to be deleted and reinit again
|
||||
const deletion = this.deletingRepos.get(repoPath);
|
||||
if (deletion) {
|
||||
await deletion;
|
||||
}
|
||||
|
||||
if (!repo) {
|
||||
repo = new LocalRepo(repoPath, this.fs, this.repoIndexService);
|
||||
this.openedRepos.set(repoPath, repo);
|
||||
repo.onDestroy.subscribe(() => {
|
||||
this.openedRepos.delete(repoPath);
|
||||
});
|
||||
await repo.init(remotes);
|
||||
} else {
|
||||
const cloneRepoPromise = config.clone().then(repo => {
|
||||
this.cloningRepos.delete(repoPath);
|
||||
return new GCRepo(repo);
|
||||
});
|
||||
this.cloningRepos.set(repoPath, cloneRepoPromise);
|
||||
return cloneRepoPromise;
|
||||
repo.ref();
|
||||
}
|
||||
}
|
||||
|
||||
private async cloneWithMultiRemote(repoPath: string, remotes: RemoteDef[]) {
|
||||
const repo = await Repository.init(repoPath, 0);
|
||||
// Remotes cannot be added in parrelel.
|
||||
for (const { name, remote } of remotes) {
|
||||
await Remote.create(repo, name, `https://${remote}`);
|
||||
if (this.repoIndexService.needToFetch(repoPath)) {
|
||||
await repo.update(options);
|
||||
}
|
||||
|
||||
return repo;
|
||||
const response = await repo.use(action);
|
||||
repo.unref();
|
||||
return response;
|
||||
}
|
||||
|
||||
public async validatePermissions(remotes: string[], options: GitBaseOptions) {
|
||||
|
@ -138,8 +123,8 @@ export class RepoService {
|
|||
|
||||
private getRepoMainPath(remote: string, namespace?: string) {
|
||||
if (namespace) {
|
||||
return path.join(this.fetchService.repoCacheFolder, namespace, encodeURIComponent(remote));
|
||||
return path.join(this.repoCacheFolder, namespace, encodeURIComponent(remote));
|
||||
}
|
||||
return path.join(this.fetchService.repoCacheFolder, encodeURIComponent(remote));
|
||||
return path.join(this.repoCacheFolder, encodeURIComponent(remote));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,22 @@
|
|||
export const notUndefined = <T>(x: T | undefined): x is T => x !== undefined;
|
||||
export const delay = (timeout?: number) => new Promise(r => setTimeout(r, timeout));
|
||||
|
||||
export class Deferred<T = void> {
|
||||
public promise: Promise<T>;
|
||||
public hasCompleted = false;
|
||||
public resolve!: T extends void ? () => void : (v: T) => void;
|
||||
public reject!: (e: unknown) => void;
|
||||
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this.reject = x => {
|
||||
this.hasCompleted = true;
|
||||
reject(x);
|
||||
};
|
||||
this.resolve = ((x: T) => {
|
||||
this.hasCompleted = true;
|
||||
resolve(x);
|
||||
}) as any;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче