#19 #20

Closed
kgorbunov wants to merge 112 commits from kgorbunov/pkgdash:#19 into master
156 changed files with 159196 additions and 42 deletions
Showing only changes of commit 8e633fe83f - Show all commits

12
.gitea/pkgdashcli.yaml Normal file
View File

@ -0,0 +1,12 @@
branches:
- master
source:
type: gitea
apiurl: git.unistack.org
repository: pkgdash
owner: kgorbunov
update_opt:
pre: false
major: false
up_major: false
cached: true

27
.gitignore vendored
View File

@ -1,7 +1,6 @@
# ---> Go
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Mac OS X files
*.DS_Store
# Binaries for programs and plugins
*.exe
*.exe~
@ -9,15 +8,25 @@
*.so
*.dylib
# Test binary, built with `go test -c`
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
# Go workspace file
go.work
# Dependency directories
vendor/
bin/
.idea
.vscode
.env
tmp/
cmd/pkgdash/pkgdash
cmd/pkgdashcli/pkgdashcli
*.sqlite
*.db

54
LICENSE
View File

@ -2,35 +2,35 @@ Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
@ -42,31 +42,31 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright 2023 unistack-org
Copyright 2023 unistack-org
Licensed under the Apache License, Version 2.0 (the "License");
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and

22
Makefile Normal file
View File

@ -0,0 +1,22 @@
.PHONY: build
build:
GOWORK=off CGO_ENABLED=0 go build -o bin/pkgdash -mod=readonly go.unistack.org/pkgdash/cmd/pkgdash
GOWORK=off CGO_ENABLED=0 go build -o bin/pkgdashcli -mod=readonly go.unistack.org/pkgdash/cmd/pkgdashcli
.PHONY: buildcli
buildcli:
CGO_ENABLED=0 go build -o bin/app -mod=readonly go.unistack.org/pkgdash/cmd/pkgdashcli
.PHONY: cli
cli:
go install go.unistack.org/pkgdash/cmd/pkgdashcli
.PHONY: test
test:
go test -v ./... -race -cover
.PHONY: lint
lint:
# Install the binary release of golangci-lint
# https://github.com/golangci/golangci-lint#install
golangci-lint run

View File

@ -1,2 +1,47 @@
# pkgdash
# pkgdashcli
## Installation
Use any of the following for a pain-free installation:
* If you have [`go`](https://go.dev/doc/install) installed, you can run:
```shell
go install git.unistack.org/unistack-org/pkgdash/cmd/pkgdashcli@latest
```
## Config
Before starting, you should fill out the configuration file.
The file must be located in the .gitea, .gitlab, .github directory
according to the git type you are using.
- branches - array of main branches
- source - information about the version control system
- source.type - type of version control system
- source.apiurl - path to the version control system api
- source.repository - repository name
- source.owner - username of the repository owner
- update_opt - dependency update options
- update_opt.pre - update to pre-release versions
- update_opt.major - compare only major versions in modules
- update_opt.up_major - raise the version according to major
- update_opt.cached - caching
### Config.Example
```yaml
# .gitea/pkgdashcli.yaml
branches: [master]
source:
type: gitea
apiurl: git.unistack.org
repository: pkgdash
owner: unistack
update_opt:
pre: false
major: false
up_major: false
cached: true
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 948 B

View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>UI</title>
<base href="/ui/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="styles.css"></head>
<body class="mat-typography">
<app-root></app-root>
<script src="runtime.js" type="module"></script><script src="polyfills.js" type="module"></script><script src="vendor.js" type="module"></script><script src="main.js" type="module"></script></body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,163 @@
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({});
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/chunk loaded */
/******/ (() => {
/******/ var deferred = [];
/******/ __webpack_require__.O = (result, chunkIds, fn, priority) => {
/******/ if(chunkIds) {
/******/ priority = priority || 0;
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
/******/ deferred[i] = [chunkIds, fn, priority];
/******/ return;
/******/ }
/******/ var notFulfilled = Infinity;
/******/ for (var i = 0; i < deferred.length; i++) {
/******/ var [chunkIds, fn, priority] = deferred[i];
/******/ var fulfilled = true;
/******/ for (var j = 0; j < chunkIds.length; j++) {
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
/******/ chunkIds.splice(j--, 1);
/******/ } else {
/******/ fulfilled = false;
/******/ if(priority < notFulfilled) notFulfilled = priority;
/******/ }
/******/ }
/******/ if(fulfilled) {
/******/ deferred.splice(i--, 1)
/******/ var r = fn();
/******/ if (r !== undefined) result = r;
/******/ }
/******/ }
/******/ return result;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "runtime": 0
/******/ };
/******/
/******/ // no chunk on demand loading
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ __webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/ var [chunkIds, moreModules, runtime] = data;
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0;
/******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) var result = runtime(__webpack_require__);
/******/ }
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ installedChunks[chunkId][0]();
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ return __webpack_require__.O(result);
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunkui"] = self["webpackChunkui"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ })();
/******/
/************************************************************************/
/******/
/******/
/******/ })()
;
//# sourceMappingURL=runtime.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

121153
cmd/pkgdash/assets/ui/vendor.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

237
cmd/pkgdash/main.go Normal file
View File

@ -0,0 +1,237 @@
package main
import (
"context"
"crypto/tls"
"embed"
"io/fs"
"net/http"
"time"
jsoncodec "go.unistack.org/micro-codec-json/v3"
jsonpbcodec "go.unistack.org/micro-codec-jsonpb/v3"
yamlcodec "go.unistack.org/micro-codec-yaml/v3"
envconfig "go.unistack.org/micro-config-env/v3"
fileconfig "go.unistack.org/micro-config-file/v3"
vaultconfig "go.unistack.org/micro-config-vault/v3"
victoriameter "go.unistack.org/micro-meter-victoriametrics/v3"
httpsrv "go.unistack.org/micro-server-http/v3"
healthhandler "go.unistack.org/micro-server-http/v3/handler/health"
meterhandler "go.unistack.org/micro-server-http/v3/handler/meter"
spahandler "go.unistack.org/micro-server-http/v3/handler/spa"
swaggerui "go.unistack.org/micro-server-http/v3/handler/swagger-ui"
"go.unistack.org/micro/v3"
"go.unistack.org/micro/v3/config"
"go.unistack.org/micro/v3/logger"
slog "go.unistack.org/micro/v3/logger/slog"
"go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v3/server"
rutil "go.unistack.org/micro/v3/util/reflect"
appconfig "go.unistack.org/pkgdash/internal/config"
"go.unistack.org/pkgdash/internal/database"
"go.unistack.org/pkgdash/internal/handler"
"go.unistack.org/pkgdash/internal/storage"
_ "go.unistack.org/pkgdash/internal/storage/sqlite"
"go.unistack.org/pkgdash/internal/worker"
pb "go.unistack.org/pkgdash/proto"
)
const appName = "pkgdash"
var (
BuildDate string = "now" // filled when build
AppVersion string = "latest" // filled when build
)
//go:generate rm -rf assets
//go:generate mkdir assets
//go:generate cp -vr ../../ui/dist/ui assets/
//go:embed assets/*
var assets embed.FS
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
log := slog.NewLogger(logger.WithLevel(logger.DebugLevel))
if err := log.Init(); err != nil {
log.Fatal(ctx, "failed to init logger")
}
cfg := appconfig.NewConfig(appName, AppVersion) // create new empty config
vc := vaultconfig.NewConfig(
config.AllowFail(true), // that may be not exists
config.Struct(cfg), // load from vault
config.Codec(jsoncodec.NewCodec()), // vault config in json
config.BeforeLoad(func(ctx context.Context, c config.Config) error {
return c.Init(
vaultconfig.HTTPClient(&http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}),
vaultconfig.Address(cfg.Vault.Addr),
vaultconfig.Timeout(5*time.Second),
vaultconfig.Token(cfg.Vault.Token),
vaultconfig.Path(cfg.Vault.Path),
)
}),
)
if err := config.Load(ctx,
[]config.Config{
config.NewConfig( // load from defaults
config.Struct(cfg), // pass config struct
),
fileconfig.NewConfig( // load from file
config.AllowFail(true), // that may be not exists
config.Struct(cfg), // pass config struct
config.Codec(yamlcodec.NewCodec()), // file config in json
fileconfig.Path("./local.yaml"), // nearby file
),
envconfig.NewConfig( // load from environment
config.Struct(cfg), // pass config struct
),
vc,
}, config.LoadOverride(true),
); err != nil {
log.Fatal(ctx, "failed to load config: %v", err)
}
if err := config.Validate(ctx, cfg); err != nil {
log.Fatal(ctx, "failed to validate config: %v", err)
}
swaggerui.Config["url"] = "../service.swagger.yaml"
meter.DefaultMeter = victoriameter.NewMeter(
meter.Path(cfg.Meter.Path),
meter.WriteFDMetrics(true),
meter.WriteProcessMetrics(true),
meter.Address(cfg.Meter.Addr),
)
svc := micro.NewService()
if err := svc.Init(
micro.Server(httpsrv.NewServer()),
micro.Name(cfg.Server.Name),
micro.Version(cfg.Server.Version),
); err != nil {
log.Fatal(ctx, "failed to init service: %v", err)
}
assetsUI, err := fs.Sub(assets, "assets/ui")
if err != nil {
log.Fatal(ctx, "failed to get assets: %v", err)
}
if err := svc.Server("http").Init(
server.Address(cfg.Server.Addr),
server.Name(cfg.Server.Name),
server.Version(cfg.Server.Version),
server.Codec("application/json", jsonpbcodec.NewCodec()),
httpsrv.PathHandler(http.MethodGet, "/ui/*", spahandler.Handler("/ui/", assetsUI)),
httpsrv.PathHandler(http.MethodHead, "/ui/*", spahandler.Handler("/ui/", assetsUI)),
httpsrv.PathHandler(http.MethodGet, "/swagger-ui/*", swaggerui.Handler("/swagger-ui")),
); err != nil {
log.Fatal(ctx, "failed to init service: %v", err)
}
if err := database.ParseDSN(cfg.Database); err != nil {
log.Fatal(ctx, "failed to init database: %v", err)
}
db, err := database.Connect(ctx, cfg.Database, log)
if err != nil {
log.Fatal(ctx, "failed to connect database: %v", err)
}
store, err := storage.NewStorage(cfg.Database.Type, log, db)
if err != nil {
log.Fatal(ctx, "failed to init storage: %v", err)
}
h, err := handler.NewHandler(log, store)
if err != nil {
log.Fatal(ctx, "failed to create handler: %v", err)
}
if err := svc.Init(
micro.Logger(
log.Clone(logger.WithLevel(logger.ParseLevel(cfg.Server.LoggerLevel))),
),
); err != nil {
log.Fatal(ctx, "failed to init service", err)
}
if err := pb.RegisterPkgdashServer(svc.Server("http"), h); err != nil {
log.Fatal(ctx, "failed to register handler", err)
}
intsvc := httpsrv.NewServer(
server.Codec("application/json", jsoncodec.NewCodec()),
server.Address(cfg.Meter.Addr),
)
if err := intsvc.Init(); err != nil {
log.Fatal(ctx, "failed to init http srv: %v", err)
}
if err := healthhandler.RegisterHealthServiceServer(intsvc, healthhandler.NewHandler()); err != nil {
log.Fatal(ctx, "failed to set http handler: %v", err)
}
if err := meterhandler.RegisterMeterServiceServer(intsvc, meterhandler.NewHandler()); err != nil {
log.Fatal(ctx, "failed to set http handler: %v", err)
}
if err := intsvc.Start(); err != nil {
log.Fatal(ctx, "failed to run http srv: %v", err)
}
cw, err := vc.Watch(ctx, config.WatchCoalesce(true), config.WatchInterval(1*time.Second, 5*time.Second))
if err != nil {
log.Fatal(ctx, "failed to watch config: %v", err)
}
defer func() {
if err := cw.Stop(); err != nil {
log.Error(ctx, err.Error())
}
}()
go func() {
for {
changes, err := cw.Next()
if err != nil {
log.Error(ctx, "failed to get config update: %v", err)
}
for k, v := range changes {
if err = rutil.SetFieldByPath(cfg, v, k); err != nil {
log.Error(ctx, "failed to set config update: %v", err)
break
}
}
if err == nil {
for k := range changes {
switch k {
case "Server.LoggerLevel":
if lvl, ok := changes[k].(string); ok {
log.Info(ctx, "logger level changed to %s", lvl)
log.Level(logger.ParseLevel(lvl))
}
}
}
}
}
}()
go func() {
worker.Run(ctx, log, store, time.Duration(cfg.App.CheckInterval))
}()
if err = svc.Run(); err != nil {
log.Fatal(ctx, "failed to run svc: %v", err)
}
}

756
cmd/pkgdashcli/main.go Normal file
View File

@ -0,0 +1,756 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
stdslog "log/slog"
"net/http"
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/go-git/go-git/v5"
gitconfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
httpauth "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/jdx/go-netrc"
yamlcodec "go.unistack.org/micro-codec-yaml/v3"
envconfig "go.unistack.org/micro-config-env/v3"
fileconfig "go.unistack.org/micro-config-file/v3"
microflag "go.unistack.org/micro-config-flag/v3"
"go.unistack.org/micro/v3/config"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v3/logger/slog"
"go.unistack.org/pkgdash/internal/configcli"
"go.unistack.org/pkgdash/internal/modules"
"go.unistack.org/pkgdash/internal/source"
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
)
// https://docs.github.com/ru/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
var initMsg = `
Pkgdashcli allows you to define a version update for a dependency and start
merge requests in version control systems.
Usage:
pkgdashcli --command {{command}} --path {{name of dep}}
Commands:
checkupdate | CheckUpdate collects a list of dependencies with the latest updates.
list | Returns a list of PR for this repository with update dependencies.
update --path {{name of one dep or empty for update all dep}} | Creates a PR with the specified dependency update in path or creates a PR with dependency updates for all modules if path is empty.
close --path {{name of one dep or empty for close all pr}} | Closes the PR for the specified dependency or closes all PRs with dependency updates if path is empty .
Flags:
--command | The command to execute
--path | The name of the module to create/close the PR, if empty, the command is executed for all modules.
`
var (
DefaultPullRequestTitle = `Bump {{.Name}} from {{.VersionOld}} to {{.VersionNew}}`
DefaultPullRequestBody = `Bumps {{.Name}} from {{.VersionOld}} to {{.VersionNew}}`
)
var (
configFiles = []string{
"dependabot.yml",
"pkgdashcli.yml",
"pkgdashcli.yaml",
}
configDirs = []string{
".gitea",
".github",
".gitlab",
}
repoMgmt = map[string]string{
".gitea": "gitea",
".gogs": "gogs",
".github": "github",
".gitlab": "gitlab",
}
repoAPI = map[string]string{
".gitea": "git.unistack.org",
".gogs": "gogs",
".github": "github.com/unistack-org",
".gitlab": "gitlab.mtsbank.ru",
}
)
type Data struct {
Modules map[string]modules.Update
}
func main() {
var err error
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
log := slog.NewLogger(slog.WithHandlerFunc(stdslog.NewTextHandler))
if err = log.Init(logger.WithLevel(logger.DebugLevel)); err != nil {
log.Error(ctx, fmt.Sprintf("logger init error: %v", err))
}
cfg := configcli.NewConfig()
if err = config.Load(ctx,
[]config.Config{
config.NewConfig(
config.Struct(cfg),
),
envconfig.NewConfig(
config.Struct(cfg),
),
},
config.LoadOverride(true),
); err != nil {
log.Fatal(ctx, fmt.Sprintf("failed to load config: %v", err))
}
for _, configDir := range configDirs {
for _, configFile := range configFiles {
path := filepath.Join(configDir, configFile)
if _, err = os.Stat(path); os.IsNotExist(err) {
continue
}
c := fileconfig.NewConfig(
config.AllowFail(false),
config.Struct(cfg),
config.Codec(yamlcodec.NewCodec()),
fileconfig.Path(path),
)
err = c.Init()
if err != nil {
log.Error(ctx, fmt.Sprintf("failed to init config: %v", err))
}
if err = c.Load(ctx, config.LoadOverride(true)); err != nil {
log.Error(ctx, fmt.Sprintf("failed to load config: %v", err))
}
}
}
if cfg.PullRequestBody == "" {
cfg.PullRequestBody = DefaultPullRequestBody
}
if cfg.PullRequestTitle == "" {
cfg.PullRequestTitle = DefaultPullRequestTitle
}
cliCfg := &configcli.Cli{}
c := microflag.NewConfig(config.Struct(cliCfg), microflag.FlagErrorHandling(flag.ContinueOnError))
if err = c.Init(); err != nil {
log.Fatal(ctx, fmt.Sprintf("init cli cfg failed: %v", err))
}
if err = c.Load(ctx); err != nil {
log.Fatal(ctx, fmt.Sprintf("load cli cfg failed: %v", err))
}
if cliCfg.Path == "" && cliCfg.Command == "" {
fmt.Print(initMsg)
return
}
path := "."
if len(os.Args) > 1 {
path = os.Args[1]
}
name, err := modules.FindModFile(path)
if err != nil {
panic(err)
}
buf, err := os.ReadFile(name)
if err != nil {
log.Fatal(ctx, "failed to read file", err)
}
mfile, err := modfile.Parse(name, buf, nil)
if err != nil {
log.Fatal(ctx, "failed to parse file", err)
}
mvs := make(map[string]modules.Update)
updateOptions := modules.UpdateOptions{
Pre: cfg.UpdateOpt.Pre,
Major: cfg.UpdateOpt.Major,
UpMajor: cfg.UpdateOpt.UpMajor,
Cached: cfg.UpdateOpt.Cached,
OnUpdate: func(u modules.Update) {
var modpath string // new mod path with major
if u.Err != nil {
log.Error(ctx, fmt.Sprintf("%s: failed: %v", u.Module.Path, u.Err))
return
}
modpath = u.Module.Path
v := semver.Major(u.Version)
p := modules.ModPrefix(modpath)
if !strings.HasPrefix(u.Module.Version, v) && v != "v1" && v != "v0" {
switch strings.HasPrefix(u.Module.Path, "gopkg.in") {
case true:
modpath = p + "." + v
case false:
modpath = p + "/" + v
}
}
mvs[modpath] = u
},
}
for _, req := range mfile.Require {
updateOptions.Modules = append(updateOptions.Modules, req.Mod)
}
modules.Updates(updateOptions)
if err = getRepoMgmt(ctx, log, cfg); err != nil { // Filling in empty config fields.
log.Error(ctx, err.Error())
}
if len(cfg.Branches) == 0 {
branchName, err := getCurrentBranch(ctx)
if err != nil {
log.Fatal(ctx, "failed to get current branch", err)
}
cfg.Branches = append(cfg.Branches, branchName)
}
if cfg.Source.Owner == "" {
owner, err := getOwnerRepository(ctx)
if err != nil {
log.Fatal(ctx, "failed to get current repository", err)
}
cfg.Source.Owner = owner
}
if cfg.Source.Repository == "" {
repository, err := getCurrentRepository(ctx)
if err != nil {
log.Fatal(ctx, "failed to get current repository", err)
}
cfg.Source.Repository = repository
}
gitSource := source.NewSourceControl(*cfg, log)
Execute(ctx, log, gitSource, mvs, *cliCfg, *cfg)
log.Info(ctx, "Pkgdash successfully updated dependencies")
}
func Execute(ctx context.Context, log logger.Logger, gitSource source.SourceControl, mvs map[string]modules.Update, cliCfg configcli.Cli, cfg configcli.Config) {
var mod modules.Update
var ok bool
var path string
prList := make(map[string]map[string]string)
switch cliCfg.Command {
case "checkupdate":
js, err := json.Marshal(mvs)
fmt.Println(fmt.Sprintf(`Modules get update: %s, %s`, js, err))
case "open":
if cliCfg.Path != "" { // update one dep
path = cliCfg.Path
if mod, ok = mvs[path]; !ok {
log.Fatal(ctx, fmt.Sprintf("For %s update not exist", path))
}
log.Debug(ctx, fmt.Sprintf("Start update %s from %s to %s", path, mod.Module.Version, mod.Version))
for _, branch := range cfg.Branches {
if err := gitSource.RequestOpen(ctx, branch, path, mod); err != nil {
log.Fatal(ctx, fmt.Sprintf("failed to create pr: %v", err))
}
}
log.Debug(ctx, fmt.Sprintf("Update successful for %s", path))
return
}
for _, branch := range cfg.Branches { // update all dep
for path, mod = range mvs {
log.Debug(ctx, fmt.Sprintf("Start update %s from %s to %s", path, mod.Module.Version, mod.Version))
err := gitSource.RequestOpen(ctx, branch, path, mod)
if err != nil {
if strings.Contains(err.Error(), "already exists") {
log.Debug(ctx, fmt.Sprintf("skip %s, branch already exists", path))
continue
}
log.Fatal(ctx, fmt.Sprintf("failed to create pr: %v", err))
}
log.Debug(ctx, fmt.Sprintf("Update successful for %s", path))
}
}
case "update":
if cliCfg.Path != "" { // update one dep
path = cliCfg.Path
if mod, ok = mvs[path]; !ok {
log.Fatal(ctx, fmt.Sprintf("For %s update not exist", path))
}
log.Debug(ctx, fmt.Sprintf("Start update %s from %s to %s", path, mod.Module.Version, mod.Version))
for _, branch := range cfg.Branches {
if err := gitSource.RequestUpdate(ctx, branch, path, mod); err != nil {
log.Fatal(ctx, fmt.Sprintf("failed to create pr: %v", err))
}
}
log.Debug(ctx, fmt.Sprintf("Update successful for %s", path))
return
}
for _, branch := range cfg.Branches { // update all dep
for path, mod = range mvs {
log.Debug(ctx, fmt.Sprintf("Start update %s from %s to %s", path, mod.Module.Version, mod.Version))
err := gitSource.RequestUpdate(ctx, branch, path, mod)
if err != nil {
if strings.Contains(err.Error(), "already exists") {
log.Debug(ctx, fmt.Sprintf("skip %s, branch already exists", path))
continue
}
log.Fatal(ctx, fmt.Sprintf("failed to create pr: %v", err))
}
log.Debug(ctx, fmt.Sprintf("Update successful for %s", path))
}
}
case "close":
if cliCfg.Path != "" { // close one dep
path = cliCfg.Path
log.Debug(ctx, fmt.Sprintf("Start close for %s", path))
for _, branch := range cfg.Branches {
if err := gitSource.RequestClose(ctx, branch, path); err != nil {
log.Fatal(ctx, fmt.Sprintf("failed to close pr: %v", err))
}
}
log.Debug(ctx, fmt.Sprintf("Close successful for %s", path))
return
}
for _, branch := range cfg.Branches {
log.Info(ctx, fmt.Sprintf("Start getting pr for %s", branch))
rMap, err := gitSource.RequestList(ctx, branch)
if err != nil {
log.Fatal(ctx, fmt.Sprintf("Error with getting pr list for branch: %s", branch))
}
log.Info(ctx, fmt.Sprintf("for %s:\n%s", branch, rMap))
log.Info(ctx, fmt.Sprintf("Start close pr for base branch %s", branch))
for path = range rMap {
log.Debug(ctx, fmt.Sprintf("Start close for %s", path))
if err = gitSource.RequestClose(ctx, branch, path); err != nil {
log.Fatal(ctx, fmt.Sprintf("failed to close pr: %v", err))
}
log.Debug(ctx, fmt.Sprintf("Close successful for %s", path))
}
}
case "list":
for _, branch := range cfg.Branches {
rMap, err := gitSource.RequestList(ctx, branch)
if err != nil {
log.Fatal(ctx, fmt.Sprintf("RequestList: error %s", err))
}
prList[branch] = rMap
}
js, err := json.Marshal(prList)
if err != nil {
log.Error(ctx, fmt.Sprintf("error: %s", err))
}
fmt.Printf("for %s:\n%s\n", cfg.Source.Repository, js)
default:
fmt.Print(initMsg)
}
}
func getCurrentRepository(ctx context.Context) (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
p := filepath.Clean(wd)
repo, err := git.PlainOpen(p)
if err != nil {
return "", err
}
cfg, err := repo.Config()
if err != nil {
return "", err
}
for k, v := range cfg.Remotes {
if k != "origin" {
continue
}
u, err := url.Parse(v.URLs[0])
if err != nil {
continue
}
return filepath.Base(u.Path), nil
}
return "", fmt.Errorf("failed to get remotes")
}
func getOwnerRepository(ctx context.Context) (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
p := filepath.Clean(wd)
repo, err := git.PlainOpen(p)
if err != nil {
return "", err
}
cfg, err := repo.Config()
if err != nil {
return "", err
}
for k, v := range cfg.Remotes {
if k != "origin" {
continue
}
u, err := url.Parse(v.URLs[0])
if err != nil {
continue
}
return filepath.Base(filepath.Dir(u.Path)), nil
}
return "", fmt.Errorf("failed to get remotes")
}
func getCurrentBranch(ctx context.Context) (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
p := filepath.Clean(wd)
repo, err := git.PlainOpen(p)
if err != nil {
return "", err
}
ref, err := repo.Head()
if err != nil {
return "", err
}
return ref.Name().Short(), nil
}
func getRepoMgmt(ctx context.Context, log logger.Logger, cfg *configcli.Config) error {
wd, err := os.Getwd()
if err != nil {
return err
}
p := filepath.Clean(wd)
for _, configDir := range configDirs {
_, err := os.Stat(filepath.Join(p, configDir))
if err != nil {
continue
}
log.Info(ctx, fmt.Sprintf("check config dir %s", configDir))
if name, ok := repoMgmt[configDir]; ok && cfg.Source.TypeGit == "" {
cfg.Source.TypeGit = name
}
if api, ok := repoAPI[configDir]; ok && cfg.Source.APIURL == "" {
cfg.Source.APIURL = api
}
}
if p == "/" && cfg.Source.TypeGit == "" && cfg.Source.APIURL == "" {
return fmt.Errorf("unknown")
}
// p = filepath.Clean(filepath.Join(p, ".."))
usr, err := user.Current()
if err != nil {
log.Fatal(ctx, fmt.Sprintf("pkgdash/main cant get info about user: %s", err))
}
log.Info(ctx, fmt.Sprintf("try to configure scm source %v", cfg.Source))
netrcfile := filepath.Join(usr.HomeDir, ".netrc")
log.Info(ctx, "try to parse netrc file "+netrcfile)
n, err := netrc.Parse(netrcfile)
if err != nil {
log.Error(ctx, "pkgdash/main cant parse .netrc: %s", err)
}
log.Info(ctx, "try to configure scm for "+cfg.Source.APIURL)
if cfg.Source.Username == "" {
cfg.Source.Username = n.Machine(cfg.Source.APIURL).Get("login")
}
if cfg.Source.Password == "" {
cfg.Source.Password = n.Machine(cfg.Source.APIURL).Get("password")
}
return nil
}
func giteaPullRequest(ctx context.Context, log logger.Logger, cfg *configcli.Config, branch string, mods map[string]modules.Update) error {
envAPIURL := os.Getenv("GITHUB_API_URL")
envREPOSITORY := os.Getenv("GITHUB_REPOSITORY")
envTOKEN := os.Getenv("GITHUB_TOKEN")
var buf []byte
var err error
tplTitle, err := template.New("pull_request_title").Parse(cfg.PullRequestTitle)
if err != nil {
log.Fatal(ctx, "failed to parse template: %v", err)
}
wTitle := bytes.NewBuffer(nil)
tplBody, err := template.New("pull_request_body").Parse(cfg.PullRequestBody)
if err != nil {
log.Fatal(ctx, "failed to parse template: %v", err)
}
wBody := bytes.NewBuffer(nil)
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
log.Fatal(ctx, "failed to open repo: %v", err)
}
if err = repo.FetchContext(ctx, &git.FetchOptions{
Auth: &httpauth.BasicAuth{Username: envTOKEN, Password: envTOKEN},
Force: true,
}); err != nil && err != git.NoErrAlreadyUpToDate {
log.Fatal(ctx, "failed to fetch repo: %v", err)
}
var headRef *plumbing.Reference
refIter, err := repo.Branches()
if err != nil {
log.Fatal(ctx, "failed to get branches: %v", err)
}
for {
ref, err := refIter.Next()
if err != nil {
break
}
if ref.Name().String() == branch {
headRef = ref
break
}
}
refIter.Close()
if headRef == nil {
log.Fatal(ctx, "failed to get repo branch head")
}
log.Info(ctx, "repo head %s", headRef)
wtree, err := repo.Worktree()
if err != nil {
log.Fatal(ctx, "failed to get worktree: %v", err)
}
type giteaPull struct {
URL string `json:"url"`
Title string `json:"title"`
Base struct {
Ref string `json:"ref"`
} `json:"base"`
ID int64 `json:"id"`
}
var pulls []*giteaPull
req, err := http.NewRequestWithContext(ctx, http.MethodGet, envAPIURL+"/repos/"+envREPOSITORY+"/pulls?state=open&token="+envTOKEN, nil)
if err != nil {
return err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
rsp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
buf, _ = io.ReadAll(rsp.Body)
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("unknown error: %s", buf)
}
if err = json.Unmarshal(buf, &pulls); err != nil {
log.Fatal(ctx, "failed to decode response %s err: %v", buf, err)
}
for path := range mods {
for _, pull := range pulls {
if strings.Contains(pull.Title, path) && pull.Base.Ref == branch {
log.Info(ctx, "skip %s as pr already exists %s", path, pull.URL)
delete(mods, path)
}
}
}
for path, mod := range mods {
wTitle.Reset()
wBody.Reset()
log.Info(ctx, "update %s from %s to %s", path, mod.Module.Version, mod.Version)
log.Info(ctx, "reset worktree")
if err = wtree.Reset(&git.ResetOptions{Mode: git.HardReset}); err != nil {
log.Fatal(ctx, "failed to reset repo branch: %v", err)
}
if err = wtree.PullContext(ctx, &git.PullOptions{
Auth: &httpauth.BasicAuth{Username: envTOKEN, Password: envTOKEN},
Depth: 1,
// RemoteURL :
Force: true,
RemoteName: "origin",
}); err != nil && err != git.NoErrAlreadyUpToDate {
log.Fatal(ctx, "failed to pull repo: %v", err)
}
log.Info(ctx, "checkout ref %s", headRef)
if err = wtree.Checkout(&git.CheckoutOptions{
Hash: headRef.Hash(),
Branch: plumbing.NewBranchReferenceName(fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version)),
Create: true,
Force: true,
}); err != nil {
log.Fatal(ctx, "failed to checkout tree: %v", err)
}
epath, err := exec.LookPath("go")
if errors.Is(err, exec.ErrDot) {
err = nil
}
if err != nil {
log.Fatal(ctx, "failed to find go command: %v", err)
}
var cmd *exec.Cmd
var out []byte
cmd = exec.CommandContext(ctx, epath, "mod", "edit", fmt.Sprintf("-require=%s@%s", path, mod.Version))
if out, err = cmd.CombinedOutput(); err != nil {
log.Fatal(ctx, "failed to run go mod edit: %s err: %v", out, err)
}
cmd = exec.CommandContext(ctx, epath, "mod", "tidy")
if out, err = cmd.CombinedOutput(); err != nil {
log.Fatal(ctx, "failed to run go mod tidy: %s err: %v", out, err)
}
log.Info(ctx, "worktree add go.mod")
if _, err = wtree.Add("go.mod"); err != nil {
log.Fatal(ctx, "failed to add file: %v", err)
}
log.Info(ctx, "worktree add go.sum")
if _, err = wtree.Add("go.sum"); err != nil {
log.Fatal(ctx, "failed to add file: %v", err)
}
log.Info(ctx, "worktree commit")
_, err = wtree.Commit(wTitle.String(), &git.CommitOptions{
Parents: []plumbing.Hash{headRef.Hash()},
Author: &object.Signature{
Name: "gitea-actions",
Email: "info@unistack.org",
When: time.Now(),
},
})
if err != nil {
log.Fatal(ctx, "failed to commit: %v", err)
}
// newref := plumbing.NewHashReference(plumbing.ReferenceName(fmt.Sprintf("refs/heads/pkgdash/go_modules/%s-%s", path, mod.Version)), headRef.Hash())
/*
if err = repo.Storer.SetReference(newref); err != nil {
log.Fatal(ctx, "failed to create repo branch: %v", err)
}
*/
refspec := gitconfig.RefSpec(fmt.Sprintf("+refs/heads/pkgdash/go_modules/%s-%s:refs/heads/pkgdash/go_modules/%s-%s", path, mod.Version, path, mod.Version))
log.Info(ctx, "try to push refspec %s", refspec)
if err = repo.PushContext(ctx, &git.PushOptions{
RefSpecs: []gitconfig.RefSpec{refspec},
Auth: &httpauth.BasicAuth{Username: envTOKEN, Password: envTOKEN},
Force: true,
}); err != nil {
log.Fatal(ctx, "failed to push repo branch: %v", err)
}
data := map[string]string{
"Name": path,
"VersionOld": mod.Module.Version,
"VersionNew": mod.Version,
}
if err = tplTitle.Execute(wTitle, data); err != nil {
log.Fatal(ctx, "failed to execute template: %v", err)
}
if err = tplBody.Execute(wBody, data); err != nil {
log.Fatal(ctx, "failed to execute template: %v", err)
}
body := map[string]string{
"base": branch,
"body": wBody.String(),
"head": fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version),
"title": wTitle.String(),
}
log.Info(ctx, "raw body: %#+v", body)
buf, err = json.Marshal(body)
if err != nil {
return err
}
log.Info(ctx, "marshal body: %s", buf)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, envAPIURL+"/repos/"+envREPOSITORY+"/pulls?token="+envTOKEN, bytes.NewReader(buf))
if err != nil {
return err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
rsp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if rsp.StatusCode != http.StatusCreated {
buf, _ = io.ReadAll(rsp.Body)
return fmt.Errorf("unknown error: %s", buf)
}
}
return nil
}

12
generate.go Normal file
View File

@ -0,0 +1,12 @@
//go:build tools
// +build tools
package main
//go:generate ./generate.sh
import (
_ "github.com/envoyproxy/protoc-gen-validate"
_ "go.unistack.org/micro-proto/v3"
_ "go.unistack.org/protoc-gen-go-micro/v3"
)

15
generate.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/sh -ex
PROTO_ARGS=" \
--proto_path=$(go list -f '{{ .Dir }}' -m github.com/envoyproxy/protoc-gen-validate) \
--proto_path=$(go list -f '{{ .Dir }}' -m go.unistack.org/micro-proto/v3) \
--go_out=paths=source_relative:./proto \
--go-micro_out=paths=source_relative,components=micro|http,standalone=false:./proto \
--validate_out=paths=source_relative,lang=go:./proto \
--go-micro_out=components="openapiv3",openapi_file=./apidocs.swagger.yaml,debug=true,paths=source_relative:./proto
"
find ./proto -type f -name "*.pb.go" -delete
protoc -I./proto $PROTO_ARGS ./proto/*.proto || find ./proto -type f -name "*.pb.go" -delete
#./ui/node_modules/.bin/ng-openapi-gen -i ./proto/apidocs.swagger.yaml -o ./ui/src/app/api --removeStaleFiles true --ignoreUnusedModels false

125
go.mod Normal file
View File

@ -0,0 +1,125 @@
module go.unistack.org/pkgdash
go 1.22.7
toolchain go1.23.3
require (
github.com/envoyproxy/protoc-gen-validate v1.1.0
github.com/go-git/go-git/v5 v5.12.1-0.20241206065855-b2aea86f9eef
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.5.4
github.com/jdx/go-netrc v1.0.0
github.com/jmoiron/sqlx v1.3.5
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.10.0
go.unistack.org/micro-client-http/v3 v3.9.14
go.unistack.org/micro-codec-json/v3 v3.10.1
go.unistack.org/micro-codec-jsonpb/v3 v3.10.3
go.unistack.org/micro-codec-yaml/v3 v3.10.2
go.unistack.org/micro-config-env/v3 v3.8.7
go.unistack.org/micro-config-file/v3 v3.8.10
go.unistack.org/micro-config-flag/v3 v3.8.11
go.unistack.org/micro-config-vault/v3 v3.8.9
go.unistack.org/micro-meter-victoriametrics/v3 v3.8.9
go.unistack.org/micro-proto/v3 v3.4.1
go.unistack.org/micro-server-http/v3 v3.11.37
go.unistack.org/micro/v3 v3.11.12
go.unistack.org/protoc-gen-go-micro/v3 v3.10.10
golang.org/x/mod v0.22.0
golang.org/x/sync v0.10.0
golang.org/x/tools v0.28.0
google.golang.org/protobuf v1.35.2
modernc.org/sqlite v1.29.5
)
require (
github.com/KimMachineGun/automemlimit v0.6.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cilium/ebpf v0.9.1 // indirect
github.com/containerd/cgroups/v3 v3.0.1 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cyphar/filepath-securejoin v0.3.5 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/godbus/dbus/v5 v5.0.4 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jackc/pgx/v4 v4.18.3 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/opencontainers/runtime-spec v1.0.2 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.unistack.org/metrics v0.0.1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/grpc v1.68.0 // indirect
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
)
require (
dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.0 // indirect
github.com/golang-migrate/migrate/v4 v4.18.1
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/gnostic v0.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/vault/api v1.15.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgtype v1.14.3 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/crypto v0.30.0 // indirect
golang.org/x/net v0.32.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.7.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.49.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

1988
go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,95 @@
package coverage
import (
"context"
"fmt"
"io"
"strings"
"go.unistack.org/pkgdash/internal/models"
"golang.org/x/tools/cover"
)
func Analyze(ctx context.Context, dataCoverage io.Reader, pack models.Package) (float64, error) {
calculcate, err := calculateFiles(dataCoverage)
if err != nil {
return 0, err
}
mapCover := make(map[string]float64)
{
tree, err := GetTreeFromGit(ctx, pack.URL)
if err != nil {
return 0, err
}
list, err := tree.GoFileList("")
if err != nil {
return 0, err
}
for _, f := range list {
mapCover[f] = 0.0
}
}
cur := len(mapCover)
for _, d := range calculcate.Files {
file := strings.TrimPrefix(d.Name, pack.Name+"/")
mapCover[file] = d.Coverage
}
// check)
if len(mapCover) != cur {
fmt.Printf("add new keys, was: %d, has: %d", cur, len(mapCover))
}
// TODO add calculate full
return 0, nil
}
type Data struct {
Files []*calculateFile
Set bool
}
type calculateFile struct {
Name string
Coverage float64
}
func calculateFiles(coverSrc io.Reader) (d *Data, err error) {
profiles, err := cover.ParseProfilesFromReader(coverSrc)
if err != nil {
return nil, err
}
d = new(Data)
for _, profile := range profiles {
fn := profile.FileName
if profile.Mode == "set" {
d.Set = true
}
d.Files = append(d.Files, &calculateFile{
Name: fn,
Coverage: percentCovered(profile),
})
}
return d, err
}
func percentCovered(p *cover.Profile) float64 {
var total, covered int64
for _, b := range p.Blocks {
total += int64(b.NumStmt)
if b.Count > 0 {
covered += int64(b.NumStmt)
}
}
if total == 0 {
return 0
}
return float64(covered) / float64(total) * 100
}

View File

@ -0,0 +1,37 @@
package coverage
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/assert"
"go.unistack.org/pkgdash/internal/models"
)
func Test_Calculate(t *testing.T) {
file, err := os.Open("cover_test.out")
assert.Nil(t, err)
defer func() {
assert.Nil(t, file.Close())
}()
dataFiles, err := calculateFiles(file)
assert.Nil(t, err)
assert.NotNil(t, dataFiles)
}
func Test_Analyze(t *testing.T) {
file, err := os.Open("cover_test.out")
assert.Nil(t, err)
defer func() {
assert.Nil(t, file.Close())
}()
analyze, err := Analyze(context.Background(), file, models.Package{
Name: "go.unistack.org/micro/v3",
URL: "https://git.unistack.org/unistack-org/micro.git",
})
assert.Nil(t, err)
assert.Equal(t, analyze, 0.0)
}

View File

@ -0,0 +1,70 @@
package coverage
import (
"context"
"errors"
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/memory"
"os"
"regexp"
"strings"
)
var (
fileNil = errors.New("file pointer is nil")
)
func GetTreeFromGit(ctx context.Context, url string) (*Tree, error) {
cloneOpts := &git.CloneOptions{
URL: url,
Progress: os.Stdout,
}
repo, err := git.CloneContext(ctx, memory.NewStorage(), nil, cloneOpts)
if err != nil {
return nil, err
}
ref, err := repo.Head()
if err != nil {
return nil, fmt.Errorf("failed to get head: %v", err)
}
commit, err := repo.CommitObject(ref.Hash())
if err != nil {
return nil, fmt.Errorf("failed to get commit: %v", err)
}
tree, err := commit.Tree()
return &Tree{tree}, err
}
type Tree struct {
*object.Tree
}
func (t Tree) GoFileList(pattern string) ([]string, error) {
matcher, err := regexp.Compile(pattern)
if err != nil {
return nil, err
}
var list []string
err = t.Files().ForEach(func(file *object.File) error {
if file == nil {
return fileNil
}
if file.Mode == filemode.Regular && strings.HasSuffix(file.Name, ".go") && !strings.HasSuffix(file.Name, "_test.go") && matcher.MatchString(file.Name) {
list = append(list, file.Name)
}
return nil
})
if err != nil {
return nil, err
}
return list, err
}

70
internal/config/config.go Normal file
View File

@ -0,0 +1,70 @@
package config
import (
"time"
mtime "go.unistack.org/micro/v3/util/time"
)
type AppConfig struct {
CheckInterval mtime.Duration `json:"check_interval" yaml:"check_interval" default:"1d"`
}
type ServerConfig struct {
Name string `json:"name" yaml:"name"`
Version string `json:"-" yaml:"-"`
Addr string `json:"addr" yaml:"addr" default:":9090"`
Crt string `json:"crt" yaml:"crt"`
Key string `json:"key" yaml:"key"`
ID string `json:"-" yaml:"-" default:"micro:generate uuid"`
LoggerLevel string `json:"logger_level" yaml:"logger_level"`
}
type TracerConfig struct {
Metadata map[string]string `json:"metadata" yaml:"metadata"`
AgentHost string `env:"JAEGER_AGENT_HOST" json:"host" yaml:"host" default:"127.0.0.1"`
AgentPort string `env:"JAEGER_AGENT_PORT" json:"port" yaml:"port" default:"6831"`
Collector string `env:"JAEGER_ENDPOINT,TRACER_ENDPOINT" json:"endpoint" yaml:"endpoint"`
}
type VaultConfig struct {
Addr string `env:"VAULT_ADDR" json:"addr" yaml:"addr" default:"http://127.0.0.1:8200"`
Token string `env:"VAULT_TOKEN" json:"-" yaml:"-"`
Path string `env:"VAULT_PATH" json:"-" yaml:"-" default:"pkgdash/data/pkgdash"`
}
type MeterConfig struct {
Addr string `json:"addr" yaml:"addr" default:"0.0.0.0:8080"`
Path string `json:"path" yaml:"path" default:"/metrics"`
}
type DatabaseConfig struct {
DSN string `json:"dsn" yaml:"dsn"`
Type string `json:"-" yaml:"-"`
Migrate string `json:"-" yaml:"-"`
ConnStr string `json:"-" yaml:"-"`
MaxOpenConns int `json:"-" yaml:"-"`
MaxIdleConns int `json:"-" yaml:"-"`
ConnMaxLifetime time.Duration `json:"-" yaml:"-"`
ConnMaxIdleTime time.Duration `json:"-" yaml:"-"`
MigrateForce bool `json:"-" yaml:"-"`
}
type Config struct {
App *AppConfig `json:"app" yaml:"app"`
Database *DatabaseConfig `json:"database" yaml:"database"`
Server *ServerConfig `json:"server" yaml:"server"`
Meter *MeterConfig `json:"meter" yaml:"meter"`
Vault *VaultConfig `json:"-" yaml:"-"`
Tracer *TracerConfig `json:"tracer" yaml:"tracer"`
}
func NewConfig(name, version string) *Config {
return &Config{
App: &AppConfig{},
Server: &ServerConfig{Name: name, Version: version},
Tracer: &TracerConfig{},
Meter: &MeterConfig{},
Vault: &VaultConfig{},
}
}

View File

@ -0,0 +1,37 @@
package configcli
type Config struct {
PullRequestTitle string `json:"pull_request_title" yaml:"pull_request_title"`
PullRequestBody string `json:"pull_request_body" yaml:"pull_request_body"`
Branches []string `json:"branches" yaml:"branches"`
Source *Source `json:"source" yaml:"source"`
UpdateOpt *UpdateOpt `json:"update_opt" yaml:"update_opt"`
}
type Source struct {
TypeGit string `json:"type" yaml:"type" env:"GIT_TYPE"`
Username string `json:"username" yaml:"username" env:"GIT_USERNAME"`
Password string `json:"password" yaml:"password" env:"GIT_PASSWORD,GIT_TOKEN"`
APIURL string `json:"apiurl" yaml:"apiurl" env:"GIT_API"`
Repository string `json:"repository" yaml:"repository" env:"GIT_REPO"`
Owner string `json:"owner" yaml:"owner" env:"GIT_OWNER"`
}
type UpdateOpt struct {
Pre bool `json:"pre" yaml:"pre" default:"false"`
Major bool `json:"major" yaml:"major" default:"false"`
UpMajor bool `json:"up_major" yaml:"up_major" default:"false"`
Cached bool `json:"cached" yaml:"cached" default:"true"`
}
type Cli struct {
Command string `flag:"name=command,desc='choice command(update, close, checkupdaue, list)',default=''"`
Path string `flag:"name=path,desc='title of mod',default=''"`
}
func NewConfig() *Config {
return &Config{
Source: &Source{},
UpdateOpt: &UpdateOpt{},
}
}

View File

@ -0,0 +1,253 @@
package database
import (
"context"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
mpgx "github.com/golang-migrate/migrate/v4/database/pgx"
msqlite "github.com/golang-migrate/migrate/v4/database/sqlite"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/stdlib"
"github.com/jmoiron/sqlx"
"go.unistack.org/micro/v3/logger"
appconfig "go.unistack.org/pkgdash/internal/config"
_ "modernc.org/sqlite"
)
func ParseDSN(cfg *appconfig.DatabaseConfig) error {
var err error
u, err := url.Parse(cfg.DSN)
if err != nil {
return err
}
values := u.Query()
var value string
if value = values.Get("conn_max"); value != "" {
values.Del("conn_max")
maxOpenConns, err := strconv.Atoi(value)
if err != nil {
return err
}
cfg.MaxOpenConns = maxOpenConns
cfg.MaxIdleConns = maxOpenConns / 2
}
if value = values.Get("conn_maxidle"); value != "" {
values.Del("conn_maxidle")
maxIdleConns, err := strconv.Atoi(value)
if err != nil {
return err
}
cfg.MaxIdleConns = maxIdleConns
}
if value = values.Get("conn_lifetime"); value != "" {
values.Del("conn_lifetime")
connMaxLifetime, err := time.ParseDuration(value)
if err != nil {
return err
}
cfg.ConnMaxLifetime = connMaxLifetime
}
if value = values.Get("conn_maxidletime"); value != "" {
values.Del("conn_maxidletime")
connMaxIdleTime, err := time.ParseDuration(value)
if err != nil {
return err
}
cfg.ConnMaxIdleTime = connMaxIdleTime
}
if mtype := values.Get("migrate"); mtype != "" {
values.Del("migrate")
cfg.Migrate = mtype
}
switch u.Scheme {
case "postgres", "pgsql", "postgresql":
u.Scheme = "postgres"
case "sqlite", "sqlite3":
u.Scheme = "sqlite"
default:
return fmt.Errorf("unknown database %s", u.Scheme)
}
cfg.Type = u.Scheme
u.RawQuery = values.Encode()
cfg.ConnStr = u.String()
return nil
}
func connect(ctx context.Context, cfg *appconfig.DatabaseConfig, log logger.Logger) (*sqlx.DB, error) {
var db *sqlx.DB
var err error
log.Info(ctx, "connect to %s", cfg.Type)
switch cfg.Type {
case "postgres", "pgsql", "postgresql":
db, err = connectPostgres(ctx, cfg.ConnStr)
cfg.Type = "postgres"
case "sqlite", "sqlite3":
db, err = connectSqlite(ctx, cfg.ConnStr)
cfg.Type = "sqlite"
default:
return nil, fmt.Errorf("unknown database type %s", cfg.Type)
}
if err != nil {
return nil, err
}
return db, nil
}
func Connect(ctx context.Context, cfg *appconfig.DatabaseConfig, log logger.Logger) (*sqlx.DB, error) {
db, err := connect(ctx, cfg, log)
if err != nil {
return nil, err
}
m, err := migratePrepare(ctx, db, log, cfg.Type)
if err != nil {
return nil, err
}
switch cfg.Migrate {
case "":
break
case "up":
log.Info(ctx, "migrate up")
err = m.Up()
case "down":
log.Info(ctx, "migrate down")
err = m.Down()
case "seed":
log.Info(ctx, "migrate seed")
if err = m.Drop(); err == nil {
err = m.Up()
}
default:
log.Info(ctx, "migrate version")
v, verr := strconv.ParseUint(cfg.Type, 10, 64)
if verr != nil {
return nil, err
}
err = m.Migrate(uint(v))
}
if err == nil || err == migrate.ErrNoChange {
srcerr, dberr := m.Close()
if srcerr != nil {
err = srcerr
} else if dberr != nil {
err = dberr
} else {
err = nil
}
}
if err == nil {
db, err = connect(ctx, cfg, log)
}
if err != nil {
return nil, err
}
db.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)
db.SetConnMaxLifetime(cfg.ConnMaxLifetime)
db.SetMaxIdleConns(cfg.MaxIdleConns)
db.SetMaxOpenConns(cfg.MaxOpenConns)
return db, nil
}
func connectSqlite(ctx context.Context, connstr string) (*sqlx.DB, error) {
if !strings.Contains(connstr, ":memory:") {
return sqlx.ConnectContext(ctx, "sqlite", "file:"+connstr[9:])
}
return sqlx.ConnectContext(ctx, "sqlite", connstr[9:])
}
func connectPostgres(ctx context.Context, connstr string) (*sqlx.DB, error) {
// parse connection string
dbConf, err := pgx.ParseConfig(connstr)
if err != nil {
return nil, err
}
// needed for pgbouncer
dbConf.RuntimeParams = map[string]string{
"standard_conforming_strings": "on",
"application_name": "authn",
}
// may be needed for pbbouncer, needs to check
// dbConf.PreferSimpleProtocol = true
// register pgx conn
connStr := stdlib.RegisterConnConfig(dbConf)
db, err := sqlx.ConnectContext(ctx, "pgx", connStr)
if err != nil {
return nil, err
}
return db, nil
}
func migratePrepare(ctx context.Context, db *sqlx.DB, log logger.Logger, dbtype string) (*migrate.Migrate, error) {
var driver database.Driver
var err error
switch dbtype {
case "postgres":
driver, err = mpgx.WithInstance(db.DB, &mpgx.Config{
DatabaseName: "pkgdash",
MigrationsTable: "schema_migrations",
})
case "sqlite":
driver, err = msqlite.WithInstance(db.DB, &msqlite.Config{
DatabaseName: "pkgdash",
MigrationsTable: "schema_migrations",
})
}
if err != nil {
return nil, err
}
source, err := iofs.New(assets, "migrations/"+dbtype)
if err != nil {
return nil, err
}
m, err := migrate.NewWithInstance("fs", source, "apigw", driver)
if err != nil {
return nil, err
}
m.Log = &mLog{ctx: ctx, l: log}
return m, nil
}
type mLog struct {
ctx context.Context
l logger.Logger
}
func (l *mLog) Verbose() bool {
return l.l.V(logger.DebugLevel)
}
func (l *mLog) Printf(format string, v ...interface{}) {
l.l.Info(l.ctx, format, v...)
}

View File

@ -0,0 +1,8 @@
package database
import (
"embed"
)
//go:embed migrations
var assets embed.FS

View File

@ -0,0 +1 @@
drop table if exists dashboard, package, module, issue, comment;

View File

@ -0,0 +1,39 @@
create table if not exists dashboard (
id serial not null unique primary key ,
"uuid" uuid not null unique default gen_random_uuid() ,
package integer[] default '{}'::integer[]
);
create table if not exists comment (
id serial not null unique primary key ,
"text" text ,
package integer not null,
created timestamp not null default current_timestamp ,
updated timestamp default current_timestamp
);
create table if not exists module (
id serial not null unique primary key ,
name varchar not null ,
version varchar not null
);
create table if not exists issue (
id serial not null unique primary key ,
--package integer references package(id) ,
modules integer[] default '{}'::integer[],
status integer default 0 ,
"desc" varchar
);
create table if not exists package (
id serial not null unique primary key ,
name varchar not null ,
url varchar ,
modules integer[] default '{}'::integer[],
issues integer[] default '{}'::integer[],
comments integer[] default '{}'::integer[]
);
create unique index module_info on module(name, version);

View File

@ -0,0 +1,5 @@
drop table if exists packages;
drop table if exists modules;
drop table if exists issues;
drop table if exists comments;
drop table if exists handlers;

View File

@ -0,0 +1,57 @@
create table if not exists comments (
id integer primary key autoincrement not null,
comment text,
package integer not null,
created timestamp not null default current_timestamp,
updated timestamp not null default current_timestamp
);
create table if not exists issues (
id integer primary key autoincrement not null,
status integer default 0,
comment varchar,
created timestamp not null default current_timestamp,
updated timestamp not null default current_timestamp
);
create table if not exists handlers (
id integer primary key autoincrement not null,
package integer not null,
name varchar,
coverage number default 0
);
create table if not exists packages (
id integer primary key autoincrement not null,
name varchar not null,
url varchar not null,
description varchar,
modules integer default 0,
issues integer default 0,
comments integer default 0,
coverage number default 0,
created timestamp not null default current_timestamp,
updated timestamp not null default current_timestamp,
status integer default 1,
last_check timestamp
);
CREATE UNIQUE INDEX IF NOT EXISTS unique_idx_url on packages (url);
create table if not exists modules (
id integer primary key autoincrement not null,
name varchar not null,
version varchar not null,
last_check timestamp not null default current_timestamp
);
CREATE UNIQUE INDEX IF NOT EXISTS unique_idx_name_version on modules (name,version);
create table if not exists packages_modules (
id integer primary key autoincrement not null,
package integer,
module integer not null
);
CREATE UNIQUE INDEX IF NOT EXISTS unique_idx_package_module on packages_modules (package,module);

View File

@ -0,0 +1,39 @@
package handler
import (
"context"
"database/sql"
"errors"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v3"
"go.unistack.org/pkgdash/internal/models"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) CommentCreate(ctx context.Context, req *pb.CommentCreateReq, rsp *pb.CommentCreateRsp) error {
h.logger.Debug(ctx, "Start AddComment")
err := req.Validate()
if err != nil {
h.logger.Error(ctx, "validation error", err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
var com *models.Comment
if com, err = h.store.CommentCreate(ctx, req); err != nil {
if errors.Is(err, sql.ErrNoRows) {
httpsrv.SetRspCode(ctx, http.StatusNotFound)
return httpsrv.SetError(NewNotFoundError(err))
}
h.logger.Error(ctx, "comment create error", err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
rsp.Comment = models.NewComment(com)
h.logger.Debug(ctx, "Success finish addComment")
return nil
}

View File

@ -0,0 +1,36 @@
package handler
import (
"context"
"database/sql"
"errors"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v3"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) CommentDelete(ctx context.Context, req *pb.CommentDeleteReq, rsp *pb.CommentDeleteRsp) error {
h.logger.Debug(ctx, "Start AddComment")
err := req.Validate()
if err != nil {
h.logger.Error(ctx, "validate error", err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
if err = h.store.CommentDelete(ctx, req); err != nil {
if errors.Is(err, sql.ErrNoRows) {
httpsrv.SetRspCode(ctx, http.StatusNotFound)
return httpsrv.SetError(NewNotFoundError(err))
}
h.logger.Error(ctx, "comment delete error", err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
h.logger.Debug(ctx, "Success finish addComment")
return nil
}

View File

@ -0,0 +1,35 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v3"
"go.unistack.org/pkgdash/internal/models"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) CommentList(ctx context.Context, req *pb.CommentListReq, rsp *pb.CommentListRsp) error {
h.logger.Debug(ctx, "Start GetModule")
err := req.Validate()
if err != nil {
h.logger.Error(ctx, "validate error", err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
comments, err := h.store.CommentList(ctx, req)
if err != nil {
h.logger.Error(ctx, "comment list error", err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
for _, com := range comments {
rsp.Comments = append(rsp.Comments, models.NewComment(com))
}
h.logger.Debug(ctx, "Success finish getModule")
return nil
}

View File

@ -0,0 +1,11 @@
package handler
import (
"context"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) CommentLookup(ctx context.Context, req *pb.CommentLookupReq, rsp *pb.CommentLookupRsp) error {
return nil
}

View File

@ -0,0 +1,56 @@
package handler
import (
"net/http"
"strconv"
"github.com/google/uuid"
jsonpbcodec "go.unistack.org/micro-codec-jsonpb/v3"
"go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/pkgdash/internal/storage"
pb "go.unistack.org/pkgdash/proto"
)
type Handler struct {
logger logger.Logger
store storage.Storage
codec codec.Codec
}
func NewNotFoundError(err error) *pb.ErrorRsp {
return &pb.ErrorRsp{
Code: strconv.Itoa(http.StatusBadRequest),
Title: "NotFound",
Uuid: uuid.New().String(),
Details: err.Error(),
}
}
func NewInternalError(err error) *pb.ErrorRsp {
return &pb.ErrorRsp{
Code: strconv.Itoa(http.StatusInternalServerError),
Title: "InternalServerError",
Uuid: uuid.New().String(),
Details: err.Error(),
}
}
func NewValidationError(err error) *pb.ErrorRsp {
return &pb.ErrorRsp{
Code: strconv.Itoa(http.StatusBadRequest),
Title: "BadRequest",
Uuid: uuid.New().String(),
Details: err.Error(),
}
}
func NewHandler(log logger.Logger, store storage.Storage) (*Handler, error) {
h := &Handler{
logger: log,
codec: jsonpbcodec.NewCodec(),
store: store,
}
return h, nil
}

View File

@ -0,0 +1,27 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v3"
"go.unistack.org/pkgdash/internal/models"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) HandlerList(ctx context.Context, req *pb.HandlerListReq, rsp *pb.HandlerListRsp) error {
h.logger.Debug(ctx, "HandlerList handler start")
packages, err := h.store.HandlerList(ctx, req)
if err != nil {
h.logger.Error(ctx, "error db response: %v", err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
for _, hdlr := range packages {
rsp.Handlers = append(rsp.Handlers, models.NewHandler(hdlr))
}
h.logger.Debug(ctx, "HandlerList handler stop")
return nil
}

View File

@ -0,0 +1,34 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v3"
"go.unistack.org/pkgdash/internal/models"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) ModuleList(ctx context.Context, req *pb.ModuleListReq, rsp *pb.ModuleListRsp) error {
h.logger.Debug(ctx, "Start GetModule")
err := req.Validate()
if err != nil {
h.logger.Error(ctx, "validate error", err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
modules, err := h.store.ModuleList(ctx, req)
if err != nil {
h.logger.Error(ctx, "module list error", err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
for _, mod := range modules {
rsp.Modules = append(rsp.Modules, models.NewModule(mod))
}
h.logger.Debug(ctx, "Success finish getModule")
return nil
}

View File

@ -0,0 +1,32 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v3"
"go.unistack.org/pkgdash/internal/models"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) PackageCreate(ctx context.Context, req *pb.PackageCreateReq, rsp *pb.PackageCreateRsp) error {
h.logger.Debug(ctx, "PackagesCreate handler start")
if err := req.Validate(); err != nil {
h.logger.Error(ctx, "validate error", err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
pkg, err := h.store.PackageCreate(ctx, req)
if err != nil {
h.logger.Error(ctx, "package create error", err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
rsp.Package = models.NewPackage(pkg)
h.logger.Debug(ctx, "PackagesCreate handler stop")
return nil
}

View File

@ -0,0 +1,28 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v3"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) PackageDelete(ctx context.Context, req *pb.PackageDeleteReq, rsp *pb.PackageDeleteRsp) error {
h.logger.Debug(ctx, "Start UpdatePackage")
if err := req.Validate(); err != nil {
h.logger.Error(ctx, "validate error", err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
if err := h.store.PackageDelete(ctx, req); err != nil {
h.logger.Error(ctx, "package delete error", err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
h.logger.Debug(ctx, "Success finish UpdatePackage")
return nil
}

View File

@ -0,0 +1,27 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v3"
"go.unistack.org/pkgdash/internal/models"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) PackageList(ctx context.Context, req *pb.PackageListReq, rsp *pb.PackageListRsp) error {
h.logger.Debug(ctx, "PackagesList handler start")
packages, err := h.store.PackageList(ctx, req)
if err != nil {
h.logger.Error(ctx, "error db response: %v", err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
for _, pkg := range packages {
rsp.Packages = append(rsp.Packages, models.NewPackage(pkg))
}
h.logger.Debug(ctx, "PackagesList handler stop")
return nil
}

View File

@ -0,0 +1,32 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v3"
"go.unistack.org/pkgdash/internal/models"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) PackageLookup(ctx context.Context, req *pb.PackageLookupReq, rsp *pb.PackageLookupRsp) error {
h.logger.Debug(ctx, "Start PackagesLookup")
if err := req.Validate(); err != nil {
h.logger.Error(ctx, "validate error", err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
pkg, err := h.store.PackageLookup(ctx, req)
if err != nil {
h.logger.Error(ctx, "package lookup", err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
rsp.Package = models.NewPackage(pkg)
h.logger.Debug(ctx, "Success finish PackagesLookup")
return nil
}

View File

@ -0,0 +1,27 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v3"
"go.unistack.org/pkgdash/internal/models"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) PackageModules(ctx context.Context, req *pb.PackageModulesReq, rsp *pb.PackageModulesRsp) error {
h.logger.Debug(ctx, "PackageModules handler start")
modules, err := h.store.PackageModules(ctx, req)
if err != nil {
h.logger.Error(ctx, "error db response: %v", err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
for _, mod := range modules {
rsp.Modules = append(rsp.Modules, models.NewModule(mod))
}
h.logger.Debug(ctx, "PackagesModules handler stop")
return nil
}

View File

@ -0,0 +1,32 @@
package handler
import (
"context"
"net/http"
httpsrv "go.unistack.org/micro-server-http/v3"
"go.unistack.org/pkgdash/internal/models"
pb "go.unistack.org/pkgdash/proto"
)
func (h *Handler) PackageUpdate(ctx context.Context, req *pb.PackageUpdateReq, rsp *pb.PackageUpdateRsp) error {
h.logger.Debug(ctx, "Start UpdatePackage")
if err := req.Validate(); err != nil {
h.logger.Error(ctx, "validate error", err)
httpsrv.SetRspCode(ctx, http.StatusBadRequest)
return httpsrv.SetError(NewValidationError(err))
}
pkg, err := h.store.PackageUpdate(ctx, req)
if err != nil {
h.logger.Error(ctx, "package update error", err)
httpsrv.SetRspCode(ctx, http.StatusInternalServerError)
return httpsrv.SetError(NewInternalError(err))
}
rsp.Package = models.NewPackage(pkg)
h.logger.Debug(ctx, "Success finish UpdatePackage")
return nil
}

126
internal/models/models.go Normal file
View File

@ -0,0 +1,126 @@
package models
import (
"database/sql"
"time"
pb "go.unistack.org/pkgdash/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
type Handler struct {
ID uint64 `db:"id"`
Package uint64 `db:"package"`
Name string `db:"name"`
Coverage sql.NullFloat64 `db:"coverage"`
}
func NewHandler(hdlr *Handler) *pb.Handler {
if hdlr == nil {
return nil
}
rsp := &pb.Handler{
Id: hdlr.ID,
Package: hdlr.Package,
Name: hdlr.Name,
}
if hdlr.Coverage.Valid {
rsp.Coverage = hdlr.Coverage.Float64
}
return rsp
}
type Package struct {
Created time.Time `db:"created"`
Updated time.Time `db:"updated"`
LastCheck sql.NullTime `db:"last_check"`
Type string `db:"type"`
Name string `db:"name"`
URL string `db:"url"`
Description sql.NullString `db:"description"`
Coverage sql.NullFloat64 `db:"coverage"`
Modules uint64 `db:"modules"`
ID uint64 `db:"id"`
Status uint64 `db:"status"`
Comments uint64 `db:"comments"`
Issues uint64 `db:"issues"`
}
func NewPackage(pkg *Package) *pb.Package {
if pkg == nil {
return nil
}
rsp := &pb.Package{
Name: pkg.Name,
Url: pkg.URL,
Modules: pkg.Modules,
Issues: pkg.Issues,
Comments: pkg.Comments,
Id: pkg.ID,
Created: timestamppb.New(pkg.Created),
Updated: timestamppb.New(pkg.Updated),
Type: pkg.Type,
}
if rsp.Type == "" {
rsp.Type = "package"
}
if pkg.Description.Valid {
rsp.Description = pkg.Description.String
}
if pkg.LastCheck.Valid {
rsp.LastCheck = timestamppb.New(pkg.LastCheck.Time)
}
if pkg.Coverage.Valid {
rsp.Coverage = pkg.Coverage.Float64
}
return rsp
}
type Module struct {
LastCheck sql.NullTime `db:"last_check"`
Name string `db:"name"`
Version string `db:"version"`
ID uint64 `db:"id"`
}
func NewModule(mod *Module) *pb.Module {
if mod == nil {
return nil
}
rsp := &pb.Module{
Name: mod.Name,
Version: mod.Version,
Id: mod.ID,
}
if mod.LastCheck.Valid {
rsp.LastCheck = timestamppb.New(mod.LastCheck.Time)
}
return rsp
}
type Issue struct {
Comment string `db:"comment"`
Modules []int64 `db:"modules"`
ID uint64 `db:"id"`
Status uint64 `db:"status"`
Package uint64 `db:"package"`
}
type Comment struct {
Created time.Time `db:"created"`
Updated time.Time `db:"updated"`
Comment string `db:"comment"`
ID uint64 `db:"id"`
}
func NewComment(com *Comment) *pb.Comment {
if com == nil {
return nil
}
return &pb.Comment{
Id: com.ID,
Comment: com.Comment,
Created: timestamppb.New(com.Created),
Updated: timestamppb.New(com.Updated),
}
}

View File

@ -0,0 +1,300 @@
package modules
import (
"bufio"
"bytes"
"fmt"
"io"
"net/http"
"os"
"path"
"strconv"
"strings"
"golang.org/x/mod/module"
"golang.org/x/mod/semver"
"golang.org/x/sync/errgroup"
)
// Module contains the module path and versions
type Module struct {
Path string
Versions []string
}
// MaxVersion returns the latest version.
// If there are no versions, the empty string is returned.
// Prefix can be used to filter the versions based on a prefix.
// If pre is false, pre-release versions will are excluded.
func (m *Module) MaxVersion(prefix string, pre bool) string {
var max string
for _, v := range m.Versions {
if !semver.IsValid(v) || !strings.HasPrefix(v, prefix) {
continue
}
if !pre && semver.Prerelease(v) != "" {
continue
}
max = MaxVersion(v, max)
}
return max
}
// IsNewerVersion returns true if newversion is greater than oldversion in terms of semver.
// If major is true, then newversion must be a major version ahead of oldversion to be considered newer.
func IsNewerVersion(oldversion, newversion string, major bool) bool {
if major {
return semver.Compare(semver.Major(oldversion), semver.Major(newversion)) < 0
}
return semver.Compare(oldversion, newversion) < 0
}
// MaxVersion returns the larger of two versions according to semantic version precedence.
// Incompatible versions are considered lower than non-incompatible ones.
// Invalid versions are considered lower than valid ones.
// If both versions are invalid, the empty string is returned.
func MaxVersion(v, w string) string {
// sort by validity
vValid := semver.IsValid(v)
wValid := semver.IsValid(w)
if !vValid && !wValid {
return ""
}
if vValid != wValid {
if vValid {
return v
}
return w
}
// sort by compatibility
vIncompatible := strings.HasSuffix(semver.Build(v), "+incompatible")
wIncompatible := strings.HasSuffix(semver.Build(w), "+incompatible")
if vIncompatible != wIncompatible {
if wIncompatible {
return v
}
return w
}
// sort by semver
if semver.Compare(v, w) == 1 {
return v
}
return w
}
// NextMajor returns the next major version after the provided version
func NextMajor(version string) (string, error) {
major, err := strconv.Atoi(strings.TrimPrefix(semver.Major(version), "v"))
if err != nil {
return "", err
}
major++
return fmt.Sprintf("v%d", major), nil
}
// WithMajorPath returns the module path for the provided version
func (m *Module) WithMajorPath(version string) string {
prefix := ModPrefix(m.Path)
return JoinPath(prefix, version, "")
}
// NextMajorPath returns the module path of the next major version
func (m *Module) NextMajorPath() (string, bool) {
latest := m.MaxVersion("", true)
if latest == "" {
return "", false
}
if semver.Major(latest) == "v0" {
return "", false
}
next, err := NextMajor(latest)
if err != nil {
return "", false
}
return m.WithMajorPath(next), true
}
// Query the module proxy for all versions of a module.
// If the module does not exist, the second return parameter will be false
// cached sets the Disable-Module-Fetch: true header
func Query(modpath string, cached bool) (*Module, bool, error) {
escaped, err := module.EscapePath(modpath)
if err != nil {
return nil, false, err
}
url := fmt.Sprintf("https://proxy.golang.org/%s/@v/list", escaped)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, false, err
}
req.Header.Set("User-Agent", "GoMajor/1.0")
if cached {
req.Header.Set("Disable-Module-Fetch", "true")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, false, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
if res.StatusCode == http.StatusNotFound && bytes.HasPrefix(body, []byte("not found:")) {
return nil, false, nil
}
msg := string(body)
if msg == "" {
msg = res.Status
}
return nil, false, fmt.Errorf("proxy: %s", msg)
}
var mod Module
mod.Path = modpath
sc := bufio.NewScanner(res.Body)
for sc.Scan() {
mod.Versions = append(mod.Versions, sc.Text())
}
if err := sc.Err(); err != nil {
return nil, false, err
}
return &mod, true, nil
}
// Latest finds the latest major version of a module
// cached sets the Disable-Module-Fetch: true header
func Latest(modpath string, cached bool) (*Module, error) {
latest, ok, err := Query(modpath, cached)
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("module not found: %s", modpath)
}
for i := 0; i < 100; i++ {
nextpath, ok := latest.NextMajorPath()
if !ok {
return latest, nil
}
next, ok, err := Query(nextpath, cached)
if err != nil {
return nil, err
}
if !ok {
// handle the case where a project switched to modules
// without incrementing the major version
version := latest.MaxVersion("", true)
if semver.Build(version) == "+incompatible" {
nextpath = latest.WithMajorPath(semver.Major(version))
if nextpath != latest.Path {
next, ok, err = Query(nextpath, cached)
if err != nil {
return nil, err
}
}
}
}
if !ok {
return latest, nil
}
latest = next
}
return nil, fmt.Errorf("request limit exceeded")
}
// QueryPackage tries to find the module path for the provided package path
// it does so by repeatedly chopping off the last path element and trying to
// use it as a path.
func QueryPackage(pkgpath string, cached bool) (*Module, error) {
prefix := pkgpath
for prefix != "" {
if module.CheckPath(prefix) == nil {
mod, ok, err := Query(prefix, cached)
if err != nil {
return nil, err
}
if ok {
modprefix := ModPrefix(mod.Path)
if modpath, pkgdir, ok := SplitPath(modprefix, pkgpath); ok && modpath != mod.Path {
if major, ok := ModMajor(modpath); ok {
if v := mod.MaxVersion(major, false); v != "" {
spec := JoinPath(modprefix, "", pkgdir) + "@" + v
return nil, fmt.Errorf("%s doesn't support import versioning; use %s", major, spec)
}
return nil, fmt.Errorf("failed to find module for package: %s", pkgpath)
}
}
return mod, nil
}
}
remaining, last := path.Split(prefix)
if last == "" {
break
}
prefix = strings.TrimSuffix(remaining, "/")
}
return nil, fmt.Errorf("failed to find module for package: %s", pkgpath)
}
// Update reports a newer version of a module.
// The Err field will be set if an error occured.
type Update struct {
Err error
Module module.Version
Version string
}
// UpdateOptions specifies a set of modules to check for updates.
// The OnUpdate callback will be invoked with any updates found.
type UpdateOptions struct {
OnUpdate func(Update)
Modules []module.Version
Pre bool
Cached bool
Major bool // Major true compare only major
UpMajor bool // UpMajor module up with major
}
// Updates finds updates for a set of specified modules.
func Updates(opt UpdateOptions) {
ch := make(chan Update)
go func() {
defer close(ch)
private := os.Getenv("GOPRIVATE")
var group errgroup.Group
if opt.Cached {
group.SetLimit(3)
} else {
group.SetLimit(1)
}
for _, m := range opt.Modules {
m := m
if module.MatchPrefixPatterns(private, m.Path) {
continue
}
group.Go(func() error {
mod, err := Latest(m.Path, opt.Cached)
if err != nil {
ch <- Update{Module: m, Err: err}
return nil
}
major := semver.Major(m.Version)
var v string
switch opt.UpMajor {
case true:
v = mod.MaxVersion("", opt.Pre)
case false:
v = mod.MaxVersion(major, opt.Pre)
}
if IsNewerVersion(m.Version, v, opt.Major) {
ch <- Update{Module: m, Version: v}
}
return nil
})
}
_ = group.Wait()
}()
for u := range ch {
if opt.OnUpdate != nil {
opt.OnUpdate(u)
}
}
}

View File

@ -0,0 +1,115 @@
package modules
import (
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/mod/module"
"golang.org/x/mod/semver"
)
// ModPrefix returns the module path with no SIV
func ModPrefix(modpath string) string {
prefix, _, ok := module.SplitPathVersion(modpath)
if !ok {
prefix = modpath
}
return prefix
}
// ModMajor returns the major version in vN format
func ModMajor(modpath string) (string, bool) {
_, major, ok := module.SplitPathVersion(modpath)
if ok {
major = strings.TrimPrefix(major, "/")
major = strings.TrimPrefix(major, ".")
}
return major, ok
}
// SplitPath splits the package path into the module path and the package subdirectory.
// It requires the a module path prefix to figure this out.
func SplitPath(modprefix, pkgpath string) (modpath, pkgdir string, ok bool) {
if !strings.HasPrefix(pkgpath, modprefix) {
return "", "", false
}
modpathlen := len(modprefix)
if rest := pkgpath[modpathlen:]; len(rest) > 0 && rest[0] != '/' && rest[0] != '.' {
return "", "", false
}
if strings.HasPrefix(pkgpath[modpathlen:], "/") {
modpathlen++
}
if idx := strings.Index(pkgpath[modpathlen:], "/"); idx >= 0 {
modpathlen += idx
} else {
modpathlen = len(pkgpath)
}
modpath = modprefix
if major, ok := ModMajor(pkgpath[:modpathlen]); ok {
modpath = JoinPath(modprefix, major, "")
}
pkgdir = strings.TrimPrefix(pkgpath[len(modpath):], "/")
return modpath, pkgdir, true
}
// SplitSpec splits the path/to/package@query format strings
func SplitSpec(spec string) (path, query string) {
parts := strings.SplitN(spec, "@", 2)
if len(parts) == 2 {
path = parts[0]
query = parts[1]
} else {
path = spec
}
return
}
// JoinPath creates a full package path given a module prefix, version, and package directory.
func JoinPath(modprefix, version, pkgdir string) string {
version = strings.TrimPrefix(version, ".")
version = strings.TrimPrefix(version, "/")
major := semver.Major(version)
pkgpath := modprefix
switch {
case strings.HasPrefix(pkgpath, "gopkg.in"):
pkgpath += "." + major
case major != "" && major != "v0" && major != "v1" && !strings.Contains(version, "+incompatible"):
if !strings.HasSuffix(pkgpath, "/") {
pkgpath += "/"
}
pkgpath += major
}
if pkgdir != "" {
pkgpath += "/" + pkgdir
}
return pkgpath
}
// FindModFile recursively searches up the directory structure until it
// finds the go.mod, reaches the root of the directory tree, or encounters
// an error.
func FindModFile(dir string) (string, error) {
var err error
dir, err = filepath.Abs(dir)
if err != nil {
return "", err
}
for {
name := filepath.Join(dir, "go.mod")
_, err := os.Stat(name)
if err == nil {
return name, nil
}
if !os.IsNotExist(err) {
return "", err
}
parent := filepath.Dir(dir)
if parent == dir {
return "", fmt.Errorf("cannot find go.mod")
}
dir = parent
}
}

View File

@ -0,0 +1,55 @@
package modules
import "testing"
func TestModMajor(t *testing.T) {
type args struct {
modpath string
}
var tests = []struct {
name string
args args
want string
want1 bool
}{
{"Test #1",
args{
"github.com/jackc/chunkreader/v2",
},
"v2",
true,
},
{"Test #2",
args{
"github.com/jackc/chunkreader",
},
"",
true,
},
{"Test #3",
args{
"gopkg.in/yaml.v2",
},
"v2",
true,
},
{"Test #4",
args{
"github.com/jackc/chunkreader/v1",
},
"",
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := ModMajor(tt.args.modpath)
if got != tt.want {
t.Error("ModMajor() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Error("ModMajor() got1 = %v, want %v", got1, tt.want1)
}
})
}
}

View File

@ -0,0 +1,48 @@
//go:build gogit
package git
/*
import "context"
type Repository interface {
Checkout(ctx context.Context, hash string) error
}
type repository struct {
path string
}
func NewRepositoryFromURL(ctx context.Context, url string) (Repository, error) {
return nil, nil
}
Branches() {
refIter, err := repo.Branches() // получение веток
if err != nil {
g.logger.Error(ctx, "failed to get branches", err)
return err
}
for {
ref, err := refIter.Next()
if err != nil {
if err == io.EOF {
break
}
g.logger.Error(ctx, "ref iter error", err)
return err
}
g.logger.Info(ctx, fmt.Sprintf("check %s == %s", ref.Name().Short(), branch))
if ref.Name().Short() == branch {
headRef = plumbing.NewHashReference(ref.Name(), ref.Hash())
g.logger.Info(ctx, "headRef set to "+headRef.String())
break
}
} // перебираем получение ветки и когда находим нужную выходим из цикла записав ветку в headRef
refIter.Close()
}
*/

View File

@ -0,0 +1,262 @@
//go:build !gogit
package git
import (
"bytes"
"context"
"fmt"
"io"
"os/exec"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)
type Repository interface {
Branches() ([]*plumbing.Reference, error)
// Auth(username string, password string) error
FetchContext(ctx context.Context, opts *git.FetchOptions) error
PushContext(ctx context.Context, opts *git.PushOptions) error
Head() (*plumbing.Reference, error)
Worktree() (Worktree, error)
}
type Worktree interface {
Checkout(*git.CheckoutOptions) error
PullContext(ctx context.Context, opts *git.PullOptions) error
Status() (git.Status, error)
AddWithOptions(opts *git.AddOptions) error
Commit(msg string, opts *git.CommitOptions) (plumbing.Hash, error)
Reset(opts *git.ResetOptions) error
}
type repository struct {
gocmd string
path string
// authUsername string
// authPassword string
}
func PlainOpenWithOptions(path string, opts *git.PlainOpenOptions) (Repository, error) {
gopath, err := exec.LookPath("git")
if err != nil {
return nil, err
}
return &repository{path: path, gocmd: gopath}, nil
}
/*
func (r *repository) Auth(username string, password string) error {
r.authUsername = username
r.authPassword = password
return nil
}
*/
func (r *repository) Branches() ([]*plumbing.Reference, error) {
var branches []*plumbing.Reference
cmd := exec.Command(r.gocmd, "show-ref", "--branches")
buf, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("output %s error %w", buf, err)
}
br := bytes.NewBuffer(buf)
for {
line, err := br.ReadString('\n')
if err != nil {
if err == io.EOF && line == "" {
break
} else if err != io.EOF && line == "" {
return nil, err
}
}
fields := strings.Fields(line)
if len(fields) != 2 {
return nil, fmt.Errorf("invalid fields %s", line)
}
branches = append(branches, plumbing.NewReferenceFromStrings(fields[1], fields[0]))
}
return branches, nil
}
func (r *repository) FetchContext(ctx context.Context, opts *git.FetchOptions) error {
args := []string{"fetch"}
if opts.Force {
args = append(args, "-f")
}
cmd := exec.CommandContext(ctx, r.gocmd, args...)
buf, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("output %s error %w", buf, err)
}
return nil
}
func (r *repository) PushContext(ctx context.Context, opts *git.PushOptions) error {
args := []string{"push"}
if opts.Force {
args = append(args, "-f")
}
/* TODO
var refs []string
for _, ref := range opts.RefSpecs {
refs = append(refs, ref.String())
}
args = append(args, strings.Join(refs, " "))
*/
cmd := exec.CommandContext(ctx, r.gocmd, args...)
buf, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("output %s error %w", buf, err)
}
return nil
}
func (r *repository) Head() (*plumbing.Reference, error) {
var head *plumbing.Reference
cmd := exec.Command(r.gocmd, "symbolic-ref", "--short", "HEAD")
buf, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("output %s error %w", buf, err)
}
br := bytes.NewBuffer(buf)
for {
line, err := br.ReadString('\n')
if err != nil {
if err == io.EOF && line == "" {
break
} else if err != io.EOF && line == "" {
return nil, err
}
}
fields := strings.Fields(line)
if len(fields) != 2 {
return nil, fmt.Errorf("invalid fields %s", line)
}
head = plumbing.NewReferenceFromStrings("HEAD", fields[0])
}
return head, nil
}
type worktree struct {
gocmd string
}
func (r *repository) Worktree() (Worktree, error) {
return &worktree{gocmd: r.gocmd}, nil
}
func (w *worktree) Checkout(opts *git.CheckoutOptions) error {
args := []string{"checkout"}
if opts.Create {
args = append(args, "-b", opts.Branch.Short())
}
if opts.Force {
args = append(args, "-f")
}
if opts.Hash.IsZero() {
args = append(args, opts.Branch.Short())
} else {
args = append(args, opts.Hash.String())
}
cmd := exec.Command(w.gocmd, args...)
buf, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("output %s error %w", buf, err)
}
return nil
}
func (w *worktree) Status() (git.Status, error) {
return git.Status{}, nil
}
func (w *worktree) Reset(opts *git.ResetOptions) error {
args := []string{"reset"}
if opts.Mode == git.HardReset {
args = append(args, "--hard")
}
args = append(args, opts.Commit.String())
cmd := exec.Command(w.gocmd, args...)
buf, err := cmd.CombinedOutput()
if err != nil {
return err
}
_ = buf
return nil
}
func (w *worktree) Commit(msg string, opts *git.CommitOptions) (plumbing.Hash, error) {
cmd := exec.Command(w.gocmd, `commit`,
fmt.Sprintf(`--author="%s <%s>"`, opts.Author.Name, opts.Author.Email),
"-m", msg,
fmt.Sprintf(`--date="%s"`, opts.Author.When.Format(`Mon Jan _2 15:04:05 2006 -0700`)),
)
buf, err := cmd.CombinedOutput()
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("output %s error %w", buf, err)
}
var head *plumbing.Reference
cmd = exec.Command(w.gocmd, "show-ref", "HEAD")
buf, err = cmd.CombinedOutput()
if err != nil {
return plumbing.ZeroHash, err
}
br := bytes.NewBuffer(buf)
for {
line, err := br.ReadString('\n')
if err != nil {
if err == io.EOF && line == "" {
break
} else if err != io.EOF && line == "" {
return plumbing.ZeroHash, err
}
}
fields := strings.Fields(line)
if len(fields) != 2 {
return plumbing.ZeroHash, fmt.Errorf("invalid fields %s", line)
}
head = plumbing.NewReferenceFromStrings("HEAD", fields[0])
}
return head.Hash(), nil
}
func (w *worktree) PullContext(ctx context.Context, opts *git.PullOptions) error {
args := []string{"pull"}
if opts.Force {
args = append(args, "-f")
}
if opts.Depth != 0 {
args = append(args, fmt.Sprintf("--depth=%d", opts.Depth))
}
cmd := exec.CommandContext(ctx, w.gocmd, args...)
buf, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("output %s error %w", buf, err)
}
return nil
}
func (w *worktree) AddWithOptions(opts *git.AddOptions) error {
cmd := exec.Command(w.gocmd, "add", opts.Path)
buf, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("output %s error %w", buf, err)
}
return nil
}

View File

@ -0,0 +1,676 @@
package gitea
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os/exec"
"regexp"
"strings"
"text/template"
"time"
"github.com/go-git/go-git/v5"
gitconfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
httpauth "github.com/go-git/go-git/v5/plumbing/transport/http"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/pkgdash/internal/configcli"
"go.unistack.org/pkgdash/internal/modules"
//gogit "go.unistack.org/pkgdash/internal/source/git"
)
var ErrPRNotExist = errors.New("pull request does not exist")
type Gitea struct {
logger logger.Logger
URL string
Username string
Password string
PRTitle string
PRBody string
Repository string
Owner string
pulls []*giteaPull
}
func NewGitea(cfg configcli.Config, log logger.Logger) *Gitea {
return &Gitea{
logger: log,
URL: cfg.Source.APIURL,
Username: cfg.Source.Username,
Password: cfg.Source.Password,
PRTitle: cfg.PullRequestTitle,
PRBody: cfg.PullRequestBody,
Repository: cfg.Source.Repository,
Owner: cfg.Source.Owner,
}
}
type giteaPull struct {
URL string `json:"url"`
Title string `json:"title"`
Base struct {
Ref string `json:"ref"`
} `json:"base"`
Head struct {
Ref string `json:"ref"`
} `json:"head"`
ID int64 `json:"id"`
}
func (g *Gitea) Name() string {
return "gitea"
}
func (g *Gitea) RequestOpen(ctx context.Context, branch string, path string, mod modules.Update) error {
g.logger.Debug(ctx, fmt.Sprintf("RequestOpen start, mod title: %s", path))
var buf []byte
var err error
// создания шаблона названия для пулл реквеста
tplTitle, err := template.New("pull_request_title").Parse(g.PRTitle)
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to parse template: %v", err))
}
wTitle := bytes.NewBuffer(nil)
// создания шаблона тела для пулл реквеста
tplBody, err := template.New("pull_request_body").Parse(g.PRTitle)
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to parse template: %v", err))
}
wBody := bytes.NewBuffer(nil)
data := map[string]string{
"Name": path,
"VersionOld": mod.Module.Version,
"VersionNew": mod.Version,
}
if err = tplTitle.Execute(wTitle, data); err != nil {
g.logger.Error(ctx, "failed to execute template", err)
return err
}
if err = tplBody.Execute(wBody, data); err != nil {
g.logger.Error(ctx, "failed to execute template", err)
return err
}
// открытие гит репозитория с опцией обхода репозитория для нахождения .git
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to open repo: %v", err))
}
wtree, headRef, err := g.fetchCheckout(ctx, repo, branch, path, mod)
if err != nil {
g.logger.Error(ctx, "failed to checkout", err)
}
defer func() {
_ = g.checkout(wtree, headRef)
}()
if err = g.scopeUpdateDep(ctx, path, mod); err != nil {
return err
}
g.logger.Info(ctx, "worktree add go.mod")
if err = wtree.AddWithOptions(&git.AddOptions{Path: "go.mod"}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to add file: %v", err))
return err
}
g.logger.Info(ctx, "worktree add go.sum")
if err = wtree.AddWithOptions(&git.AddOptions{Path: "go.sum"}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to add file: %v", err))
return err
}
g.logger.Info(ctx, "worktree commit")
_, err = wtree.Commit(wTitle.String(), &git.CommitOptions{
Parents: []plumbing.Hash{headRef.Hash()},
Author: &object.Signature{
Name: "gitea-actions",
Email: "info@unistack.org",
When: time.Now(),
},
}) // хотим за коммитить изменения
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to commit: %v", err))
return err
}
refspec := gitconfig.RefSpec(fmt.Sprintf("+refs/heads/pkgdash/go_modules/%s-%s:refs/heads/pkgdash/go_modules/%s-%s", path, mod.Version, path, mod.Version)) // todo как будто нужно переделать
g.logger.Info(ctx, fmt.Sprintf("try to push refspec %s", refspec))
if err = repo.PushContext(ctx, &git.PushOptions{
RefSpecs: []gitconfig.RefSpec{refspec},
Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Force: true,
}); err != nil {
g.logger.Error(ctx, "failed to push repo branch", err)
return err
} // пытаемся за пушить изменения
rsp, err := g.postPullRequest(ctx, wBody, wTitle, branch, path, mod)
if err != nil {
return err
}
// Вроде создаем новый реквест на создание пулл реквеста
if rsp.StatusCode != http.StatusCreated {
buf, _ = io.ReadAll(rsp.Body)
return fmt.Errorf("unknown error: %s", buf)
}
g.logger.Info(ctx, fmt.Sprintf("PR create for %s-%s", path, mod.Version))
return nil
}
func (g *Gitea) RequestClose(ctx context.Context, branch string, path string) error {
g.logger.Debug(ctx, fmt.Sprintf("RequestClose start, mod title: %s", path))
var err error
g.pulls, err = g.GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return err
}
prExist := false
var b string // Name of the branch to be deleted
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) && pull.Base.Ref == branch {
g.logger.Info(ctx, fmt.Sprintf("PR for %s exists: %s", path, pull.URL))
prExist = true
b = pull.Head.Ref
}
}
if !prExist {
g.logger.Error(ctx, fmt.Sprintf("skip %s since pr does not exist", path))
return ErrPRNotExist
}
req, err := g.DeleteBranch(ctx, g.URL, g.Owner, g.Repository, b, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to create request for delete the branch: %s, err: %s", branch, err))
return err
}
rsp, err := http.DefaultClient.Do(req)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to do request for delete the branch: %s, err: %s, code: %v", branch, err, rsp.StatusCode))
return err
}
g.logger.Info(ctx, fmt.Sprintf("Delete branch for %s successful", path))
return nil
}
func (g *Gitea) RequestUpdate(ctx context.Context, branch string, path string, mod modules.Update) error {
g.logger.Debug(ctx, fmt.Sprintf("RequestUpdate start, mod title: %s", path))
var err error
g.pulls, err = g.GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return err
}
prExist := false
var pullId int64
var targetBranch plumbing.ReferenceName
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) && pull.Base.Ref == branch {
g.logger.Info(ctx, fmt.Sprintf("don't skip %s since pr exist %s", path, pull.URL)) // todo
tVersion := getVersions(pull.Head.Ref) // Надо взять просто из названия ветки последнюю версию
if !modules.IsNewerVersion(tVersion, mod.Version, false) {
g.logger.Debug(ctx, "The existing PR is relevant")
return nil
}
prExist = true
pullId = pull.ID
targetBranch = plumbing.ReferenceName(pull.Head.Ref)
}
}
if !prExist {
g.logger.Error(ctx, fmt.Sprintf("skip %s since pr does not exist", path))
return ErrPRNotExist
}
// создания шаблона названия для пулл реквеста
tplTitle, err := template.New("pull_request_title").Parse(g.PRTitle)
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to parse template: %v", err))
}
wTitle := bytes.NewBuffer(nil)
// создания шаблона тела для пулл реквеста
tplBody, err := template.New("pull_request_body").Parse(g.PRTitle)
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to parse template: %v", err))
}
wBody := bytes.NewBuffer(nil)
data := map[string]string{
"Name": path,
"VersionOld": mod.Module.Version,
"VersionNew": mod.Version,
}
if err = tplTitle.Execute(wTitle, data); err != nil {
g.logger.Error(ctx, "failed to execute template", err)
return err
}
if err = tplBody.Execute(wBody, data); err != nil {
g.logger.Error(ctx, "failed to execute template", err)
return err
}
// открытие гит репозитория с опцией обхода репозитория для нахождения .git
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to open repo: %v", err))
}
wtree, headRef, err := g.fetchCheckout(ctx, repo, targetBranch.Short(), path, mod)
if err != nil {
g.logger.Error(ctx, "failed to checkout", err)
return err
}
defer func() {
_ = g.checkout(wtree, headRef)
}()
if err = g.scopeUpdateDep(ctx, path, mod); err != nil {
return err
}
g.logger.Info(ctx, "worktree add go.mod")
if err = wtree.AddWithOptions(&git.AddOptions{Path: "go.mod"}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to add file: %v", err))
return err
}
g.logger.Info(ctx, "worktree add go.sum")
if err = wtree.AddWithOptions(&git.AddOptions{Path: "go.sum"}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to add file: %v", err))
return err
}
g.logger.Info(ctx, "worktree commit")
_, err = wtree.Commit(wTitle.String(), &git.CommitOptions{
Parents: []plumbing.Hash{headRef.Hash()},
Author: &object.Signature{
Name: "gitea-actions",
Email: "info@unistack.org",
When: time.Now(),
},
}) // хотим за коммитить изменения
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to commit: %v", err))
return err
}
refspec := gitconfig.RefSpec(fmt.Sprintf("+refs/heads/pkgdash/go_modules/%s-%s:refs/heads/pkgdash/go_modules/%s-%s", path, mod.Version, path, mod.Version)) // todo как будто нужно переделать
g.logger.Info(ctx, fmt.Sprintf("try to push refspec %s", refspec))
if err = repo.PushContext(ctx, &git.PushOptions{
//RefSpecs: []gitconfig.RefSpec{refspec},
Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Force: true,
}); err != nil {
g.logger.Error(ctx, "failed to push repo branch", err)
return err
} // пытаемся за пушить изменения
err = g.patchPullRequest(ctx, wBody, wTitle, pullId)
if err != nil {
return err
}
g.logger.Info(ctx, fmt.Sprintf("PR update for %s-%s", path, mod.Version))
return nil
}
func (g *Gitea) RequestList(ctx context.Context, branch string) (map[string]string, error) {
g.logger.Debug(ctx, fmt.Sprintf("RequestList for %s", branch))
var err error
g.pulls, err = g.GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return nil, err
}
var path string
rMap := make(map[string]string)
for _, pull := range g.pulls {
if !strings.HasPrefix(pull.Title, "Bump ") || pull.Base.Ref != branch { // добавляем только реквесты бота по обновлению модулей
continue
}
path = strings.Split(pull.Title, " ")[1] // todo Работет только для дефолтного шаблона
rMap[path] = pull.Title
}
return rMap, nil
}
func getVersions(s string) string {
re := regexp.MustCompile("[vV][0-9]+\\.[0-9]+\\.[0-9]+")
version := re.FindString(s)
return version
}
func (g *Gitea) DeleteBranch(ctx context.Context, url, owner, repo, branch, password string) (*http.Request, error) {
var buf []byte
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, fmt.Sprintf("https://%s/api/v1/repos/%s/%s/branches/%s", url, owner, repo, branch), bytes.NewReader(buf))
if err != nil {
return nil, err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+password)
return req, err
}
func (g *Gitea) GetPulls(ctx context.Context, url, owner, repo, password string) ([]*giteaPull, error) {
var pullsAll []*giteaPull
page := 1
for {
pulls := make([]*giteaPull, 0, 10)
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls?state=open&page=%v", url, owner, repo, page),
nil)
if err != nil {
return nil, err
} // вроде запроса к репозиторию
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+password)
rsp, err := http.DefaultClient.Do(req) // выполнение запроса
if err != nil {
return nil, err
}
buf, _ := io.ReadAll(rsp.Body)
switch rsp.StatusCode {
case http.StatusOK:
if err = json.Unmarshal(buf, &pulls); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to decode response %s err: %v", buf, err))
return nil, err
}
pullsAll = append(pullsAll, pulls...)
page++
case http.StatusNotFound:
g.logger.Info(ctx, fmt.Sprintf("pull-request is not exist for %s", repo))
return nil, ErrPRNotExist
default:
return nil, fmt.Errorf("unknown error: %s", buf)
}
if len(pulls) == 0 {
break
}
}
return pullsAll, nil
}
func (g *Gitea) checkout(w *git.Worktree, ref *plumbing.Reference) error {
ctx := context.Background()
g.logger.Debug(ctx, "checkout: "+ref.String())
if err := w.Checkout(&git.CheckoutOptions{
Branch: ref.Name(),
Create: false,
Force: true,
Keep: false,
}); err != nil {
g.logger.Error(ctx, "failed to reset", err)
return err
}
return nil
}
func (g Gitea) fetchCheckout(ctx context.Context, repo *git.Repository, branch, path string, mod modules.Update) (*git.Worktree, *plumbing.Reference, error) {
// обновляем ветки
if err := repo.FetchContext(ctx, &git.FetchOptions{
Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Force: true,
}); err != nil && err != git.NoErrAlreadyUpToDate {
g.logger.Error(ctx, "failed to fetch repo", err)
return nil, nil, err
}
var headRef *plumbing.Reference
branches, err := repo.Branches()
if err != nil {
g.logger.Error(ctx, "cant get repo branch", err)
return nil, nil, err
}
for {
ref, err := branches.Next()
if err != nil {
return nil, nil, err
}
if ref.Name().Short() == branch {
//Получаем ссылку на нужную ветку
headRef = ref
g.logger.Info(ctx, "headRef set to "+headRef.String())
break
}
}
if headRef == nil {
g.logger.Error(ctx, "failed to get repo branch head")
return nil, nil, err
} // Не получили нужную ветку
g.logger.Info(ctx, "repo head "+headRef.String())
wtree, err := repo.Worktree()
if err != nil {
g.logger.Error(ctx, "failed to get worktree", err)
return nil, nil, err
}
if err = wtree.Reset(&git.ResetOptions{
Mode: git.HardReset,
}); err != nil {
g.logger.Error(ctx, "reset work_tree error: ", err)
return nil, nil, err
}
g.pulls, err = g.GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password)
if err != nil && err != ErrPRNotExist {
g.logger.Error(ctx, "GetPulls error", err)
return nil, nil, err
}
var pullExist bool
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) && (strings.Contains(pull.Base.Ref, branch) || strings.Contains(pull.Head.Ref, branch)) {
pullExist = true
} // хотим проверить есть ли пулл реквест для этой ветки, если есть то выходим
}
g.logger.Info(ctx, fmt.Sprintf("update %s from %s to %s", path, mod.Module.Version, mod.Version))
wstatus, err := wtree.Status()
if err != nil {
g.logger.Error(ctx, "failed to get worktree status", err)
return nil, nil, err
}
g.logger.Info(ctx, "worktree status "+wstatus.String())
if err = wtree.PullContext(ctx, &git.PullOptions{
Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
// Depth: 1,
// RemoteURL :
ReferenceName: headRef.Name(),
Force: true,
RemoteName: "origin",
}); err != nil && err != git.NoErrAlreadyUpToDate {
g.logger.Error(ctx, fmt.Sprintf("failed to pull repo: %v", err)) // подтягиваем изменения с удаленого репозитория
return nil, nil, err
}
g.logger.Info(ctx, fmt.Sprintf("checkout ref %s", headRef))
if pullExist {
if err = wtree.Checkout(&git.CheckoutOptions{
Branch: headRef.Name(),
Create: false,
Force: true,
}); err != nil && err != git.ErrBranchExists {
g.logger.Error(ctx, fmt.Sprintf("failed to checkout tree: %v", err))
return nil, nil, err
} //переходим на существующею
} else {
if err = wtree.Checkout(&git.CheckoutOptions{
Hash: headRef.Hash(),
Branch: plumbing.NewBranchReferenceName(fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version)),
Create: true,
Force: true,
}); err != nil && err != git.ErrBranchExists {
g.logger.Error(ctx, fmt.Sprintf("failed to checkout tree: %v", err))
return nil, nil, err
} // создаем новую ветку
}
return wtree, headRef, nil
}
func (g *Gitea) postPullRequest(ctx context.Context, wBody, wTitle *bytes.Buffer, branch, path string, mod modules.Update) (*http.Response, error) {
body := map[string]string{
"base": branch,
"body": wBody.String(),
"head": fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version),
"title": wTitle.String(),
}
g.logger.Info(ctx, fmt.Sprintf("raw body: %#+v", body))
buf, err := json.Marshal(body)
if err != nil {
g.logger.Error(ctx, "failed to marshal", err)
return nil, err
}
g.logger.Info(ctx, fmt.Sprintf("marshal body: %s", buf))
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls", g.URL, g.Owner, g.Repository),
bytes.NewReader(buf),
)
if err != nil {
g.logger.Error(ctx, "http request error", err)
return nil, err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+g.Password)
rsp, err := http.DefaultClient.Do(req)
if err != nil {
g.logger.Error(ctx, "failed to call http request", err)
return rsp, err
}
return rsp, nil
}
func (g *Gitea) patchPullRequest(ctx context.Context, wBody, wTitle *bytes.Buffer, indexPR int64) error {
body := map[string]string{
"body": wBody.String(),
"title": wTitle.String(),
}
g.logger.Info(ctx, fmt.Sprintf("raw body: %#+v", body))
buf, err := json.Marshal(body)
if err != nil {
g.logger.Error(ctx, "failed to marshal", err)
return err
}
g.logger.Info(ctx, fmt.Sprintf("marshal body: %s", buf))
req, err := http.NewRequestWithContext(
ctx,
http.MethodPatch,
fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls/%d", g.URL, g.Owner, g.Repository, indexPR),
bytes.NewReader(buf),
)
if err != nil {
g.logger.Error(ctx, "http request error", err)
return err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+g.Password)
_, err = http.DefaultClient.Do(req)
if err != nil {
g.logger.Error(ctx, "failed to call http request", err)
return err
}
return nil
}
func (g *Gitea) scopeUpdateDep(ctx context.Context, path string, mod modules.Update) error {
epath, err := exec.LookPath("go")
if errors.Is(err, exec.ErrDot) {
err = nil
}
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to find go command: %v", err))
} // ищем go файл
var cmd *exec.Cmd
var out []byte
cmd = exec.CommandContext(ctx, epath, "mod", "edit", fmt.Sprintf("-droprequire=%s", mod.Module.Path))
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err))
return err
}
cmd = exec.CommandContext(ctx, epath, "mod", "edit", fmt.Sprintf("-require=%s@%s", path, mod.Version))
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err))
return err
} // пытаемся выполнить команду go mod edit с новой версией модуля
cmd = exec.CommandContext(ctx, epath, "mod", "tidy")
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to run go mod tidy: %s err: %v", out, err))
return err
} // пытаемся выполнить команду go mod tidy пытаемся подтянуть новую версию модуля
return nil
}

View File

@ -0,0 +1,396 @@
package github
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os/exec"
"strings"
"text/template"
"time"
"github.com/go-git/go-git/v5"
gitconfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
httpauth "github.com/go-git/go-git/v5/plumbing/transport/http"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/pkgdash/internal/configcli"
"go.unistack.org/pkgdash/internal/modules"
)
var ErrPRNotExist = errors.New("pull request does not exist")
type Github struct {
logger logger.Logger
URL string
Username string
Password string
PRTitle string
PRBody string
Repository string
Owner string
pulls []*githubPull
baseRef *plumbing.Reference
}
func NewGithub(cfg configcli.Config, log logger.Logger) *Github {
return &Github{
logger: log,
URL: cfg.Source.APIURL,
Username: cfg.Source.Username,
Password: cfg.Source.Password,
PRTitle: cfg.PullRequestTitle,
PRBody: cfg.PullRequestBody,
Repository: cfg.Source.Repository,
Owner: cfg.Source.Owner,
}
}
type githubPull struct {
URL string `json:"url"`
Title string `json:"title"`
Base struct {
Ref string `json:"ref"`
} `json:"base"`
Head struct {
Ref string `json:"ref"`
} `json:"head"`
ID int64 `json:"id"`
}
func (g *Github) Name() string {
return "github"
}
func (g *Github) RequestOpen(ctx context.Context, branch string, path string, mod modules.Update) error {
g.logger.Debug(ctx, fmt.Sprintf("RequestOpen start, mod title: %s", path))
var buf []byte
var err error
// создания шаблона названия для пулл реквеста
tplTitle, err := template.New("pull_request_title").Parse(g.PRTitle)
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to parse template: %v", err))
}
wTitle := bytes.NewBuffer(nil)
// создания шаблона тела для пулл реквеста
tplBody, err := template.New("pull_request_body").Parse(g.PRTitle)
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to parse template: %v", err))
}
wBody := bytes.NewBuffer(nil)
data := map[string]string{
"Name": path,
"VersionOld": mod.Module.Version,
"VersionNew": mod.Version,
}
if err = tplTitle.Execute(wTitle, data); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to execute template: %v", err))
}
if err = tplBody.Execute(wBody, data); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to execute template: %v", err))
}
// открытие гит репозитория с опцией обхода репозитория для нахождения .git
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to open repo: %v", err))
}
// извлекаем ссылки с объектами из удаленного объекта??
if err = repo.FetchContext(ctx, &git.FetchOptions{
// Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Force: true,
}); err != nil && err != git.NoErrAlreadyUpToDate {
g.logger.Fatal(ctx, fmt.Sprintf("failed to fetch repo : %v", err))
} // обновляем репозиторий
var headRef *plumbing.Reference // вроде ссылка на гит
if g.baseRef == nil {
g.baseRef, err = repo.Head()
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("Error head: %s", err))
}
}
refIter, err := repo.Branches() // получение веток
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to get branches: %v", err))
return err
}
for {
ref, err := refIter.Next()
if err != nil {
break
}
if ref.Name().Short() == branch { // todo вот тут возможно нужно переделать
headRef = ref
break
}
} // перебираем получение ветки и когда находим нужную выходим из цикла записав ветку в headRef
refIter.Close()
if headRef == nil {
g.logger.Fatal(ctx, "failed to get repo branch head")
return err
} // Не получили нужную ветку
g.logger.Info(ctx, fmt.Sprintf("repo head %s", headRef))
wtree, err := repo.Worktree() // todo вроде рабочее дерево не нужно
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to get worktree: %v", err))
}
defer g.checkout(*wtree, *g.baseRef)
g.pulls, err = g.GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password)
if err != nil && err != ErrPRNotExist {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return err
}
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) && strings.Contains(pull.Base.Ref, branch) {
g.logger.Info(ctx, fmt.Sprintf("PR for %s exists %s, call RequestUpdate", path, pull.URL))
return g.RequestUpdate(ctx, branch, path, mod)
} // хотим проверить есть ли пулл реквест для этой ветки, если есть то выходим
}
g.logger.Info(ctx, fmt.Sprintf("update %s from %s to %s", path, mod.Module.Version, mod.Version))
g.logger.Info(ctx, "reset worktree")
if err = wtree.Reset(&git.ResetOptions{Commit: headRef.Hash(), Mode: git.HardReset}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to reset repo branch: %v", err))
} // вроде меняем ветку todo вроде можно удалить
if err = wtree.PullContext(ctx, &git.PullOptions{
Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Depth: 1,
// RemoteURL :
Force: true,
RemoteName: "origin",
}); err != nil && err != git.NoErrAlreadyUpToDate {
g.logger.Error(ctx, fmt.Sprintf("failed to pull repo: %v", err)) // подтягиваем изменения с удаленого репозитория
}
g.logger.Info(ctx, fmt.Sprintf("checkout ref %s", headRef))
if err = wtree.Checkout(&git.CheckoutOptions{
Hash: headRef.Hash(),
Branch: plumbing.NewBranchReferenceName(fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version)),
Create: true,
Force: true,
}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to checkout tree: %v", err))
return err
} // создаем новую ветку
epath, err := exec.LookPath("go")
if errors.Is(err, exec.ErrDot) {
err = nil
}
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to find go command: %v", err))
} // ищем go файл
var cmd *exec.Cmd
var out []byte
cmd = exec.CommandContext(ctx, epath, "mod", "edit", fmt.Sprintf("-droprequire=%s", mod.Module.Path))
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err))
}
cmd = exec.CommandContext(ctx, epath, "mod", "edit", fmt.Sprintf("-require=%s@%s", path, mod.Version))
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err))
} // пытаемся выполнить команду go mod edit с новой версией модуля
cmd = exec.CommandContext(ctx, epath, "mod", "tidy")
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to run go mod tidy: %s err: %v", out, err))
} // пытаемся выполнить команду go mod tidy пытаемся подтянуть новую версию модуля
g.logger.Info(ctx, "worktree add go.mod")
if _, err = wtree.Add("go.mod"); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to add file: %v", err))
}
g.logger.Info(ctx, "worktree add go.sum")
if _, err = wtree.Add("go.sum"); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to add file: %v", err))
}
g.logger.Info(ctx, "worktree commit")
_, err = wtree.Commit(wTitle.String(), &git.CommitOptions{
Parents: []plumbing.Hash{headRef.Hash()},
Author: &object.Signature{
Name: "gitea-actions",
Email: "info@unistack.org",
When: time.Now(),
},
}) // хотим за коммитить изменения
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to commit: %v", err))
}
refspec := gitconfig.RefSpec(fmt.Sprintf("+refs/heads/pkgdash/go_modules/%s-%s:refs/heads/pkgdash/go_modules/%s-%s", path, mod.Version, path, mod.Version)) // todo как будто нужно переделать
g.logger.Info(ctx, fmt.Sprintf("try to push refspec %s", refspec))
if err = repo.PushContext(ctx, &git.PushOptions{
RefSpecs: []gitconfig.RefSpec{refspec},
Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Force: true,
}); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to push repo branch: %v", err))
} // пытаемся за пушить изменения
body := map[string]string{
"base": branch,
"body": wBody.String(),
"head": fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version),
"title": wTitle.String(),
}
g.logger.Info(ctx, fmt.Sprintf("raw body: %#+v", body))
buf, err = json.Marshal(body)
if err != nil {
return err
}
g.logger.Info(ctx, fmt.Sprintf("marshal body: %s", buf))
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls", g.URL, g.Owner, g.Repository),
bytes.NewReader(buf),
)
if err != nil {
return err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+g.Password)
rsp, err := http.DefaultClient.Do(req)
if err != nil {
return err
} // Вроде создаем новый реквест на создание пулл реквеста
if rsp.StatusCode != http.StatusCreated {
buf, _ = io.ReadAll(rsp.Body)
return fmt.Errorf("unknown error: %s", buf)
}
g.logger.Info(ctx, fmt.Sprintf("PR create for %s-%s", path, mod.Version))
repo, err = git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to open repo: %v", err))
}
return nil
}
func (g *Github) RequestClose(ctx context.Context, branch string, path string) error {
return fmt.Errorf("implement me")
}
func (g *Github) RequestUpdate(ctx context.Context, branch string, path string, mod modules.Update) error {
return fmt.Errorf("implement me")
}
func (g *Github) RequestList(ctx context.Context, branch string) (map[string]string, error) {
g.logger.Debug(ctx, fmt.Sprintf("RequestList for %s", branch))
var err error
g.pulls, err = g.GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return nil, err
}
var path string
rMap := make(map[string]string)
for _, pull := range g.pulls {
if !strings.HasPrefix(pull.Title, "Bump ") || pull.Base.Ref != branch { // добавляем только реквесты бота по обновлению модулей
continue
}
path = strings.Split(pull.Title, " ")[1] // todo Работет только для дефолтного шаблона
rMap[path] = pull.Title
}
return rMap, nil
}
func (g *Github) GetPulls(ctx context.Context, url, owner, repo, password string) ([]*githubPull, error) {
var pullsAll []*githubPull
page := 1
for {
pulls := make([]*githubPull, 0, 10)
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls?state=open&page=%v", url, owner, repo, page),
nil)
if err != nil {
return nil, err
} // вроде запроса к репозиторию
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+password)
rsp, err := http.DefaultClient.Do(req) // выполнение запроса
if err != nil {
return nil, err
}
buf, _ := io.ReadAll(rsp.Body)
switch rsp.StatusCode {
case http.StatusOK:
if err = json.Unmarshal(buf, &pulls); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to decode response %s err: %v", buf, err))
return nil, err
}
pullsAll = append(pullsAll, pulls...)
page++
case http.StatusNotFound:
g.logger.Info(ctx, fmt.Sprintf("pull-request is not exist for %s", repo))
return nil, ErrPRNotExist
default:
return nil, fmt.Errorf("unknown error: %s", buf)
}
if len(pulls) == 0 {
break
}
}
return pullsAll, nil
}
func (g *Github) checkout(w git.Worktree, ref plumbing.Reference) {
ctx := context.Background()
g.logger.Debug(ctx, fmt.Sprintf("Checkout: %s", ref.Name().Short()))
if err := w.Checkout(&git.CheckoutOptions{
Branch: ref.Name(),
Create: false,
Force: true,
Keep: false,
}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to reset: %v", err))
}
}

View File

@ -0,0 +1,522 @@
package gitlab
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os/exec"
"regexp"
"strconv"
"strings"
"text/template"
"time"
"github.com/go-git/go-git/v5"
gitconfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
httpauth "github.com/go-git/go-git/v5/plumbing/transport/http"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/pkgdash/internal/configcli"
"go.unistack.org/pkgdash/internal/modules"
)
var ErrPRNotExist = errors.New("pull request does not exist")
type Gitlab struct {
logger logger.Logger
URL string
Username string
Password string
PRTitle string
PRBody string
Repository string
RepositoryId string
Owner string
pulls []*gitlabPull
baseRef *plumbing.Reference
}
func NewGitlab(cfg configcli.Config, log logger.Logger) *Gitlab {
return &Gitlab{
logger: log,
URL: cfg.Source.APIURL,
Username: cfg.Source.Username,
Password: cfg.Source.Password,
PRTitle: cfg.PullRequestTitle,
PRBody: cfg.PullRequestBody,
Repository: cfg.Source.Repository,
Owner: cfg.Source.Owner,
}
}
type gitlabPull struct {
URL string `json:"web_url"`
Title string `json:"title"`
Target string `json:"target_branch"`
Source string `json:"source_branch"`
ID int64 `json:"id"`
}
type gitlabProject struct {
Id int64 `json:"id"`
Name string `json:"name"`
}
func (g *Gitlab) Name() string {
return "gitlab"
}
func (g *Gitlab) RequestOpen(ctx context.Context, branch string, path string, mod modules.Update) error {
g.logger.Debug(ctx, fmt.Sprintf("RequestOpen start, mod title: %s", path))
var buf []byte
var err error
// создания шаблона названия для пулл реквеста
tplTitle, err := template.New("pull_request_title").Parse(g.PRTitle)
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to parse template: %v", err))
}
wTitle := bytes.NewBuffer(nil)
// создания шаблона тела для пулл реквеста
tplBody, err := template.New("pull_request_body").Parse(g.PRTitle)
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to parse template: %v", err))
}
wBody := bytes.NewBuffer(nil)
data := map[string]string{
"Name": path,
"VersionOld": mod.Module.Version,
"VersionNew": mod.Version,
}
if err = tplTitle.Execute(wTitle, data); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to execute template: %v", err))
}
if err = tplBody.Execute(wBody, data); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to execute template: %v", err))
}
// открытие гит репозитория с опцией обхода репозитория для нахождения .git
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to open repo: %v", err))
}
// извлекаем ссылки с объектами из удаленного объекта??
if err = repo.FetchContext(ctx, &git.FetchOptions{
// Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Force: true,
}); err != nil && err != git.NoErrAlreadyUpToDate {
g.logger.Fatal(ctx, fmt.Sprintf("failed to fetch repo : %v", err))
} // обновляем репозиторий
var headRef *plumbing.Reference // вроде ссылка на гит
if g.baseRef == nil {
g.baseRef, err = repo.Head()
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("Error head: %s", err))
}
}
refIter, err := repo.Branches() // получение веток
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to get branches: %v", err))
}
for {
ref, err := refIter.Next()
if err != nil {
break
}
if ref.Name().Short() == branch { // todo вот тут возможно нужно переделать
headRef = ref
break
}
} // перебираем получение ветки и когда находим нужную выходим из цикла записав ветку в headRef
refIter.Close()
if headRef == nil {
g.logger.Fatal(ctx, "failed to get repo branch head")
return err
} // Не получили нужную ветку
g.logger.Info(ctx, fmt.Sprintf("repo head %s", headRef))
wtree, err := repo.Worktree() // todo вроде рабочее дерево не нужно
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to get worktree: %v", err))
}
defer g.checkout(*wtree, *g.baseRef)
g.pulls, err = g.GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password)
if err != nil && err != ErrPRNotExist {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return err
}
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) {
g.logger.Info(ctx, fmt.Sprintf("PR for %s exists %s, call RequestUpdate", path, pull.URL))
return g.RequestUpdate(ctx, branch, path, mod)
} // хотим проверить есть ли пулл реквест для этой ветки, если есть то выходим
}
g.logger.Info(ctx, fmt.Sprintf("update %s from %s to %s", path, mod.Module.Version, mod.Version))
sourceBranch := fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version)
g.logger.Info(ctx, "reset worktree")
if err = wtree.Reset(&git.ResetOptions{Commit: headRef.Hash(), Mode: git.HardReset}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to reset repo branch: %v", err))
}
if err = wtree.PullContext(ctx, &git.PullOptions{
Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Depth: 1,
// RemoteURL :
Force: true,
RemoteName: "origin",
}); err != nil && err != git.NoErrAlreadyUpToDate {
g.logger.Error(ctx, fmt.Sprintf("failed to pull repo: %v", err)) // подтягиваем изменения с удаленого репозитория
}
g.logger.Info(ctx, fmt.Sprintf("checkout ref %s", headRef))
if err = wtree.Checkout(&git.CheckoutOptions{
Hash: headRef.Hash(),
Branch: plumbing.NewBranchReferenceName(sourceBranch),
Create: true,
Force: true,
}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to checkout tree: %v", err))
return err
} // создаем новую ветку
epath, err := exec.LookPath("go")
if errors.Is(err, exec.ErrDot) {
err = nil
}
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to find go command: %v", err))
} // ищем go файл
var cmd *exec.Cmd
var out []byte
cmd = exec.CommandContext(ctx, epath, "mod", "edit", fmt.Sprintf("-droprequire=%s", mod.Module.Path))
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err))
}
cmd = exec.CommandContext(ctx, epath, "mod", "edit", fmt.Sprintf("-require=%s@%s", path, mod.Version))
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err))
} // пытаемся выполнить команду go mod edit с новой версией модуля
cmd = exec.CommandContext(ctx, epath, "mod", "tidy")
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to run go mod tidy: %s err: %v", out, err))
} // пытаемся выполнить команду go mod tidy пытаемся подтянуть новую версию модуля
g.logger.Info(ctx, "worktree add go.mod")
if _, err = wtree.Add("go.mod"); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to add file: %v", err))
}
g.logger.Info(ctx, "worktree add go.sum")
if _, err = wtree.Add("go.sum"); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to add file: %v", err))
}
g.logger.Info(ctx, "worktree commit")
_, err = wtree.Commit(wTitle.String(), &git.CommitOptions{
Parents: []plumbing.Hash{headRef.Hash()},
Author: &object.Signature{
Name: "gitea-actions",
Email: "info@unistack.org",
When: time.Now(),
},
}) // хотим за коммитить изменения
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to commit: %v", err))
}
refspec := gitconfig.RefSpec(fmt.Sprintf("+refs/heads/pkgdash/go_modules/%s-%s:refs/heads/pkgdash/go_modules/%s-%s", path, mod.Version, path, mod.Version)) // todo как будто нужно переделать
g.logger.Info(ctx, fmt.Sprintf("try to push refspec %s", refspec))
if err = repo.PushContext(ctx, &git.PushOptions{
RefSpecs: []gitconfig.RefSpec{refspec},
Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Force: true,
}); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to push repo branch: %v", err))
} // пытаемся за пушить изменения
body := map[string]string{
"id": g.RepositoryId,
"source_branch": sourceBranch,
"target_branch": branch,
"title": wTitle.String(),
"description": wBody.String(),
}
g.logger.Info(ctx, fmt.Sprintf("raw body: %#+v", body))
buf, err = json.Marshal(body)
if err != nil {
return err
}
g.logger.Info(ctx, fmt.Sprintf("marshal body: %s", buf))
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("https://%s/api/v4/projects/%s/merge_requests", g.URL, g.RepositoryId),
bytes.NewReader(buf),
)
if err != nil {
return err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+g.Password)
rsp, err := http.DefaultClient.Do(req)
if err != nil {
return err
} // Вроде создаем новый реквест на создание пулл реквеста
if rsp.StatusCode != http.StatusCreated {
buf, _ = io.ReadAll(rsp.Body)
return fmt.Errorf("unknown error: %s", buf)
}
g.logger.Info(ctx, fmt.Sprintf("PR create for %s-%s", path, mod.Version))
return nil
}
func (g *Gitlab) RequestClose(ctx context.Context, branch string, path string) error {
g.logger.Debug(ctx, fmt.Sprintf("RequestClose start, mod title: %s", path))
var err error
g.pulls, err = g.GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return err
}
prExist := false
var b string // Name of the branch to be deleted
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) {
g.logger.Info(ctx, fmt.Sprintf("PR for %s exists: %s", path, pull.URL))
prExist = true
b = pull.Source
}
}
if !prExist {
g.logger.Error(ctx, fmt.Sprintf("skip %s since pr does not exist", path))
return ErrPRNotExist
}
req, err := g.DeleteBranch(ctx, g.URL, g.RepositoryId, b, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to create request for delete the branch: %s, err: %s", branch, err))
return err
}
rsp, err := http.DefaultClient.Do(req)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to do request for delete the branch: %s, err: %s, code: %v", branch, err, rsp.StatusCode))
return err
}
g.logger.Info(ctx, fmt.Sprintf("Delete branch for %s successful", path))
return nil
}
func (g *Gitlab) RequestUpdate(ctx context.Context, branch string, path string, mod modules.Update) error {
g.logger.Debug(ctx, fmt.Sprintf("RequestUpdate start, mod title: %s", path))
var err error
g.RepositoryId, err = g.GetRepoID(ctx, g.URL, g.Owner, g.Repository, g.Password)
if err != nil || g.RepositoryId == "" {
return fmt.Errorf("project id is empty")
}
g.pulls, err = g.GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return err
}
prExist := false
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) {
g.logger.Info(ctx, fmt.Sprintf("don't skip %s since pr exist %s", path, pull.URL)) // todo
tVersion := getVersions(pull.Source) // Надо взять просто из названия ветки последнюю версию
if modules.IsNewerVersion(tVersion, mod.Version, false) {
reqDel, err := g.DeleteBranch(ctx, g.URL, g.RepositoryId, pull.Source, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("Error with create request for branch: %s, err: %s", branch, err))
return err
}
rsp, err := http.DefaultClient.Do(reqDel)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("Error with do request for branch: %s, err: %s, code: %v", branch, err, rsp.StatusCode))
return err
}
g.logger.Info(ctx, fmt.Sprintf("Old pr %s successful delete", pull.Source))
} else {
g.logger.Debug(ctx, "The existing PR is relevant")
return nil
}
prExist = true
}
}
if !prExist {
g.logger.Error(ctx, fmt.Sprintf("skip %s since pr does not exist", path))
return ErrPRNotExist
}
return g.RequestOpen(ctx, branch, path, mod) // todo это мне не нравится
}
func (g *Gitlab) RequestList(ctx context.Context, branch string) (map[string]string, error) {
g.logger.Debug(ctx, fmt.Sprintf("RequestList for %s", branch))
var err error
g.RepositoryId, err = g.GetRepoID(ctx, g.URL, g.Owner, g.Repository, g.Password)
if err != nil || g.RepositoryId == "" {
return nil, fmt.Errorf("project id is empty")
}
g.pulls, err = g.GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return nil, err
}
var path string
rMap := make(map[string]string)
for _, pull := range g.pulls {
if !strings.HasPrefix(pull.Title, "Bump ") { // добавляем только реквесты бота по обновлению модулей
continue
}
path = strings.Split(pull.Title, " ")[1] // todo Работет только для дефолтного шаблона
rMap[path] = pull.Title
}
return rMap, nil
}
func getVersions(s string) string {
re := regexp.MustCompile("[vV][0-9]+\\.[0-9]+\\.[0-9]+")
version := re.FindString(s)
return version
}
func (g *Gitlab) DeleteBranch(ctx context.Context, url, projectId, branch, password string) (*http.Request, error) {
var buf []byte
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, fmt.Sprintf("https://%s/api/v4/projects/%s/repository/branches/%s", url, projectId, branch), bytes.NewReader(buf))
if err != nil {
return nil, err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+password)
return req, err
}
func (g *Gitlab) GetPulls(ctx context.Context, url, projectId, branch, password string) ([]*gitlabPull, error) {
pulls := make([]*gitlabPull, 0, 10)
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
fmt.Sprintf("https://%s/api/v4/projects/%s/merge_requests?state=opened&target_branch=%s", url, projectId, branch),
nil)
if err != nil {
return nil, err
} // вроде запроса к репозиторию
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+password)
rsp, err := http.DefaultClient.Do(req) // выполнение запроса
if err != nil {
return nil, err
}
buf, _ := io.ReadAll(rsp.Body)
switch rsp.StatusCode {
case http.StatusOK:
if err = json.Unmarshal(buf, &pulls); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to decode response %s err: %v", buf, err))
return nil, err
}
return pulls, nil
case http.StatusNotFound:
g.logger.Info(ctx, fmt.Sprintf("pull-request is not exist for %s", projectId))
return nil, ErrPRNotExist
default:
return nil, fmt.Errorf("unknown error: %s", buf)
}
}
func (g *Gitlab) GetRepoID(ctx context.Context, url, owner, repo, password string) (rId string, err error) {
var buf []byte
projects := make([]*gitlabProject, 0, 10)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s/api/v4/users/%s/projects?owned=true", url, owner), nil)
if err != nil {
return
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", password)
rsp, err := http.DefaultClient.Do(req)
if err != nil {
return
}
buf, _ = io.ReadAll(rsp.Body)
switch rsp.StatusCode {
case http.StatusOK:
if err = json.Unmarshal(buf, &projects); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to decode response %s err: %v", buf, err))
}
for _, p := range projects {
if p.Name == repo {
rId = strconv.Itoa(int(p.Id))
}
}
return
default:
return rId, fmt.Errorf("unknown error: %s", buf)
}
}
func (g *Gitlab) checkout(w git.Worktree, ref plumbing.Reference) {
ctx := context.Background()
g.logger.Debug(ctx, fmt.Sprintf("Checkout: %s", ref.Name().Short()))
if err := w.Checkout(&git.CheckoutOptions{
Branch: ref.Name(),
Create: false,
Force: true,
Keep: false,
}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to reset: %v", err))
}
}

View File

@ -0,0 +1,44 @@
package gogs
import (
"context"
"fmt"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/pkgdash/internal/configcli"
"go.unistack.org/pkgdash/internal/modules"
)
type Gogs struct {
logger logger.Logger
Username string
Password string
}
func NewGogs(cfg configcli.Config, log logger.Logger) *Gogs {
return &Gogs{
logger: log,
Username: cfg.Source.Username,
Password: cfg.Source.Password,
}
}
func (g *Gogs) Name() string {
return "gogs"
}
func (g *Gogs) RequestOpen(ctx context.Context, branch string, path string, mod modules.Update) error {
return fmt.Errorf("implement me")
}
func (g *Gogs) RequestClose(ctx context.Context, branch string, path string) error {
return fmt.Errorf("implement me")
}
func (g *Gogs) RequestUpdate(ctx context.Context, branch string, path string, mod modules.Update) error {
return fmt.Errorf("implement me")
}
func (g *Gogs) RequestList(ctx context.Context, branch string) (map[string]string, error) {
return nil, fmt.Errorf("implement me")
}

35
internal/source/source.go Normal file
View File

@ -0,0 +1,35 @@
package source
import (
"context"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/pkgdash/internal/configcli"
"go.unistack.org/pkgdash/internal/modules"
"go.unistack.org/pkgdash/internal/source/gitea"
"go.unistack.org/pkgdash/internal/source/github"
"go.unistack.org/pkgdash/internal/source/gitlab"
"go.unistack.org/pkgdash/internal/source/gogs"
)
type SourceControl interface {
Name() string
RequestOpen(ctx context.Context, branch string, path string, mod modules.Update) error
RequestClose(ctx context.Context, branch string, path string) error
RequestUpdate(ctx context.Context, branch string, path string, mod modules.Update) error
RequestList(ctx context.Context, branch string) (map[string]string, error)
}
func NewSourceControl(cfg configcli.Config, log logger.Logger) SourceControl {
switch cfg.Source.TypeGit {
case "github":
return github.NewGithub(cfg, log)
case "gitlab":
return gitlab.NewGitlab(cfg, log)
case "gitea":
return gitea.NewGitea(cfg, log)
case "gogs":
return gogs.NewGogs(cfg, log)
}
return nil
}

View File

@ -0,0 +1,34 @@
//go:build ignore
package postgres
const (
queryListPackage = `
select
id,
name,
url,
comments
--modules,
--issues,
from package;
`
queryAddComment = `
with insert_comm as (
insert into comment(text) values ($1) returning id
)
update package set comments = array_append(comments, (select * from insert_comm)) where id=$2;
`
queryAddPackage = `
insert into package(name, url, modules) values ($1, $2, $3);
`
queryInsMsgGetIDs = `
insert into module(name, version, last_version) values
%s
returning id;
`
queryGetModule = `
select id, name, version, last_version from module
where id in %s ;
`
)

View File

@ -0,0 +1,235 @@
//go:build ignore
package postgres
import (
"context"
"database/sql"
"embed"
"errors"
"fmt"
"strings"
"github.com/golang-migrate/migrate/v3"
mpgx "github.com/golang-migrate/migrate/v4/database/pgx"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/lib/pq"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/pkgdash/internal/config"
"go.unistack.org/pkgdash/internal/models"
pb "go.unistack.org/pkgdash/proto"
)
const (
pathMigration = `migrations/postgres`
)
type Postgres struct {
db *sql.DB
fs embed.FS
}
func NewStorage() func(*sql.DB, embed.FS) interface{} {
return func(db *sql.DB, fs embed.FS) interface{} {
return &Postgres{db: db, fs: fs}
}
}
func (s *Postgres) MigrateUp() error {
driver, err := mpgx.WithInstance(s.db, &mpgx.Config{
MigrationsTable: mpgx.DefaultMigrationsTable,
DatabaseName: config.ServiceName,
})
if err != nil {
return err
}
source, err := iofs.New(s.fs, pathMigration)
if err != nil {
return err
}
// TODO: pass own logger
m, err := migrate.NewWithInstance("fs", source, config.ServiceName, driver)
if err != nil {
return err
}
if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
return nil
}
func (s *Postgres) MigrateDown() error {
driver, err := mpgx.WithInstance(s.db, &mpgx.Config{
MigrationsTable: mpgx.DefaultMigrationsTable,
DatabaseName: config.ServiceName,
})
if err != nil {
return err
}
source, err := iofs.New(s.fs, pathMigration)
if err != nil {
return err
}
// TODO: pass own logger
m, err := migrate.NewWithInstance("fs", source, config.ServiceName, driver)
if err != nil {
return err
}
if err = m.Down(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
return nil
}
func (s *Postgres) PackagesUpdate(ctx context.Context, req *pb.PackagesUpdateReq) error {
panic("need implement")
}
func (s *Postgres) PackagesList(ctx context.Context, req *pb.PackagesListReq) (models.ListPackage, error) {
rows, err := s.db.QueryContext(ctx, queryListPackage)
if err != nil {
return nil, err
}
defer func() {
if err = rows.Close(); err != nil {
return
}
err = rows.Err()
}()
result := make([]*models.Package, 0)
for rows.Next() {
tmp := &models.Package{}
if err = rows.Scan(
&tmp.ID,
&tmp.Name,
&tmp.URL,
pq.Array(&tmp.Comments),
); err != nil {
return nil, err
}
}
return result, err
}
func (s *Postgres) CommentsCreate(ctx context.Context, req *pb.CommentsCreateReq) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
logger.Error(ctx, "AddComment: unable to rollback: %v", rollbackErr)
}
} else {
err = tx.Commit()
}
}()
res, err := tx.ExecContext(ctx, queryAddComment, req.Text, req.PackageId)
if err != nil {
return err
}
if aff, affErr := res.RowsAffected(); err != nil {
err = affErr
} else if aff == 0 {
err = errors.New("rows affected is 0")
}
return err
}
func (s *Postgres) PackagesCreate(ctx context.Context, req *pb.PackagesCreateReq) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
logger.Error(ctx, "AddPackage: unable to rollback: %v", rollbackErr)
}
} else {
err = tx.Commit()
}
}()
res, err := tx.ExecContext(ctx, queryAddPackage, req.Name, req.Url, pq.Array(req.Modules))
if err != nil {
return err
}
if aff, affErr := res.RowsAffected(); err != nil {
err = affErr
} else if aff == 0 {
err = errors.New("rows affected is 0")
}
return err
}
func (s *Postgres) InsertButchModules(ctx context.Context, req []models.Module) ([]uint64, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
logger.Error(ctx, "AddPackage: unable to rollback: %v", rollbackErr)
}
} else {
err = tx.Commit()
}
}()
query := generateQuery(req)
rows, err := tx.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer func() {
if err = rows.Close(); err != nil {
return
}
err = rows.Err()
}()
result := make([]uint64, 0)
for rows.Next() {
tmp := uint64(0)
if err = rows.Scan(&tmp); err != nil {
return nil, err
}
result = append(result, tmp)
}
return result, err
}
func generateQuery(rsp []models.Module) string {
const pattern = `%c('%s', '%s', '%s')`
build := strings.Builder{}
comma := ' '
for i := range rsp {
str := fmt.Sprintf(pattern, comma, rsp[i].Name, rsp[i].Version, rsp[i].LastVersion)
build.WriteString(str)
comma = ','
}
return fmt.Sprintf(queryInsMsgGetIDs, build.String())
}

View File

@ -0,0 +1,40 @@
//go:build ignore
package postgres
import (
"fmt"
"testing"
"go.unistack.org/pkgdash/internal/models"
)
func TestGenerate(t *testing.T) {
m := []models.Module{
{
ID: 1,
Name: "test",
Version: "1.2.3",
Package: 2,
LastVersion: "23.31",
},
{
ID: 1,
Name: "321test",
Version: "1.3",
Package: 4,
LastVersion: "2111.31",
},
{
ID: 1,
Name: "testabcd",
Version: "1.2.3",
Package: 2,
LastVersion: "153453.31",
},
}
str := generateQuery(m)
fmt.Println(str)
}

View File

@ -0,0 +1,19 @@
package sqlite
const (
queryPackageModulesCount = `update packages set modules = $2 where id = $1;`
queryPackagesModulesCreate = `insert into packages_modules as pm (package, module) values ($1, $2) on conflict (package,module) do nothing;`
queryPackagesUpdateLastCheck = `update packages set last_check = CURRENT_TIMESTAMP where id = $1;`
queryPackagesModules = `select modules.id, modules.name, modules.version from modules left join packages_modules on modules.id = packages_modules.module left join packages on packages.id = packages_modules.package where packages_modules.package = $1;`
queryPackagesProcess = `select id, name, url, comments, modules, issues, created, updated, last_check from packages where ROUND((JULIANDAY(CURRENT_TIMESTAMP) - JULIANDAY(last_check)) * 86400) > $1 or last_check is NULL`
queryModulesProcess = `select id, name, version, last_check from modules where ROUND((JULIANDAY(CURRENT_TIMESTAMP) - JULIANDAY(last_check)) * 86400) > $1 or last_check is NULL`
queryPackagesModulesCount = `update packages set modules = $2, last_check = CURRENT_TIMESTAMP where id = $1;`
queryPackagesList = `select id, name, url, comments, modules, issues, created, updated from packages;`
queryPackagesLookup = `select id, name, url, comments, modules, issues, created, updated from packages where id = $1;`
queryCommentsCreate = `insert into comments (comment) values ($1) returning id;`
queryPackagesCreate = `insert into packages as p (name, url) values ($1, $2) on conflict (url) do update set name = p.name returning *;`
queryModulesList = `select id, name, version from modules;`
queryModulesCreate = `insert into modules as m (name, version) values ($1, $2) on conflict (name,version) do update set last_check = CURRENT_TIMESTAMP returning *;`
queryCommentsList = `select id, text, created, updated from comments where package = $1;`
queryHandlersList = `select id, name, coverage from handlers where package = $1;`
)

View File

@ -0,0 +1,254 @@
package sqlite
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/jmoiron/sqlx"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/pkgdash/internal/models"
"go.unistack.org/pkgdash/internal/storage"
pb "go.unistack.org/pkgdash/proto"
)
func init() {
storage.RegisterStorage("sqlite", NewStorage)
}
var _ storage.Storage = (*Sqlite)(nil)
type Sqlite struct {
logger logger.Logger
db *sqlx.DB
}
func NewStorage(log logger.Logger, db *sqlx.DB) interface{} {
return &Sqlite{db: db, logger: log}
}
func (s *Sqlite) PackageModulesCreate(ctx context.Context, pkg *models.Package, modules []*models.Module) error {
tx, err := s.db.BeginTxx(ctx, nil)
if err != nil {
return err
}
for _, mod := range modules {
err = tx.GetContext(ctx, mod, queryModulesCreate, mod.Name, mod.Version)
if err != nil {
_ = tx.Rollback()
return err
}
_, err = tx.ExecContext(ctx, queryPackagesModulesCreate, pkg.ID, mod.ID)
if err != nil {
_ = tx.Rollback()
return err
}
}
_, err = tx.ExecContext(ctx, queryPackageModulesCount, pkg.ID, len(modules))
if err != nil {
_ = tx.Rollback()
return err
}
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
return err
}
return nil
}
func (s *Sqlite) PackageDelete(ctx context.Context, req *pb.PackageDeleteReq) error {
return fmt.Errorf("need implement")
}
func (s *Sqlite) PackageUpdate(ctx context.Context, req *pb.PackageUpdateReq) (*models.Package, error) {
return nil, fmt.Errorf("need implement")
}
func (s *Sqlite) PackageLookup(ctx context.Context, req *pb.PackageLookupReq) (*models.Package, error) {
pkg := &models.Package{}
err := s.db.GetContext(ctx, pkg, queryPackagesLookup, req.Id)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return pkg, err
}
func (s *Sqlite) PackageList(ctx context.Context, req *pb.PackageListReq) ([]*models.Package, error) {
var packages []*models.Package
err := s.db.SelectContext(ctx, &packages, queryPackagesList)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return packages, nil
}
func (s *Sqlite) PackageModules(ctx context.Context, req *pb.PackageModulesReq) ([]*models.Module, error) {
var modules []*models.Module
err := s.db.SelectContext(ctx, &modules, queryPackagesModules, req.Package)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return modules, nil
}
func (s *Sqlite) CommentDelete(ctx context.Context, req *pb.CommentDeleteReq) error {
return nil
}
func (s *Sqlite) CommentCreate(ctx context.Context, req *pb.CommentCreateReq) (*models.Comment, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error(ctx, "AddComment: unable to rollback: %v", rollbackErr)
}
} else {
err = tx.Commit()
}
}()
if _, err = tx.ExecContext(ctx, queryCommentsCreate, req.Comment, req.PackageId); err != nil {
return nil, err
}
return nil, nil
}
func (s *Sqlite) PackagesProcess(ctx context.Context, td time.Duration) ([]*models.Package, error) {
var packages []*models.Package
err := s.db.SelectContext(ctx, &packages, queryPackagesProcess, td.Seconds())
if err != nil {
return nil, err
}
return packages, nil
}
func (s *Sqlite) PackagesUpdateLastCheck(ctx context.Context, packages []*models.Package) error {
tx, err := s.db.BeginTxx(ctx, nil)
if err != nil {
return err
}
for _, pkg := range packages {
if _, err = tx.ExecContext(ctx, queryPackagesUpdateLastCheck, pkg.ID); err != nil {
tx.Rollback()
return err
}
}
if err = tx.Commit(); err != nil {
tx.Rollback()
return err
}
return nil
}
func (s *Sqlite) ModulesProcess(ctx context.Context, td time.Duration) ([]*models.Module, error) {
var modules []*models.Module
err := s.db.SelectContext(ctx, &modules, queryModulesProcess, td.Seconds())
if err != nil {
return nil, err
}
return modules, nil
}
func (s *Sqlite) PackageCreate(ctx context.Context, req *pb.PackageCreateReq) (*models.Package, error) {
pkg := &models.Package{}
err := s.db.GetContext(ctx, pkg, queryPackagesCreate, req.Name, req.Url)
if err != nil {
return nil, err
}
return pkg, nil
}
func (s *Sqlite) ModuleCreate(ctx context.Context, modules []*models.Module) error {
tx, err := s.db.BeginTxx(ctx, nil)
if err != nil {
return err
}
for _, mod := range modules {
err = tx.GetContext(ctx, mod, queryModulesCreate, mod.Name, mod.Version)
if err != nil {
_ = tx.Rollback()
return err
}
}
if err = tx.Commit(); err != nil {
_ = tx.Rollback()
return err
}
return nil
}
func (s *Sqlite) ModuleList(ctx context.Context, req *pb.ModuleListReq) ([]*models.Module, error) {
var modules []*models.Module
err := s.db.SelectContext(ctx, &modules, queryModulesList)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return modules, nil
}
func (s *Sqlite) CommentList(ctx context.Context, req *pb.CommentListReq) ([]*models.Comment, error) {
var comments []*models.Comment
err := s.db.SelectContext(ctx, &comments, queryCommentsList, req.Package)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return comments, nil
}
func (s *Sqlite) HandlerList(ctx context.Context, req *pb.HandlerListReq) ([]*models.Handler, error) {
var handlers []*models.Handler
err := s.db.SelectContext(ctx, &handlers, queryHandlersList, req.Package)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return handlers, nil
}

View File

@ -0,0 +1,50 @@
package storage
import (
"context"
"errors"
"time"
"github.com/jmoiron/sqlx"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/pkgdash/internal/models"
pb "go.unistack.org/pkgdash/proto"
)
func RegisterStorage(name string, fn func(logger.Logger, *sqlx.DB) interface{}) {
storages[name] = fn
}
var storages = map[string]func(logger.Logger, *sqlx.DB) interface{}{}
type Storage interface {
PackageModulesCreate(ctx context.Context, pkg *models.Package, modules []*models.Module) error
PackagesUpdateLastCheck(ctx context.Context, packages []*models.Package) error
PackageModules(ctx context.Context, req *pb.PackageModulesReq) ([]*models.Module, error)
ModulesProcess(ctx context.Context, td time.Duration) ([]*models.Module, error)
PackagesProcess(ctx context.Context, td time.Duration) ([]*models.Package, error)
PackageCreate(ctx context.Context, req *pb.PackageCreateReq) (*models.Package, error)
HandlerList(ctx context.Context, req *pb.HandlerListReq) ([]*models.Handler, error)
PackageList(ctx context.Context, req *pb.PackageListReq) ([]*models.Package, error)
PackageLookup(ctx context.Context, req *pb.PackageLookupReq) (*models.Package, error)
PackageUpdate(ctx context.Context, req *pb.PackageUpdateReq) (*models.Package, error)
PackageDelete(ctx context.Context, req *pb.PackageDeleteReq) error
CommentCreate(ctx context.Context, req *pb.CommentCreateReq) (*models.Comment, error)
CommentDelete(ctx context.Context, req *pb.CommentDeleteReq) error
CommentList(ctx context.Context, req *pb.CommentListReq) ([]*models.Comment, error)
ModuleList(ctx context.Context, req *pb.ModuleListReq) ([]*models.Module, error)
ModuleCreate(ctx context.Context, modules []*models.Module) error
}
func NewStorage(name string, log logger.Logger, db *sqlx.DB) (Storage, error) {
fn, ok := storages[name]
if !ok {
return nil, errors.New("incorrect name store")
}
store := fn(log, db)
database, ok := store.(Storage)
if !ok {
return nil, errors.New("dont implements interface Storage")
}
return database, nil
}

View File

@ -0,0 +1,69 @@
package storage
import (
"context"
"database/sql"
"fmt"
"testing"
"go.unistack.org/pkgdash/internal/storage/sqlite"
pb "go.unistack.org/pkgdash/proto"
)
func TestGetModule(t *testing.T) {
conn, err := sql.Open("sqlite3", "/Users/devstigneev_local/GolandProjects/unistack/pkgdash/identifier.sqlite")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
if err = conn.Ping(); err != nil {
t.Fatal(err)
}
st := sqlite.NewStorage()
store := st(conn, fs)
s, ok := store.(Storage)
if !ok {
t.Fatal("dont implements interface Storage")
}
req := &pb.GetModuleReq{
Id: []uint64{1, 2, 3},
}
module, err := s.GetModule(context.Background(), req)
if err != nil {
t.Fatal(err)
}
fmt.Println(module)
}
func TestGetComment(t *testing.T) {
conn, err := sql.Open("sqlite3", "/Users/devstigneev_local/GolandProjects/unistack/pkgdash/identifier.sqlite")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
if err = conn.Ping(); err != nil {
t.Fatal(err)
}
st := sqlite.NewStorage()
store := st(conn, fs)
s, ok := store.(Storage)
if !ok {
t.Fatal("dont implements interface Storage")
}
req := &pb.GetCommentsReq{
Id: []uint64{1, 2, 3, 15},
}
comments, err := s.GetComment(context.Background(), req)
if err != nil {
t.Fatal(err)
}
fmt.Println(comments.Decode())
}

226
internal/worker/worker.go Normal file
View File

@ -0,0 +1,226 @@
package worker
import (
"context"
"database/sql"
"fmt"
"io"
"net/url"
"os"
"sort"
"strings"
"sync"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/pkg/errors"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/pkgdash/internal/models"
"go.unistack.org/pkgdash/internal/modules"
"go.unistack.org/pkgdash/internal/storage"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
)
func Run(ctx context.Context, log logger.Logger, store storage.Storage, td time.Duration) {
modTicker := time.NewTicker(5 * time.Second)
defer modTicker.Stop()
pkgTicker := time.NewTicker(5 * time.Second)
defer pkgTicker.Stop()
var wg sync.WaitGroup
for {
select {
case <-ctx.Done():
return
case <-pkgTicker.C:
packages, err := store.PackagesProcess(ctx, td)
if err != nil {
if err != sql.ErrNoRows {
continue
}
log.Fatal(ctx, "failed to get packages to process: %v", err)
}
wg.Add(len(packages))
for _, pkg := range packages {
go func(p *models.Package) {
if err := parseModFile(ctx, log, store, p); err != nil {
log.Error(ctx, "failed to process package %s: %v", p.Name, err)
}
p.LastCheck.Time = time.Now()
wg.Done()
}(pkg)
}
wg.Wait()
if err = store.PackagesUpdateLastCheck(ctx, packages); err != nil {
log.Error(ctx, "update packages last_check %#+v, err: %v", packages, err)
}
case <-modTicker.C:
modules, err := store.ModulesProcess(ctx, td)
if err != nil {
if err != sql.ErrNoRows {
continue
}
log.Fatal(ctx, "failed to get modules to process: %v", err)
}
if err := processModules(ctx, log, store, modules); err != nil {
log.Error(ctx, "failed to process modules: %v", err)
}
}
}
}
func parseModFile(ctx context.Context, log logger.Logger, store storage.Storage, pkg *models.Package) error {
log.Info(ctx, "process package %v", pkg)
u, err := url.Parse(pkg.URL)
if err != nil {
return err
}
var rev string
if idx := strings.Index(u.Path, "@"); idx > 0 {
rev = u.Path[idx+1:]
}
cloneOpts := &git.CloneOptions{
URL: pkg.URL,
Progress: os.Stdout,
}
if len(rev) == 0 {
cloneOpts.SingleBranch = true
cloneOpts.Depth = 1
}
if err = cloneOpts.Validate(); err != nil {
return err
}
repo, err := git.CloneContext(ctx, memory.NewStorage(), nil, cloneOpts)
if err != nil {
return err
}
ref, err := repo.Head()
if err != nil {
return fmt.Errorf("failed to get head: %v", err)
}
commit, err := repo.CommitObject(ref.Hash())
if err != nil {
return fmt.Errorf("failed to get commit: %v", err)
}
tree, err := commit.Tree()
if err != nil {
return err
}
unique := make(map[string]*models.Module)
var mvs []module.Version
err = tree.Files().ForEach(func(file *object.File) error {
if file == nil {
err = errors.New("file pointer is nil")
log.Error(ctx, "file tree error", err)
return err
}
switch file.Mode {
case filemode.Regular:
if strings.HasSuffix(file.Name, "go.mod") {
if mvs, err = parseFile(file); err != nil {
return err
}
for i := range mvs {
unique[mvs[i].Path] = &models.Module{
Name: mvs[i].Path,
Version: mvs[i].Version,
}
}
}
}
return nil
})
modules := make([]*models.Module, 0, len(unique))
for _, v := range unique {
modules = append(modules, v)
}
sort.Slice(modules, func(i, j int) bool {
return modules[i].Name < modules[j].Name
})
if err = store.PackageModulesCreate(ctx, pkg, modules); err != nil {
log.Error(ctx, "failed to set create modules: %v", err)
return err
}
return nil
}
func processModules(ctx context.Context, log logger.Logger, store storage.Storage, mods []*models.Module) error {
mvs := make(map[string]*models.Module, len(mods))
for _, mod := range mods {
mvs[mod.Name] = mod
}
mvsu := make([]module.Version, 0, len(mvs))
for _, mv := range mvs {
mvsu = append(mvsu, module.Version{Path: mv.Name, Version: mv.Version})
}
modules.Updates(modules.UpdateOptions{
Pre: false,
Major: false,
Cached: false,
Modules: mvsu,
OnUpdate: func(u modules.Update) {
if u.Err != nil {
log.Error(ctx, "%s: failed: %v", u.Module.Path, u.Err)
} else {
mvs[u.Module.Path].Version = u.Version
}
},
})
if err := store.ModuleCreate(ctx, mods); err != nil {
return err
}
return nil
}
func parseFile(file *object.File) ([]module.Version, error) {
r, err := file.Reader()
if err != nil {
return nil, err
}
data, err := io.ReadAll(r)
r.Close()
if err != nil {
return nil, err
}
modfile, err := modfile.ParseLax("go.mod", data, nil)
if err != nil {
return nil, err
}
mods := make([]module.Version, 0, len(modfile.Require))
for _, req := range modfile.Require {
mods = append(mods, req.Mod)
}
sort.Slice(mods, func(i, j int) bool {
return mods[i].Path < mods[j].Path
})
return mods, nil
}

8
local.yaml Normal file
View File

@ -0,0 +1,8 @@
server:
addr: ":9091"
logger_level: "debug"
meter:
addr: ":8081"
path: "/metrics"
database:
dsn: "sqlite://./database.db?migrate=up"

519
proto/apidocs.swagger.yaml Normal file
View File

@ -0,0 +1,519 @@
# Generated with protoc-gen-go-micro
openapi: 3.0.3
info:
title: Pkgdash API
version: 0.0.1
paths:
/v1/comments/{id}:
get:
tags:
- Pkgdash
operationId: CommentLookup
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: uint64
- name: package
in: query
schema:
type: integer
format: uint64
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/CommentLookupRsp'
/v1/modules:
get:
tags:
- Pkgdash
operationId: ModuleList
parameters:
- name: package
in: query
schema:
type: integer
format: uint64
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ModuleListRsp'
/v1/packages:
get:
tags:
- Pkgdash
operationId: PackageList
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PackageListRsp'
post:
tags:
- Pkgdash
operationId: PackageCreate
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PackageCreateReq'
required: true
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PackageCreateRsp'
/v1/packages/{id}:
get:
tags:
- Pkgdash
operationId: PackageLookup
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: uint64
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PackageLookupRsp'
put:
tags:
- Pkgdash
operationId: PackageUpdate
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: uint64
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PackageUpdateReq'
required: true
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PackageUpdateRsp'
delete:
tags:
- Pkgdash
operationId: PackageDelete
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: uint64
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PackageDeleteRsp'
/v1/packages/{package_id}/comments/{id}:
delete:
tags:
- Pkgdash
operationId: CommentDelete
parameters:
- name: package_id
in: path
required: true
schema:
type: integer
format: uint64
- name: id
in: path
required: true
schema:
type: integer
format: uint64
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/CommentDeleteRsp'
/v1/packages/{package}/comments:
get:
tags:
- Pkgdash
operationId: CommentList
parameters:
- name: package
in: path
required: true
schema:
type: integer
format: uint64
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/CommentListRsp'
post:
tags:
- Pkgdash
operationId: CommentCreate
parameters:
- name: package
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CommentCreateReq'
required: true
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/CommentCreateRsp'
/v1/packages/{package}/handlers:
get:
tags:
- Pkgdash
operationId: HandlerList
parameters:
- name: package
in: path
required: true
schema:
type: integer
format: uint64
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/HandlerListRsp'
/v1/packages/{package}/modules:
get:
tags:
- Pkgdash
operationId: PackageModules
parameters:
- name: package
in: path
required: true
schema:
type: integer
format: uint64
responses:
default:
description: Default
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorRsp'
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PackageModulesRsp'
components:
schemas:
Comment:
type: object
properties:
id:
type: integer
format: uint64
package:
type: integer
format: uint64
comment:
type: string
created:
type: string
format: RFC3339
updated:
type: string
format: RFC3339
CommentCreateReq:
type: object
properties:
package_id:
type: integer
format: uint64
comment:
type: string
CommentCreateRsp:
type: object
properties:
comment:
$ref: '#/components/schemas/Comment'
CommentDeleteRsp:
type: object
properties: {}
CommentListRsp:
type: object
properties:
comments:
type: array
items:
$ref: '#/components/schemas/Comment'
CommentLookupRsp:
type: object
properties:
comment:
$ref: '#/components/schemas/Comment'
ErrorRsp:
type: object
properties:
code:
type: string
title:
type: string
uuid:
type: string
details:
type: string
Handler:
type: object
properties:
id:
type: integer
format: uint64
package:
type: integer
format: uint64
name:
type: string
coverage:
type: number
format: double
HandlerListRsp:
type: object
properties:
handlers:
type: array
items:
$ref: '#/components/schemas/Handler'
Module:
type: object
properties:
id:
type: integer
format: uint64
name:
type: string
version:
type: string
last_check:
type: string
format: RFC3339
ModuleListRsp:
type: object
properties:
modules:
type: array
items:
$ref: '#/components/schemas/Module'
Package:
type: object
properties:
id:
type: integer
format: uint64
name:
type: string
url:
type: string
description:
type: string
modules:
type: integer
format: uint64
issues:
type: integer
format: uint64
comments:
type: integer
format: uint64
handlers:
type: integer
format: uint64
created:
type: string
format: RFC3339
updated:
type: string
format: RFC3339
last_check:
type: string
format: RFC3339
type:
type: string
coverage:
type: number
format: double
PackageCreateReq:
type: object
properties:
name:
type: string
url:
type: string
description:
type: string
PackageCreateRsp:
type: object
properties:
package:
$ref: '#/components/schemas/Package'
PackageDeleteRsp:
type: object
properties: {}
PackageListRsp:
type: object
properties:
packages:
type: array
items:
$ref: '#/components/schemas/Package'
PackageLookupRsp:
type: object
properties:
package:
$ref: '#/components/schemas/Package'
PackageModulesRsp:
type: object
properties:
modules:
type: array
items:
$ref: '#/components/schemas/Module'
PackageUpdateReq:
type: object
properties:
id:
type: integer
format: uint64
name:
type: string
url:
type: string
modules:
type: array
items:
type: integer
format: uint64
issues:
type: array
items:
type: integer
format: uint64
coverprofile:
type: string
format: bytes
PackageUpdateRsp:
type: object
properties:
package:
$ref: '#/components/schemas/Package'
tags:
- name: Pkgdash

17
proto/micro_errors.pb.go Normal file
View File

@ -0,0 +1,17 @@
// Code generated by protoc-gen-go-micro. DO NOT EDIT.
// protoc-gen-go-micro version: v3.10.4
package pkgdashpb
import (
protojson "google.golang.org/protobuf/encoding/protojson"
)
var (
marshaler = protojson.MarshalOptions{}
)
func (m *ErrorRsp) Error() string {
buf, _ := marshaler.Marshal(m)
return string(buf)
}

2524
proto/pkgdash.pb.go Normal file

File diff suppressed because it is too large Load Diff

3838
proto/pkgdash.pb.validate.go Normal file

File diff suppressed because it is too large Load Diff

318
proto/pkgdash.proto Normal file
View File

@ -0,0 +1,318 @@
syntax = "proto3";
package pkgdash;
import "api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "openapiv3/annotations.proto";
import "validate/validate.proto";
option go_package = "go.unistack.org/pkgdash/proto;pkgdashpb";
service Pkgdash {
rpc PackageLookup(PackageLookupReq) returns (PackageLookupRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "PackageLookup";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {get: "/v1/packages/{id}"};
}
rpc PackageCreate(PackageCreateReq) returns (PackageCreateRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "PackageCreate";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {
post: "/v1/packages";
body: "*";
};
}
rpc PackageDelete(PackageDeleteReq) returns (PackageDeleteRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "PackageDelete";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {delete: "/v1/packages/{id}"};
}
rpc PackageList(PackageListReq) returns (PackageListRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "PackageList";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {get: "/v1/packages"};
}
rpc HandlerList(HandlerListReq) returns (HandlerListRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "HandlerList";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {get: "/v1/packages/{package}/handlers"};
}
rpc PackageModules(PackageModulesReq) returns (PackageModulesRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "PackageModules";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {get: "/v1/packages/{package}/modules"};
}
rpc PackageUpdate(PackageUpdateReq) returns (PackageUpdateRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "PackageUpdate";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {
put: "/v1/packages/{id}";
body: "*";
};
}
rpc CommentCreate(CommentCreateReq) returns (CommentCreateRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "CommentCreate";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {
post: "/v1/packages/{package}/comments";
body: "*";
};
}
rpc CommentLookup(CommentLookupReq) returns (CommentLookupRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "CommentLookup";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {
get: "/v1/comments/{id}";
additional_bindings {get: "/v1/comments/{package}/comments/{id}"};
};
}
rpc CommentList(CommentListReq) returns (CommentListRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "CommentList";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {get: "/v1/packages/{package}/comments"};
}
rpc CommentDelete(CommentDeleteReq) returns (CommentDeleteRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "CommentDelete";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {
delete: "/v1/packages/{package_id}/comments/{id}";
additional_bindings {delete: "/v1/comments/{id}"};
};
}
rpc ModuleList(ModuleListReq) returns (ModuleListRsp) {
option (micro.openapiv3.openapiv3_operation) = {
operation_id: "ModuleList";
responses: {
default: {
reference: {_ref: ".pkgdash.ErrorRsp"};
};
};
};
option (micro.api.http) = {get: "/v1/modules"};
}
}
message HandlerListReq {
uint64 package = 1;
}
message HandlerListRsp {
repeated Handler handlers = 1;
}
message PackageModulesReq {
uint64 package = 1 [json_name = "package"];
}
message PackageModulesRsp {
repeated Module modules = 1 [json_name = "modules"];
}
message PackageLookupReq {
uint64 id = 1 [json_name = "id"];
}
message PackageLookupRsp {
Package package = 1 [json_name = "package"];
}
message ErrorRsp {
string code = 1 [json_name = "code"];
string title = 2 [json_name = "title"];
string uuid = 3 [json_name = "uuid"];
string details = 4 [json_name = "details"];
}
message Package {
uint64 id = 1 [(validate.rules).uint64.gt = 0];
string name = 2 [(validate.rules).string.min_len = 1];
string url = 3 [(validate.rules).string.min_len = 1];
string description = 4 [(validate.rules).string.min_len = 1];
uint64 modules = 5;
uint64 issues = 6;
uint64 comments = 7;
uint64 handlers = 8;
google.protobuf.Timestamp created = 9;
google.protobuf.Timestamp updated = 10;
google.protobuf.Timestamp last_check = 11;
string type = 12;
double coverage = 13;
}
message Handler {
uint64 id = 1 [(validate.rules).uint64.gt = 0];
uint64 package = 2 [(validate.rules).uint64.gt = 0];
string name = 3 [(validate.rules).string.min_len = 1];
double coverage = 4;
}
message Module {
uint64 id = 1 [(validate.rules).uint64.gt = 0];
string name = 2 [(validate.rules).string.min_len = 1];
string version = 3 [(validate.rules).string.min_len = 1];
google.protobuf.Timestamp last_check = 8;
}
message Issue {
uint64 id = 1 [(validate.rules).uint64.gt = 0];
uint64 status = 2 [(validate.rules).uint64.gt = 0];
string desc = 3 [(validate.rules).string.min_len = 1];
uint64 package = 4 [(validate.rules).uint64.gt = 0];
repeated uint64 modules = 5;
google.protobuf.Timestamp created = 6;
google.protobuf.Timestamp updated = 7;
}
message Comment {
uint64 id = 1 [(validate.rules).uint64.gt = 0];
uint64 package = 2 [(validate.rules).uint64.gt = 0];
string comment = 3;
google.protobuf.Timestamp created = 4;
google.protobuf.Timestamp updated = 5;
}
message CommentDeleteReq {
uint64 id = 1 [json_name = "id"];
uint64 package_id = 2 [json_name = "package_id"];
}
message CommentDeleteRsp {}
message PackageDeleteReq {
uint64 id = 1 [json_name = "id"];
}
message PackageDeleteRsp {}
message PackageListReq {}
message PackageListRsp {
repeated Package packages = 1;
}
message PackageUpdateReq {
uint64 id = 1 [(validate.rules).uint64.gt = 0];
string name = 2 [(validate.rules).string.min_len = 1];
string url = 3 [(validate.rules).string.min_len = 1];
repeated uint64 modules = 4;
repeated uint64 issues = 5;
bytes coverprofile = 6;
}
message PackageUpdateRsp {
Package package = 1 [json_name = "package"];
}
message CommentCreateReq {
uint64 package_id = 1 [
json_name = "package_id",
(validate.rules).uint64.gt = 0
];
string comment = 2;
}
message CommentCreateRsp {
Comment comment = 1 [json_name = "comment"];
}
message PackageCreateReq {
string name = 1 [(validate.rules).string.min_len = 1];
string url = 2 [(validate.rules).string.min_len = 1];
string description = 3;
}
message PackageCreateRsp {
Package package = 1 [json_name = "package"];
}
message ModuleListReq {
uint64 package = 1 [json_name = "package"];
}
message ModuleListRsp {
repeated Module modules = 1 [json_name = "modules"];
}
message CommentListReq {
uint64 package = 1 [json_name = "package"];
}
message CommentListRsp {
repeated Comment comments = 1 [json_name = "comments"];
}
message CommentLookupReq {
uint64 id = 1 [json_name = "id"];
uint64 package = 2 [json_name = "package"];
}
message CommentLookupRsp {
Comment comment = 1 [json_name = "comment"];
}

46
proto/pkgdash_micro.pb.go Normal file
View File

@ -0,0 +1,46 @@
// Code generated by protoc-gen-go-micro. DO NOT EDIT.
// versions:
// - protoc-gen-go-micro v3.10.4
// - protoc v5.28.3
// source: pkgdash.proto
package pkgdashpb
import (
context "context"
client "go.unistack.org/micro/v3/client"
)
var (
PkgdashName = "Pkgdash"
)
type PkgdashClient interface {
PackageLookup(ctx context.Context, req *PackageLookupReq, opts ...client.CallOption) (*PackageLookupRsp, error)
PackageCreate(ctx context.Context, req *PackageCreateReq, opts ...client.CallOption) (*PackageCreateRsp, error)
PackageDelete(ctx context.Context, req *PackageDeleteReq, opts ...client.CallOption) (*PackageDeleteRsp, error)
PackageList(ctx context.Context, req *PackageListReq, opts ...client.CallOption) (*PackageListRsp, error)
HandlerList(ctx context.Context, req *HandlerListReq, opts ...client.CallOption) (*HandlerListRsp, error)
PackageModules(ctx context.Context, req *PackageModulesReq, opts ...client.CallOption) (*PackageModulesRsp, error)
PackageUpdate(ctx context.Context, req *PackageUpdateReq, opts ...client.CallOption) (*PackageUpdateRsp, error)
CommentCreate(ctx context.Context, req *CommentCreateReq, opts ...client.CallOption) (*CommentCreateRsp, error)
CommentLookup(ctx context.Context, req *CommentLookupReq, opts ...client.CallOption) (*CommentLookupRsp, error)
CommentList(ctx context.Context, req *CommentListReq, opts ...client.CallOption) (*CommentListRsp, error)
CommentDelete(ctx context.Context, req *CommentDeleteReq, opts ...client.CallOption) (*CommentDeleteRsp, error)
ModuleList(ctx context.Context, req *ModuleListReq, opts ...client.CallOption) (*ModuleListRsp, error)
}
type PkgdashServer interface {
PackageLookup(ctx context.Context, req *PackageLookupReq, rsp *PackageLookupRsp) error
PackageCreate(ctx context.Context, req *PackageCreateReq, rsp *PackageCreateRsp) error
PackageDelete(ctx context.Context, req *PackageDeleteReq, rsp *PackageDeleteRsp) error
PackageList(ctx context.Context, req *PackageListReq, rsp *PackageListRsp) error
HandlerList(ctx context.Context, req *HandlerListReq, rsp *HandlerListRsp) error
PackageModules(ctx context.Context, req *PackageModulesReq, rsp *PackageModulesRsp) error
PackageUpdate(ctx context.Context, req *PackageUpdateReq, rsp *PackageUpdateRsp) error
CommentCreate(ctx context.Context, req *CommentCreateReq, rsp *CommentCreateRsp) error
CommentLookup(ctx context.Context, req *CommentLookupReq, rsp *CommentLookupRsp) error
CommentList(ctx context.Context, req *CommentListReq, rsp *CommentListRsp) error
CommentDelete(ctx context.Context, req *CommentDeleteReq, rsp *CommentDeleteRsp) error
ModuleList(ctx context.Context, req *ModuleListReq, rsp *ModuleListRsp) error
}

View File

@ -0,0 +1,421 @@
// Code generated by protoc-gen-go-micro. DO NOT EDIT.
// protoc-gen-go-micro version: v3.10.4
// source: pkgdash.proto
package pkgdashpb
import (
context "context"
v31 "go.unistack.org/micro-client-http/v3"
v3 "go.unistack.org/micro-server-http/v3"
client "go.unistack.org/micro/v3/client"
server "go.unistack.org/micro/v3/server"
http "net/http"
)
var (
PkgdashServerEndpoints = []v3.EndpointMetadata{
{
Name: "Pkgdash.PackageLookup",
Path: "/v1/packages/{id}",
Method: "GET",
Body: "",
Stream: false,
},
{
Name: "Pkgdash.PackageCreate",
Path: "/v1/packages",
Method: "POST",
Body: "*",
Stream: false,
},
{
Name: "Pkgdash.PackageDelete",
Path: "/v1/packages/{id}",
Method: "DELETE",
Body: "",
Stream: false,
},
{
Name: "Pkgdash.PackageList",
Path: "/v1/packages",
Method: "GET",
Body: "",
Stream: false,
},
{
Name: "Pkgdash.HandlerList",
Path: "/v1/packages/{package}/handlers",
Method: "GET",
Body: "",
Stream: false,
},
{
Name: "Pkgdash.PackageModules",
Path: "/v1/packages/{package}/modules",
Method: "GET",
Body: "",
Stream: false,
},
{
Name: "Pkgdash.PackageUpdate",
Path: "/v1/packages/{id}",
Method: "PUT",
Body: "*",
Stream: false,
},
{
Name: "Pkgdash.CommentCreate",
Path: "/v1/packages/{package}/comments",
Method: "POST",
Body: "*",
Stream: false,
},
{
Name: "Pkgdash.CommentLookup",
Path: "/v1/comments/{id}",
Method: "GET",
Body: "",
Stream: false,
},
{
Name: "Pkgdash.CommentLookup",
Path: "/v1/comments/{package}/comments/{id}",
Method: "GET",
Body: "",
Stream: false,
},
{
Name: "Pkgdash.CommentList",
Path: "/v1/packages/{package}/comments",
Method: "GET",
Body: "",
Stream: false,
},
{
Name: "Pkgdash.CommentDelete",
Path: "/v1/packages/{package_id}/comments/{id}",
Method: "DELETE",
Body: "",
Stream: false,
},
{
Name: "Pkgdash.CommentDelete",
Path: "/v1/comments/{id}",
Method: "DELETE",
Body: "",
Stream: false,
},
{
Name: "Pkgdash.ModuleList",
Path: "/v1/modules",
Method: "GET",
Body: "",
Stream: false,
},
}
)
type pkgdashClient struct {
c client.Client
name string
}
func NewPkgdashClient(name string, c client.Client) PkgdashClient {
return &pkgdashClient{c: c, name: name}
}
func (c *pkgdashClient) PackageLookup(ctx context.Context, req *PackageLookupReq, opts ...client.CallOption) (*PackageLookupRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodGet),
v31.Path("/v1/packages/{id}"),
)
rsp := &PackageLookupRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.PackageLookup", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
func (c *pkgdashClient) PackageCreate(ctx context.Context, req *PackageCreateReq, opts ...client.CallOption) (*PackageCreateRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodPost),
v31.Path("/v1/packages"),
v31.Body("*"),
)
rsp := &PackageCreateRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.PackageCreate", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
func (c *pkgdashClient) PackageDelete(ctx context.Context, req *PackageDeleteReq, opts ...client.CallOption) (*PackageDeleteRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodDelete),
v31.Path("/v1/packages/{id}"),
)
rsp := &PackageDeleteRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.PackageDelete", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
func (c *pkgdashClient) PackageList(ctx context.Context, req *PackageListReq, opts ...client.CallOption) (*PackageListRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodGet),
v31.Path("/v1/packages"),
)
rsp := &PackageListRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.PackageList", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
func (c *pkgdashClient) HandlerList(ctx context.Context, req *HandlerListReq, opts ...client.CallOption) (*HandlerListRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodGet),
v31.Path("/v1/packages/{package}/handlers"),
)
rsp := &HandlerListRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.HandlerList", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
func (c *pkgdashClient) PackageModules(ctx context.Context, req *PackageModulesReq, opts ...client.CallOption) (*PackageModulesRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodGet),
v31.Path("/v1/packages/{package}/modules"),
)
rsp := &PackageModulesRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.PackageModules", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
func (c *pkgdashClient) PackageUpdate(ctx context.Context, req *PackageUpdateReq, opts ...client.CallOption) (*PackageUpdateRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodPut),
v31.Path("/v1/packages/{id}"),
v31.Body("*"),
)
rsp := &PackageUpdateRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.PackageUpdate", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
func (c *pkgdashClient) CommentCreate(ctx context.Context, req *CommentCreateReq, opts ...client.CallOption) (*CommentCreateRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodPost),
v31.Path("/v1/packages/{package}/comments"),
v31.Body("*"),
)
rsp := &CommentCreateRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.CommentCreate", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
func (c *pkgdashClient) CommentLookup(ctx context.Context, req *CommentLookupReq, opts ...client.CallOption) (*CommentLookupRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodGet),
v31.Path("/v1/comments/{id}"),
)
rsp := &CommentLookupRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.CommentLookup", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
func (c *pkgdashClient) CommentList(ctx context.Context, req *CommentListReq, opts ...client.CallOption) (*CommentListRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodGet),
v31.Path("/v1/packages/{package}/comments"),
)
rsp := &CommentListRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.CommentList", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
func (c *pkgdashClient) CommentDelete(ctx context.Context, req *CommentDeleteReq, opts ...client.CallOption) (*CommentDeleteRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodDelete),
v31.Path("/v1/packages/{package_id}/comments/{id}"),
)
rsp := &CommentDeleteRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.CommentDelete", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
func (c *pkgdashClient) ModuleList(ctx context.Context, req *ModuleListReq, opts ...client.CallOption) (*ModuleListRsp, error) {
errmap := make(map[string]interface{}, 1)
errmap["default"] = &ErrorRsp{}
opts = append(opts,
v31.ErrorMap(errmap),
)
opts = append(opts,
v31.Method(http.MethodGet),
v31.Path("/v1/modules"),
)
rsp := &ModuleListRsp{}
err := c.c.Call(ctx, c.c.NewRequest(c.name, "Pkgdash.ModuleList", req), rsp, opts...)
if err != nil {
return nil, err
}
return rsp, nil
}
type pkgdashServer struct {
PkgdashServer
}
func (h *pkgdashServer) PackageLookup(ctx context.Context, req *PackageLookupReq, rsp *PackageLookupRsp) error {
return h.PkgdashServer.PackageLookup(ctx, req, rsp)
}
func (h *pkgdashServer) PackageCreate(ctx context.Context, req *PackageCreateReq, rsp *PackageCreateRsp) error {
return h.PkgdashServer.PackageCreate(ctx, req, rsp)
}
func (h *pkgdashServer) PackageDelete(ctx context.Context, req *PackageDeleteReq, rsp *PackageDeleteRsp) error {
return h.PkgdashServer.PackageDelete(ctx, req, rsp)
}
func (h *pkgdashServer) PackageList(ctx context.Context, req *PackageListReq, rsp *PackageListRsp) error {
return h.PkgdashServer.PackageList(ctx, req, rsp)
}
func (h *pkgdashServer) HandlerList(ctx context.Context, req *HandlerListReq, rsp *HandlerListRsp) error {
return h.PkgdashServer.HandlerList(ctx, req, rsp)
}
func (h *pkgdashServer) PackageModules(ctx context.Context, req *PackageModulesReq, rsp *PackageModulesRsp) error {
return h.PkgdashServer.PackageModules(ctx, req, rsp)
}
func (h *pkgdashServer) PackageUpdate(ctx context.Context, req *PackageUpdateReq, rsp *PackageUpdateRsp) error {
return h.PkgdashServer.PackageUpdate(ctx, req, rsp)
}
func (h *pkgdashServer) CommentCreate(ctx context.Context, req *CommentCreateReq, rsp *CommentCreateRsp) error {
return h.PkgdashServer.CommentCreate(ctx, req, rsp)
}
func (h *pkgdashServer) CommentLookup(ctx context.Context, req *CommentLookupReq, rsp *CommentLookupRsp) error {
return h.PkgdashServer.CommentLookup(ctx, req, rsp)
}
func (h *pkgdashServer) CommentList(ctx context.Context, req *CommentListReq, rsp *CommentListRsp) error {
return h.PkgdashServer.CommentList(ctx, req, rsp)
}
func (h *pkgdashServer) CommentDelete(ctx context.Context, req *CommentDeleteReq, rsp *CommentDeleteRsp) error {
return h.PkgdashServer.CommentDelete(ctx, req, rsp)
}
func (h *pkgdashServer) ModuleList(ctx context.Context, req *ModuleListReq, rsp *ModuleListRsp) error {
return h.PkgdashServer.ModuleList(ctx, req, rsp)
}
func RegisterPkgdashServer(s server.Server, sh PkgdashServer, opts ...server.HandlerOption) error {
type pkgdash interface {
PackageLookup(ctx context.Context, req *PackageLookupReq, rsp *PackageLookupRsp) error
PackageCreate(ctx context.Context, req *PackageCreateReq, rsp *PackageCreateRsp) error
PackageDelete(ctx context.Context, req *PackageDeleteReq, rsp *PackageDeleteRsp) error
PackageList(ctx context.Context, req *PackageListReq, rsp *PackageListRsp) error
HandlerList(ctx context.Context, req *HandlerListReq, rsp *HandlerListRsp) error
PackageModules(ctx context.Context, req *PackageModulesReq, rsp *PackageModulesRsp) error
PackageUpdate(ctx context.Context, req *PackageUpdateReq, rsp *PackageUpdateRsp) error
CommentCreate(ctx context.Context, req *CommentCreateReq, rsp *CommentCreateRsp) error
CommentLookup(ctx context.Context, req *CommentLookupReq, rsp *CommentLookupRsp) error
CommentList(ctx context.Context, req *CommentListReq, rsp *CommentListRsp) error
CommentDelete(ctx context.Context, req *CommentDeleteReq, rsp *CommentDeleteRsp) error
ModuleList(ctx context.Context, req *ModuleListReq, rsp *ModuleListRsp) error
}
type Pkgdash struct {
pkgdash
}
h := &pkgdashServer{sh}
var nopts []server.HandlerOption
nopts = append(nopts, v3.HandlerEndpoints(PkgdashServerEndpoints))
return s.Handle(s.NewHandler(&Pkgdash{h}, append(nopts, opts...)...))
}

16
ui/.browserslistrc Normal file
View File

@ -0,0 +1,16 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR

3
ui/.dockerignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
*/node_modules
**/node_modules

16
ui/.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
ui/.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

27
ui/README.md Normal file
View File

@ -0,0 +1,27 @@
# Ui
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.1.4.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

115
ui/angular.json Normal file
View File

@ -0,0 +1,115 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ui": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/ui",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "ui:build:production"
},
"development": {
"browserTarget": "ui:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "ui:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": false
}
}

44
ui/karma.conf.js Normal file
View File

@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/ui'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

12910
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
ui/package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "ui",
"version": "0.0.1",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"devbuild": "ng build --configuration development",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^14.3.0",
"@angular/cdk": "^13.0.0",
"@angular/common": "^14.3.0",
"@angular/compiler": "^14.3.0",
"@angular/core": "^14.3.0",
"@angular/forms": "^14.3.0",
"@angular/material": "^13.0.0",
"@angular/platform-browser": "^14.3.0",
"@angular/platform-browser-dynamic": "^14.3.0",
"@angular/router": "^14.3.0",
"ng-openapi-gen": "^0.25.1",
"rxjs": "~7.4.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^14.2.12",
"@angular/cli": "^14.2.12",
"@angular/compiler-cli": "^14.3.0",
"@types/jasmine": "~3.10.0",
"@types/node": "^12.11.1",
"jasmine-core": "~3.10.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.1.0",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~4.8.4"
}
}

View File

@ -0,0 +1,20 @@
/* tslint:disable */
/* eslint-disable */
import { Injectable } from '@angular/core';
/**
* Global configuration
*/
@Injectable({
providedIn: 'root',
})
export class ApiConfiguration {
rootUrl: string = '';
}
/**
* Parameters for `ApiModule.forRoot()`
*/
export interface ApiConfigurationParams {
rootUrl?: string;
}

View File

@ -0,0 +1,46 @@
/* tslint:disable */
/* eslint-disable */
import { NgModule, ModuleWithProviders, SkipSelf, Optional } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ApiConfiguration, ApiConfigurationParams } from './api-configuration';
import { PkgdashService } from './services/pkgdash.service';
/**
* Module that provides all services and configuration.
*/
@NgModule({
imports: [],
exports: [],
declarations: [],
providers: [
PkgdashService,
ApiConfiguration
],
})
export class ApiModule {
static forRoot(params: ApiConfigurationParams): ModuleWithProviders<ApiModule> {
return {
ngModule: ApiModule,
providers: [
{
provide: ApiConfiguration,
useValue: params
}
]
}
}
constructor(
@Optional() @SkipSelf() parentModule: ApiModule,
@Optional() http: HttpClient
) {
if (parentModule) {
throw new Error('ApiModule is already loaded. Import in your base AppModule only.');
}
if (!http) {
throw new Error('You need to import the HttpClientModule in your AppModule! \n' +
'See also https://github.com/angular/angular/issues/20575');
}
}
}

View File

@ -0,0 +1,34 @@
/* tslint:disable */
/* eslint-disable */
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ApiConfiguration } from './api-configuration';
/**
* Base class for services
*/
@Injectable()
export class BaseService {
constructor(
protected config: ApiConfiguration,
protected http: HttpClient
) {
}
private _rootUrl: string = '';
/**
* Returns the root url for all operations in this service. If not set directly in this
* service, will fallback to `ApiConfiguration.rootUrl`.
*/
get rootUrl(): string {
return this._rootUrl || this.config.rootUrl;
}
/**
* Sets the root URL for API operations in this service.
*/
set rootUrl(rootUrl: string) {
this._rootUrl = rootUrl;
}
}

22
ui/src/app/api/models.ts Normal file
View File

@ -0,0 +1,22 @@
/* tslint:disable */
/* eslint-disable */
export { Comment } from './models/comment';
export { CommentCreateReq } from './models/comment-create-req';
export { CommentCreateRsp } from './models/comment-create-rsp';
export { CommentDeleteRsp } from './models/comment-delete-rsp';
export { CommentListRsp } from './models/comment-list-rsp';
export { CommentLookupRsp } from './models/comment-lookup-rsp';
export { ErrorRsp } from './models/error-rsp';
export { Handler } from './models/handler';
export { HandlerListRsp } from './models/handler-list-rsp';
export { Module } from './models/module';
export { ModuleListRsp } from './models/module-list-rsp';
export { Package } from './models/package';
export { PackageCreateReq } from './models/package-create-req';
export { PackageCreateRsp } from './models/package-create-rsp';
export { PackageDeleteRsp } from './models/package-delete-rsp';
export { PackageListRsp } from './models/package-list-rsp';
export { PackageLookupRsp } from './models/package-lookup-rsp';
export { PackageModulesRsp } from './models/package-modules-rsp';
export { PackageUpdateReq } from './models/package-update-req';
export { PackageUpdateRsp } from './models/package-update-rsp';

View File

@ -0,0 +1,6 @@
/* tslint:disable */
/* eslint-disable */
export interface CommentCreateReq {
comment?: string;
package_id?: number;
}

View File

@ -0,0 +1,6 @@
/* tslint:disable */
/* eslint-disable */
import { Comment } from './comment';
export interface CommentCreateRsp {
comment?: Comment;
}

View File

@ -0,0 +1,4 @@
/* tslint:disable */
/* eslint-disable */
export interface CommentDeleteRsp {
}

View File

@ -0,0 +1,6 @@
/* tslint:disable */
/* eslint-disable */
import { Comment } from './comment';
export interface CommentListRsp {
comments?: Array<Comment>;
}

View File

@ -0,0 +1,6 @@
/* tslint:disable */
/* eslint-disable */
import { Comment } from './comment';
export interface CommentLookupRsp {
comment?: Comment;
}

View File

@ -0,0 +1,9 @@
/* tslint:disable */
/* eslint-disable */
export interface Comment {
comment?: string;
created?: string;
id?: number;
package?: number;
updated?: string;
}

View File

@ -0,0 +1,8 @@
/* tslint:disable */
/* eslint-disable */
export interface ErrorRsp {
code?: string;
details?: string;
title?: string;
uuid?: string;
}

View File

@ -0,0 +1,6 @@
/* tslint:disable */
/* eslint-disable */
import { Handler } from './handler';
export interface HandlerListRsp {
handlers?: Array<Handler>;
}

View File

@ -0,0 +1,8 @@
/* tslint:disable */
/* eslint-disable */
export interface Handler {
coverage?: number;
id?: number;
name?: string;
package?: number;
}

View File

@ -0,0 +1,6 @@
/* tslint:disable */
/* eslint-disable */
import { Module } from './module';
export interface ModuleListRsp {
modules?: Array<Module>;
}

View File

@ -0,0 +1,8 @@
/* tslint:disable */
/* eslint-disable */
export interface Module {
id?: number;
last_check?: string;
name?: string;
version?: string;
}

View File

@ -0,0 +1,7 @@
/* tslint:disable */
/* eslint-disable */
export interface PackageCreateReq {
description?: string;
name?: string;
url?: string;
}

View File

@ -0,0 +1,6 @@
/* tslint:disable */
/* eslint-disable */
import { Package } from './package';
export interface PackageCreateRsp {
package?: Package;
}

Some files were not shown because too many files have changed in this diff Show More