Feature: Repo indexing + repo state machine to manage concurrent requests correctly (#51)

This commit is contained in:
Timothee Guerin 2019-07-08 08:34:55 -07:00 коммит произвёл GitHub
Родитель ed0c3ef077
Коммит d73350e3f6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
36 изменённых файлов: 1463 добавлений и 329 удалений

403
package-lock.json сгенерированный
Просмотреть файл

@ -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;
}

1
src/services/db/index.ts Normal file
Просмотреть файл

@ -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;
});
}
}