Feature: List git commits with pagination (#31)

This commit is contained in:
Timothee Guerin 2019-06-04 14:09:01 -07:00 коммит произвёл GitHub
Родитель 8b5cae5f24
Коммит cf348a397d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
20 изменённых файлов: 590 добавлений и 156 удалений

Просмотреть файл

@ -8,6 +8,10 @@ COPY ./ ./
RUN npm ci && \
rm -f .npmrc
# Set environment to production
ENV NODE_ENV=production
RUN npm run build
CMD npm run start:prod

Просмотреть файл

@ -1,4 +1,8 @@
{
"compilerOptions": {
"rootDir": "../src",
"outDir": "../bin"
},
"extends": "../tsconfig.json",
"exclude": ["node_modules", "bin", "../test", "../src/**/*.test.ts", "../src/**/*.e2e.ts"]
}

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

@ -1,6 +1,6 @@
{
"name": "git-rest-api",
"version": "0.3.1",
"version": "0.3.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -384,9 +384,9 @@
}
},
"@nestjs/common": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-6.2.0.tgz",
"integrity": "sha512-0svQuCrG6ofMYgHzk/GzJ+vpIkLq4vZmTeww1qLipggS40IjQ0+NzwEBOTr6TeHVDmsM2YDmIU1DJiaGy2o9Hw==",
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-6.2.4.tgz",
"integrity": "sha512-YZvJ6/S7yVQZK+9rupCzMCg4tpbc9DyVvLoTx0NBDqExTCUNcNEcCtn0AZrO/hLqbeYODnJwGE2NxkH1R/qw+w==",
"requires": {
"axios": "0.18.0",
"cli-color": "1.4.0",
@ -415,6 +415,169 @@
"cors": "2.8.5",
"express": "4.16.4",
"multer": "1.4.1"
},
"dependencies": {
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
},
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
},
"express": {
"version": "4.16.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
"integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
"requires": {
"accepts": "~1.3.5",
"array-flatten": "1.1.1",
"body-parser": "1.18.3",
"content-disposition": "0.5.2",
"content-type": "~1.0.4",
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.1.1",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.4",
"qs": "6.5.2",
"range-parser": "~1.2.0",
"safe-buffer": "5.1.2",
"send": "0.16.2",
"serve-static": "1.13.2",
"setprototypeof": "1.1.0",
"statuses": "~1.4.0",
"type-is": "~1.6.16",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
"body-parser": {
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
"integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
"requires": {
"bytes": "3.0.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "~1.6.3",
"iconv-lite": "0.4.23",
"on-finished": "~2.3.0",
"qs": "6.5.2",
"raw-body": "2.3.3",
"type-is": "~1.6.16"
}
}
}
},
"finalhandler": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"statuses": "~1.4.0",
"unpipe": "~1.0.0"
}
},
"http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": ">= 1.4.0 < 2"
}
},
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"mime": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
"integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
},
"raw-body": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
"integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.3",
"iconv-lite": "0.4.23",
"unpipe": "1.0.0"
}
},
"send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
"integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.6.2",
"mime": "1.4.1",
"ms": "2.0.0",
"on-finished": "~2.3.0",
"range-parser": "~1.2.0",
"statuses": "~1.4.0"
}
},
"serve-static": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.2",
"send": "0.16.2"
}
},
"setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
},
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
}
}
},
"@nestjs/swagger": {
@ -2429,9 +2592,12 @@
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-security-policy-builder": {
"version": "2.0.0",
@ -2481,9 +2647,9 @@
}
},
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
@ -3267,103 +3433,46 @@
"integrity": "sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g=="
},
"express": {
"version": "4.16.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
"integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.5",
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.18.3",
"content-disposition": "0.5.2",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.3.1",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.1.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.4",
"qs": "6.5.2",
"range-parser": "~1.2.0",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.16.2",
"serve-static": "1.13.2",
"setprototypeof": "1.1.0",
"statuses": "~1.4.0",
"type-is": "~1.6.16",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
"body-parser": {
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
"integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
"requires": {
"bytes": "3.0.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "~1.6.3",
"iconv-lite": "0.4.23",
"on-finished": "~2.3.0",
"qs": "6.5.2",
"raw-body": "2.3.3",
"type-is": "~1.6.16"
}
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": ">= 1.4.0 < 2"
}
},
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"raw-body": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
"integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.3",
"iconv-lite": "0.4.23",
"unpipe": "1.0.0"
}
},
"setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
},
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
}
}
},
@ -3541,24 +3650,17 @@
}
},
"finalhandler": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"statuses": "~1.4.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"dependencies": {
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
}
}
},
"find-up": {
@ -5786,9 +5888,9 @@
}
},
"mime": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
"integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.40.0",
@ -7000,9 +7102,9 @@
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
},
"send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
"integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
@ -7011,46 +7113,30 @@
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.6.2",
"mime": "1.4.1",
"ms": "2.0.0",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.0",
"statuses": "~1.4.0"
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": ">= 1.4.0 < 2"
}
},
"setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
},
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.2",
"send": "0.16.2"
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"set-blocking": {
@ -7831,9 +7917,9 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typescript": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz",
"integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==",
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.1.tgz",
"integrity": "sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw==",
"dev": true
},
"uglify-js": {

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "git-rest-api",
"version": "0.3.1",
"version": "0.3.2",
"description": "This project welcomes contributions and suggestions. Most contributions require you to agree to a\r Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\r the rights to use your contribution. For details, visit https://cla.microsoft.com.",
"main": "bin/main.js",
"scripts": {
@ -50,10 +50,10 @@
"tslint": "^5.16.0",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "^3.4.5"
"typescript": "^3.5.1"
},
"dependencies": {
"@nestjs/common": "^6.2.0",
"@nestjs/common": "^6.2.4",
"@nestjs/core": "^6.2.0",
"@nestjs/platform-express": "^6.2.0",
"@nestjs/swagger": "^3.0.2",
@ -62,6 +62,7 @@
"chalk": "^2.4.2",
"class-validator": "^0.9.1",
"convict": "^5.0.0",
"express": "^4.17.1",
"fast-safe-stringify": "^2.0.6",
"helmet": "^3.18.0",
"node-fetch": "^2.6.0",

Просмотреть файл

@ -10,6 +10,9 @@ async function run() {
for (const branch of branches) {
console.log(` - ${branch.name} ${branch.commit.sha}`);
}
const response = await sdk.commits.list("github.com/Azure/BatchExplorer");
console.log("Total commits", response.xTotalCount);
}
run().catch(e => {

Просмотреть файл

@ -0,0 +1,17 @@
[
{
"sha": "04ccae1ed572f3cdc277c4df2ac73130efbbba3a",
"message": "Initial commit",
"author": {
"name": "Timothee Guerin",
"email": "timothee.guerin@outlook.com",
"date": "2019-05-23T23:44:59.000Z"
},
"committer": {
"name": "GitHub",
"email": "noreply@github.com",
"date": "2019-05-23T23:44:59.000Z"
},
"parents": []
}
]

Просмотреть файл

@ -0,0 +1,17 @@
import { TEST_REPO, e2eClient } from "../../../test/e2e";
describe("Test commits controller", () => {
it("List commits in the test repo", async () => {
const response = await e2eClient.fetch(`/repos/${TEST_REPO}/commits`);
expect(response.status).toEqual(200);
const content = await response.json();
expect(content).toMatchPayload("commit_list_master");
});
it("returns empty array if asking for page that doesn't exists", async () => {
const response = await e2eClient.fetch(`/repos/${TEST_REPO}/commits?page=999999`);
expect(response.status).toEqual(200);
const content = await response.json();
expect(content).toEqual([]);
});
});

Просмотреть файл

@ -1,7 +1,9 @@
import { Controller, Get, NotFoundException, Param } from "@nestjs/common";
import { ApiNotFoundResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";
import { Controller, Get, NotFoundException, Param, Query, Res } from "@nestjs/common";
import { ApiImplicitQuery, ApiNotFoundResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";
import { Response } from "express";
import { ApiHasPassThruAuth, Auth, RepoAuth } from "../../core";
import { ApiPaginated, Page, Pagination, applyPaginatedResponse } from "../../core/pagination";
import { GitCommit } from "../../dtos";
import { CommitService } from "../../services";
@ -9,9 +11,34 @@ import { CommitService } from "../../services";
export class CommitsController {
constructor(private commitService: CommitService) {}
@Get()
@ApiHasPassThruAuth()
@ApiNotFoundResponse({})
@ApiOperation({ title: "List commits", operationId: "commits_list" })
@ApiImplicitQuery({
name: "ref",
required: false,
description: "Reference to list the commits from. Can be a branch or a commit. Default to master",
type: String,
})
@ApiPaginated(GitCommit)
public async list(
@Param("remote") remote: string,
@Query("ref") ref: string | undefined,
@Auth() auth: RepoAuth,
@Page() pagination: Pagination,
@Res() response: Response,
) {
const commits = await this.commitService.list(remote, { auth, ref, pagination });
if (commits instanceof NotFoundException) {
throw commits;
}
return applyPaginatedResponse(commits, response);
}
@Get(":commitSha")
@ApiHasPassThruAuth()
@ApiOkResponse({ type: GitCommit, isArray: true })
@ApiOkResponse({ type: GitCommit })
@ApiNotFoundResponse({})
@ApiOperation({ title: "Get a commit", operationId: "commits_get" })
public async get(@Param("remote") remote: string, @Param("commitSha") commitSha: string, @Auth() auth: RepoAuth) {

Просмотреть файл

@ -1,2 +1,4 @@
export * from "./repo-auth";
export * from "./logger";
export * from "./pagination";
export * from "./models";

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

@ -0,0 +1 @@
export * from "./models-decorators";

Просмотреть файл

@ -0,0 +1,17 @@
import { ApiModelProperty } from "@nestjs/swagger";
import { SwaggerEnumType } from "@nestjs/swagger/dist/types/swagger-enum.type";
/**
* Decorator to let swagger know about all the pagination properties available
*/
export function ApiModelEnum(metadata: { [enumName: string]: SwaggerEnumType }): PropertyDecorator {
const enumName = Object.keys(metadata)[0];
if (!enumName) {
throw new Error("You must provide an enum to ApiModelEnum. `@ApiModelEnum({ Myenum })`");
}
return ApiModelProperty({
enum: metadata[enumName],
// Adding custom properties for auto rest
"x-ms-enum": { name: enumName },
} as any);
}

Просмотреть файл

@ -0,0 +1,3 @@
export * from "./pagination";
export * from "./pagination-decorators";
export * from "./paginated-response";

Просмотреть файл

@ -0,0 +1,34 @@
import { Response } from "express";
export const TOTAL_COUNT_HEADER = "x-total-count";
export interface PaginatedList<T> {
items: T[];
page: number;
perPage: number;
// Total number of items
total: number;
}
export function applyPaginatedResponse(attributes: PaginatedList<any>, response: Response) {
if (response.req) {
const originalUrl = `${response.req.protocol}://${response.req.get("host")}${response.req.originalUrl}`;
const nextUrl = getPageUrl(originalUrl, attributes.page + 1);
const lastUrl = getPageUrl(originalUrl, Math.ceil(attributes.total / attributes.perPage));
const links = [`<${nextUrl}>; rel="next"`, `<${lastUrl}>; rel="last"`];
if (attributes.page > 1) {
const prevUrl = getPageUrl(originalUrl, attributes.page - 1);
links.unshift(`<${prevUrl}>; rel="prev"`);
}
response.setHeader("Link", links.join(", "));
response.setHeader(TOTAL_COUNT_HEADER, attributes.total);
}
response.send(attributes.items);
}
function getPageUrl(originalUrl: string, page: number) {
const nextUrl = new URL(originalUrl);
nextUrl.searchParams.set("page", page.toString());
return nextUrl;
}

Просмотреть файл

@ -0,0 +1,42 @@
import { createParamDecorator } from "@nestjs/common";
import { ApiImplicitQuery, ApiResponse } from "@nestjs/swagger";
import { Request } from "express";
import { TOTAL_COUNT_HEADER } from "./paginated-response";
import { Pagination } from "./pagination";
/**
* Decorator to let swagger know about all the pagination properties available
*/
export function ApiPaginated(type: any): MethodDecorator {
const implicitpage = ApiImplicitQuery({ name: "page", required: false, type: String });
const response = ApiResponse({ status: 200, headers: paginationHeaders, type, isArray: true });
return (...args) => {
implicitpage(...args);
response(...args);
};
}
/**
* Auth param decorator for controller to inject the repo auth object
*/
export const Page = createParamDecorator(
(_, req: Request): Pagination => {
const page = parseInt(req.query.page, 10);
return {
page: isNaN(page) ? undefined : page,
};
},
);
const paginationHeaders = {
Link: {
type: "string",
description:
"Links to navigate pagination in the format defined by [RFC 5988](https://tools.ietf.org/html/rfc5988#section-5). It will include next, last, first and prev links if applicable",
},
[TOTAL_COUNT_HEADER]: {
type: "integer",
description: "Total count of items that can be retrieved",
},
};

Просмотреть файл

@ -0,0 +1,20 @@
export interface Pagination {
page?: number;
}
/**
* Return the given page for the given pagination
* @param pagination
*/
export function getPage(pagination: Pagination | undefined) {
return pagination === undefined || pagination.page === undefined ? 1 : Math.max(pagination.page, 1);
}
/**
* Return the number of items to skip using the given pagination object
* @param pagination
*/
export function getPaginationSkip(pagination: Pagination | undefined, perPage: number) {
const page = getPage(pagination);
return (page - 1) * perPage;
}

Просмотреть файл

@ -1,5 +1,7 @@
import { ApiModelProperty, ApiModelPropertyOptional } from "@nestjs/swagger";
import { ApiModelEnum } from "../core";
export enum PatchStatus {
Unmodified = "unmodified",
Modified = "modified",
@ -13,7 +15,7 @@ export class GitFileDiff {
public filename: string;
@ApiModelProperty()
public sha: string;
@ApiModelProperty({ enum: PatchStatus })
@ApiModelEnum({ PatchStatus })
public status: PatchStatus;
@ApiModelProperty()
public additions: number;

Просмотреть файл

@ -1,14 +1,39 @@
import { Injectable } from "@nestjs/common";
import { Commit, Oid, Repository, Signature, Time } from "nodegit";
import { Injectable, NotFoundException } from "@nestjs/common";
import { Commit, Oid, Repository, Revwalk, Signature, Time } from "nodegit";
import { PaginatedList, Pagination, getPage, getPaginationSkip } from "../../core";
import { GitCommit, GitCommitRef } from "../../dtos";
import { GitSignature } from "../../dtos/git-signature";
import { GitBaseOptions, RepoService } from "../repo";
const LIST_COMMIT_PAGE_SIZE = 100;
export interface ListCommitsOptions {
pagination?: Pagination;
ref?: string;
}
@Injectable()
export class CommitService {
constructor(private repoService: RepoService) {}
public async list(
remote: string,
options: ListCommitsOptions & GitBaseOptions = {},
): Promise<PaginatedList<GitCommit> | NotFoundException> {
const repo = await this.repoService.get(remote, options);
const commits = await this.listCommits(repo, options);
if (commits instanceof NotFoundException) {
return commits;
}
const items = await Promise.all(commits.items.map(async x => toGitCommit(x)));
return {
...commits,
items,
};
}
public async get(remote: string, commitSha: string, options: GitBaseOptions = {}): Promise<GitCommit | undefined> {
const repo = await this.repoService.get(remote, options);
const commit = await this.getCommit(repo, commitSha);
@ -40,6 +65,51 @@ export class CommitService {
}
}
}
public async getCommitOrDefault(repo: Repository, ref: string | undefined) {
if (ref) {
return this.getCommit(repo, ref);
} else {
const branch = await repo.getCurrentBranch();
const name = branch.shorthand();
return repo.getReferenceCommit(`origin/${name}`);
}
}
public async listCommits(
repo: Repository,
options: ListCommitsOptions,
): Promise<PaginatedList<Commit> | NotFoundException> {
const walk = repo.createRevWalk();
const page = getPage(options.pagination);
const commit = await this.getCommitOrDefault(repo, options.ref);
if (!commit) {
return new NotFoundException(`Couldn't find reference with name ${options.ref}`);
}
walk.push(commit.id());
const skip = getPaginationSkip(options.pagination, LIST_COMMIT_PAGE_SIZE);
await walkSkip(walk, skip);
const commits = await walk.getCommits(LIST_COMMIT_PAGE_SIZE);
let total = skip + LIST_COMMIT_PAGE_SIZE;
while (true) {
try {
await walk.next();
total++;
} catch (e) {
break;
}
}
return {
items: commits,
page,
total,
perPage: LIST_COMMIT_PAGE_SIZE,
};
}
}
export async function toGitCommit(commit: Commit): Promise<GitCommit> {
@ -80,3 +150,17 @@ export function getSignature(sig: Signature): GitSignature {
export function getDateFromTime(time: Time): Date {
return new Date(time.time() * 1000);
}
/**
* Try to skip the given number of item in the walk.
* If there is less than ask remaining it will just stop gracfully
*/
async function walkSkip(revwalk: Revwalk, skip: number) {
for (let i = 0; i < skip; i++) {
try {
await revwalk.next();
} catch {
return;
}
}
}

Просмотреть файл

@ -78,6 +78,78 @@
]
}
},
"/repos/{remote}/commits": {
"get": {
"summary": "List commits",
"operationId": "commits_list",
"parameters": [
{
"type": "string",
"name": "remote",
"required": true,
"in": "path"
},
{
"name": "page",
"required": false,
"in": "query",
"type": "string"
},
{
"name": "ref",
"required": false,
"in": "query",
"description": "Reference to list the commits from. Can be a branch or a commit. Default to master",
"type": "string"
},
{
"name": "x-authorization",
"required": false,
"in": "header",
"type": "string"
},
{
"name": "x-github-token",
"required": false,
"in": "header",
"type": "string"
}
],
"responses": {
"200": {
"headers": {
"Link": {
"type": "string",
"description": "Links to navigate pagination in the format defined by [RFC 5988](https://tools.ietf.org/html/rfc5988#section-5). It will include next, last, first and prev links if applicable"
},
"x-total-count": {
"type": "integer",
"description": "Total count of items that can be retrieved"
}
},
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/GitCommit"
}
}
},
"400": {
"description": "When the x-authorization header is malformed"
},
"404": {
"description": ""
}
},
"produces": [
"application/json"
],
"consumes": [
"application/json"
]
}
},
"/repos/{remote}/commits/{commitSha}": {
"get": {
"summary": "Get a commit",
@ -112,11 +184,8 @@
"200": {
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/GitCommit"
}
}
},
"400": {
"description": "When the x-authorization header is malformed"
@ -338,7 +407,10 @@
"added",
"deleted",
"renamed"
]
],
"x-ms-enum": {
"name": "PatchStatus"
}
},
"additions": {
"type": "number"

Просмотреть файл

@ -66,7 +66,7 @@ function toMatchSpecificSnapshot(
}
if (fs.existsSync(filepath)) {
const output = fs.readFileSync(filepath, "utf8");
const output = fs.readFileSync(filepath, "utf8").replace(/\r\n/g, "\n");
// The matcher is being used with `.not`
if (output === content) {
this.snapshotState.matched++;

Просмотреть файл

@ -11,8 +11,6 @@
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"rootDir": "src",
"outDir": "bin",
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "incremental": true, /* Enable incremental compilation */