From 74fd7b53e886d8ecd81e00b746144e9827e87124 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Sat, 28 May 2022 21:34:21 +0800 Subject: [PATCH] init --- .editorconfig | 7 ++ .github/workflows/checks.yml | 21 ++++ .gitignore | 10 ++ LICENSE | 22 ++++ README.md | 39 +++++++ assets/example.cast | 116 ++++++++++++++++++++ assets/example.svg | 1 + config.ts | 6 + deps.ts | 15 +++ mod.ts | 206 +++++++++++++++++++++++++++++++++++ plugin.ts | 30 +++++ plugins/changelog/deps.ts | 1 + plugins/changelog/mod.ts | 56 ++++++++++ plugins/deps/mod.ts | 0 plugins/eggs/mod.ts | 0 plugins/github/api.ts | 56 ++++++++++ plugins/github/deps.ts | 4 + plugins/github/mod.ts | 86 +++++++++++++++ plugins/regex/deps.ts | 1 + plugins/regex/mod.ts | 31 ++++++ plugins/versionFile/deps.ts | 2 + plugins/versionFile/mod.ts | 36 ++++++ src/branch.ts | 8 ++ src/changelog.ts | 121 ++++++++++++++++++++ src/commits.ts | 136 +++++++++++++++++++++++ src/dirs.ts | 9 ++ src/error.ts | 16 +++ src/git.ts | 80 ++++++++++++++ src/remote.ts | 8 ++ src/repo.ts | 79 ++++++++++++++ src/status.ts | 101 +++++++++++++++++ src/store.ts | 37 +++++++ src/tags.ts | 79 ++++++++++++++ zen.ts | 20 ++++ 34 files changed, 1440 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/checks.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/example.cast create mode 100644 assets/example.svg create mode 100644 config.ts create mode 100644 deps.ts create mode 100644 mod.ts create mode 100644 plugin.ts create mode 100644 plugins/changelog/deps.ts create mode 100644 plugins/changelog/mod.ts create mode 100644 plugins/deps/mod.ts create mode 100644 plugins/eggs/mod.ts create mode 100644 plugins/github/api.ts create mode 100644 plugins/github/deps.ts create mode 100644 plugins/github/mod.ts create mode 100644 plugins/regex/deps.ts create mode 100644 plugins/regex/mod.ts create mode 100644 plugins/versionFile/deps.ts create mode 100644 plugins/versionFile/mod.ts create mode 100644 src/branch.ts create mode 100644 src/changelog.ts create mode 100644 src/commits.ts create mode 100644 src/dirs.ts create mode 100644 src/error.ts create mode 100644 src/git.ts create mode 100644 src/remote.ts create mode 100644 src/repo.ts create mode 100644 src/status.ts create mode 100644 src/store.ts create mode 100644 src/tags.ts create mode 100644 zen.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b3dfee7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..c512856 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,21 @@ +name: check + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Setup latest deno version + uses: denolib/setup-deno@v2 + with: + deno-version: v1.x + + - name: Run deno fmt + run: deno fmt --check + + - name: Run deno lint + run: deno lint --unstable diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c45e45a --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# OS files +.DS_Store +.cache + +# IDE +.vscode + +# testing +test +test_2 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cfd11b1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2022-present @sylc +Copyright (c) 2020-present the denosaurs team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3198f5 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# release-me + + +## Installation + +``` +$ deno install -A -f --unstable ... +``` + +## Usage + +``` +usage: release-me [options] [...] + +example: release-me major + +[options]: + -h --help Show this message + --dry Prevent changes to git + +[type]: + release type: + * patch eg: 1.2.3 -> 1.2.4 + * minor eg: 1.2.3 -> 1.3.0 + * major eg: 1.2.3 -> 2.0.0 + * prepatch eg: 1.2.3 -> 1.2.4-name + * preminor eg: 1.2.3 -> 1.2.4-name + * premajor eg: 1.2.3 -> 1.2.4-name +``` + +## Credits + +Big Credits to https://github.com/denosaurs. This project is mainly based on https://github.com/denosaurs/release, where I have done some minor refacatoring. +However due to the lack of development on the original package, I have done some update to suit my needs + +### Contribution + +Pull request, issues and feedback are very welcome. Code style is formatted with +deno fmt and commit messages are done following Conventional Commits spec. diff --git a/assets/example.cast b/assets/example.cast new file mode 100644 index 0000000..38040a4 --- /dev/null +++ b/assets/example.cast @@ -0,0 +1,116 @@ +{"version": 2, "width": 61, "height": 58, "timestamp": 1598897849, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}} +[0, "o", "$ release patch\r\n"] +[0.772547, "o", "\u001b[36m⠋\u001b[39m Loading project info\r\n"] +[0.851344, "o", "\u001b[1A\u001b[2K"] +[0.851482, "o", "\u001b[-1C\u001b[?25h\u001b[32m✔\u001b[39m Project loaded correctly\r\n"] +[0.851695, "o", "\u001b[?25l"] +[0.851779, "o", "\u001b[36m⠋\u001b[39m Checking the project\r\n"] +[0.957429, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠙\u001b[39m Checking the project\r\n"] +[1.052023, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠹\u001b[39m Checking the project\r\n"] +[1.15679, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠸\u001b[39m Checking the project\r\n"] +[1.256493, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[1.256745, "o", "\u001b[36m⠼\u001b[39m Checking the project\r\n"] +[1.353112, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠴\u001b[39m Checking the project\r\n"] +[1.453192, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠦\u001b[39m Checking the project\r\n"] +[1.553107, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠧\u001b[39m Checking the project\r\n"] +[1.653104, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠇\u001b[39m Checking the project\r\n"] +[1.753285, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠏\u001b[39m Checking the project\r\n"] +[1.855355, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[1.855547, "o", "\u001b[?25h\u001b[32m✔\u001b[39m Project check successful\r\n"] +[1.936786, "o", "\u001b[?25l\u001b[36m⠋\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[2.038977, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠙\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[2.137895, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠹\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[2.241962, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠸\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[2.340448, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠼\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[2.440119, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[2.440185, "o", "\u001b[36m⠴\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[2.538018, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠦\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[2.641962, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠧\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[2.742021, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠇\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[2.838091, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠏\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[2.941997, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠋\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[3.038192, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠙\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[3.137955, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠹\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[3.237952, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠸\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[3.341995, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[3.3422, "o", "\u001b[36m⠼\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[3.442681, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠴\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[3.541967, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠦\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[3.640199, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠧\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[3.738046, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[3.738267, "o", "\u001b[36m⠇\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[3.841978, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠏\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[3.939135, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠋\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[4.042051, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠙\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[4.139343, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠹\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[4.237163, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠸\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[4.342431, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠼\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[4.437011, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠴\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[4.540006, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠦\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[4.639091, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[4.639301, "o", "\u001b[36m⠧\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[4.742005, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠇\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[4.842027, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠏\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[4.942026, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠋\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[5.039201, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠙\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[5.141469, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠹\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[5.240055, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[5.240116, "o", "\u001b[36m⠸\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[5.33804, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠼\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[5.44247, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠴\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[5.53717, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠦\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[5.640271, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠧\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[5.742018, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠇\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[5.841989, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠏\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[5.941988, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[5.942088, "o", "\u001b[36m⠋\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[6.037907, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠙\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[6.137936, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠹\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[6.23796, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠸\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[6.342332, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠼\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[6.437968, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠴\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[6.542647, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠦\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[6.642074, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠧\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[6.742012, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠇\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[6.843017, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠏\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[6.938065, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠋\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[7.039028, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠙\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[7.137986, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠹\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[7.242082, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[7.242234, "o", "\u001b[36m⠸\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[7.338073, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠼\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[7.442457, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠴\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[7.53993, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠦\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[7.640464, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠧\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[7.737185, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠇\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[7.840134, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠏\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[7.942081, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠋\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[8.040043, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠙\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[8.141941, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[8.142004, "o", "\u001b[36m⠹\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[8.242043, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠸\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[8.342193, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠼\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[8.440999, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠴\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[8.53902, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠦\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[8.642065, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠧\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[8.739898, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠇\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[8.839237, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[8.839308, "o", "\u001b[36m⠏\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[8.937229, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠋\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[9.041977, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[9.042058, "o", "\u001b[36m⠙\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[9.139971, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠹\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[9.240071, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠸\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[9.3407, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[9.34087, "o", "\u001b[36m⠼\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[9.439119, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠴\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[9.539993, "o", "\u001b[1A\u001b[2K\u001b[-1C"] +[9.540053, "o", "\u001b[36m⠦\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[9.642064, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠧\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[9.742007, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠇\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[9.842007, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[36m⠏\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[9.942078, "o", "\u001b[1A"] +[9.942139, "o", "\u001b[2K\u001b[-1C\u001b[36m⠋\u001b[39m Releasing \u001b[1m0.1.2\u001b[22m \u001b[2m(latest was 0.1.1)\u001b[22m\r\n"] +[10.014842, "o", "\u001b[1A\u001b[2K\u001b[-1C\u001b[?25h"] +[10.0149, "o", "\u001b[32m✔\u001b[39m Released \u001b[1m0.1.2\u001b[22m!\r\n"] +[12.0149, "o", "\r\n"] diff --git a/assets/example.svg b/assets/example.svg new file mode 100644 index 0000000..ac99751 --- /dev/null +++ b/assets/example.svg @@ -0,0 +1 @@ +$releasepatchProjectloadedcorrectlyProjectchecksuccessfulReleasing0.1.2(latestwas0.1.1)Releasing0.1.2(latestwas0.1.1)Releasing0.1.2(latestwas0.1.1)Releasing0.1.2(latestwas0.1.1)Releasing0.1.2(latestwas0.1.1)Releasing0.1.2(latestwas0.1.1)Releasing0.1.2(latestwas0.1.1)Releasing0.1.2(latestwas0.1.1)Releasing0.1.2(latestwas0.1.1)Releasing0.1.2(latestwas0.1.1)Released0.1.2!LoadingprojectinfoCheckingtheprojectCheckingtheprojectCheckingtheprojectCheckingtheprojectCheckingtheprojectCheckingtheprojectCheckingtheprojectCheckingtheprojectCheckingtheprojectCheckingtheproject \ No newline at end of file diff --git a/config.ts b/config.ts new file mode 100644 index 0000000..0ca7638 --- /dev/null +++ b/config.ts @@ -0,0 +1,6 @@ +import type { ReleasePlugin } from "./plugin.ts"; + +export interface ReleaseConfig { + plugins: ReleasePlugin[]; + dry: boolean; +} diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..13ec312 --- /dev/null +++ b/deps.ts @@ -0,0 +1,15 @@ +export * as log from "https://deno.land/x/branch@0.1.6/mod.ts"; +export { Spinner, wait } from "https://deno.land/x/wait@0.1.12/mod.ts"; + +export type { Commit as CCCommit } from "https://deno.land/x/commit@0.1.5/mod.ts"; +export { parse as ccparse } from "https://deno.land/x/commit@0.1.5/mod.ts"; + +export * as semver from "https://deno.land/x/semver@v1.4.0/mod.ts"; +export * as ini from "https://deno.land/x/ini@v2.1.0/mod.ts"; + +export * as colors from "https://deno.land/std@0.107.0/fmt/colors.ts"; + +export { readLines } from "https://deno.land/std@0.107.0/io/mod.ts"; +export { join } from "https://deno.land/std@0.107.0/path/mod.ts"; +export { ensureFile, exists } from "https://deno.land/std@0.107.0/fs/mod.ts"; +export { delay } from "https://deno.land/std@0.107.0/async/delay.ts"; diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..b9d09a1 --- /dev/null +++ b/mod.ts @@ -0,0 +1,206 @@ +import { colors, delay, log, semver, wait } from "./deps.ts"; + +import type { ReleaseConfig } from "./config.ts"; +import { fetchRepo, Repo } from "./src/repo.ts"; +import { ezgit } from "./src/git.ts"; + +import { github } from "./plugins/github/mod.ts"; +import { changelog } from "./plugins/changelog/mod.ts"; +import { regex } from "./plugins/regex/mod.ts"; +import { zen } from "./zen.ts"; +import { versionFile } from "./plugins/versionFile/mod.ts"; + +const logger = log.create("r"); + +const VERSION = "0.1.2"; + +export type Action = + | "patch" + | "minor" + | "major" + | "prepatch" + | "preminor" + | "premajor"; + +if (import.meta.main) { + await log.setup({ filter: "INFO" }); + const args = [...Deno.args]; + + if (~args.indexOf("--help") || ~args.indexOf("-h") || args.length === 0) { + console.log(`${colors.bold("RELEASE")} v${VERSION} +${colors.dim("the denosaurs team")} +${colors.dim(`zen: ${colors.italic(zen())}`)} + +usage: ${colors.yellow("release")} [options] [...] + +example: ${colors.yellow("release")} major + +[options]: + ${colors.bold("-h --help")} ${colors.dim("Show this message")} + ${colors.bold("--dry")} ${colors.dim("Prevent changes to git")} + +[type]: + release type: + * ${colors.bold("patch")} ${colors.dim("eg: 1.2.3 -> 1.2.4")} + * ${colors.bold("minor")} ${colors.dim("eg: 1.2.3 -> 1.3.0")} + * ${colors.bold("major")} ${colors.dim("eg: 1.2.3 -> 2.0.0")} + * ${colors.bold("prepatch ")} ${ + colors.dim( + "eg: 1.2.3 -> 1.2.4-name", + ) + } + * ${colors.bold("preminor ")} ${ + colors.dim( + "eg: 1.2.3 -> 1.2.4-name", + ) + } + * ${colors.bold("premajor ")} ${ + colors.dim( + "eg: 1.2.3 -> 1.2.4-name", + ) + }`); + Deno.exit(0); + } + + const config: ReleaseConfig = { + plugins: [github, changelog, regex, versionFile], + dry: false, + }; + + if (~args.indexOf("--dry")) { + config.dry = true; + args.splice(args.indexOf("--dry"), 1); + } + + const actions = [ + "patch", + "minor", + "major", + "prepatch", + "preminor", + "premajor", + ]; + + let arg = args[0]; + if (!actions.includes(arg)) { + logger.critical(`"${arg}" is not a valid action!`); + Deno.exit(1); + } + if (arg === "pre") arg = "prerelease"; + + const action = arg as Action; + let suffix = undefined; + if (["prepatch", "preminor", "premajor"].includes(action)) { + if (args[1]) suffix = args[1]; + else suffix = "canary"; + } + + const features = { + setup: false, + preCommit: false, + postCommit: false, + }; + + for (const plugin of config.plugins) { + if (plugin.setup) features.setup = true; + if (plugin.preCommit) features.preCommit = true; + if (plugin.postCommit) features.postCommit = true; + } + + for (const plugin of config.plugins) { + if (!plugin.setup) continue; + try { + await plugin.setup(); + } catch (err) { + logger.critical(err.message); + Deno.exit(1); + } + } + + const fetch = wait("Loading project info").start(); + let repo: Repo; + try { + repo = await fetchRepo(Deno.cwd()); + } catch (err) { + fetch.fail(Deno.inspect(err)); + Deno.exit(1); + } + fetch.succeed("Project loaded correctly"); + + const [latest] = repo.tags; + const from = latest ? latest.version : "0.0.0"; + const to = semver.inc(from, action, undefined, suffix)!; + + const integrity = wait("Checking the project").start(); + await delay(1000); + if (repo.status.raw.length !== 0) { + integrity.fail("Uncommitted changes on your repository!"); + Deno.exit(1); + } else if (!repo.commits.some((_) => _.belongs === null)) { + integrity.fail(`No changes since the last release!`); + Deno.exit(1); + } + integrity.succeed("Project check successful"); + + if (features.preCommit) { + for (const plugin of config.plugins) { + if (!plugin.preCommit) continue; + try { + await plugin.preCommit(repo, action, from, to, config); + } catch (err) { + logger.critical(err.message); + Deno.exit(1); + } + } + } + + try { + repo = await fetchRepo(Deno.cwd()); + } catch (err) { + Deno.exit(1); + } + + const bump = wait( + `Releasing ${colors.bold(to)} ${colors.dim(`(latest was ${from})`)}`, + ).start(); + + if (!config.dry) { + try { + await ezgit(repo.path, "add -A"); + await ezgit(repo.path, [ + "commit", + "--allow-empty", + "--message", + `chore: release ${to}`, + ]); + await ezgit(repo.path, `tag ${to}`); + await ezgit(repo.path, "push"); + await ezgit(repo.path, "push --tags"); + } catch (err) { + bump.fail(`Unable to release ${colors.bold(to)}\n`); + logger.critical(err.message); + Deno.exit(1); + } + bump.succeed(`Released ${colors.bold(to)}!`); + } else { + bump.warn( + `Skipping release ${colors.bold(to)} ${ + colors.dim( + `(latest was ${from})`, + ) + }`, + ); + } + + if (features.postCommit) { + for (const plugin of config.plugins) { + if (!plugin.postCommit) continue; + try { + await plugin.postCommit(repo, action, from, to, config); + } catch (err) { + logger.critical(err.message); + Deno.exit(1); + } + } + } +} diff --git a/plugin.ts b/plugin.ts new file mode 100644 index 0000000..fad92ce --- /dev/null +++ b/plugin.ts @@ -0,0 +1,30 @@ +import type { ReleaseConfig } from "./config.ts"; +import type { Action } from "./release.ts"; +import type { Repo } from "./src/repo.ts"; + +export type { ReleaseConfig } from "./config.ts"; +export type { Action } from "./release.ts"; +export type { Repo } from "./src/repo.ts"; +export type { Tag } from "./src/tags.ts"; +export type { Commit } from "./src/commits.ts"; + +export * as store from "./src/store.ts"; + +export interface ReleasePlugin { + name: string; + setup?: () => Promise; + preCommit?: ( + repo: Repo, + action: Action, + from: string, + to: string, + config: ReleaseConfig, + ) => Promise; + postCommit?: ( + repo: Repo, + action: Action, + from: string, + to: string, + config: ReleaseConfig, + ) => Promise; +} diff --git a/plugins/changelog/deps.ts b/plugins/changelog/deps.ts new file mode 100644 index 0000000..6457892 --- /dev/null +++ b/plugins/changelog/deps.ts @@ -0,0 +1 @@ +export { join } from "https://deno.land/std@0.88.0/path/mod.ts"; diff --git a/plugins/changelog/mod.ts b/plugins/changelog/mod.ts new file mode 100644 index 0000000..e638d35 --- /dev/null +++ b/plugins/changelog/mod.ts @@ -0,0 +1,56 @@ +import { join } from "./deps.ts"; + +import type { + Action, + ReleaseConfig, + ReleasePlugin, + Repo, +} from "../../plugin.ts"; +import { + Document, + Filter, + polyfillVersion, + pushHeader, + pushTag, + render, +} from "../../src/changelog.ts"; + +export const changelog = { + name: "Changelog", + async preCommit( + repo: Repo, + action: Action, + from: string, + to: string, + config: ReleaseConfig, + ): Promise { + const doc: Document = { sections: [], links: [] }; + pushHeader(doc); + + const [tags, commits] = polyfillVersion(repo, to); + const filters: Filter[] = [ + { + type: "feat", + title: "Features", + }, + { + type: "fix", + title: "Bug Fixes", + }, + ]; + + for (let i = 0; i < tags.length; i++) { + const tag = tags[i]; + const parent = tags[i + 1]; // last is undefined + const belonging = commits.filter((_) => _.belongs?.hash === tag.hash); + pushTag(doc, repo, belonging, filters, tag, parent); + } + + const md = render(doc); + if (!config.dry) { + await Deno.writeTextFile(join(repo.path, "CHANGELOG.md"), md); + } else { + console.log(md); + } + }, +}; diff --git a/plugins/deps/mod.ts b/plugins/deps/mod.ts new file mode 100644 index 0000000..e69de29 diff --git a/plugins/eggs/mod.ts b/plugins/eggs/mod.ts new file mode 100644 index 0000000..e69de29 diff --git a/plugins/github/api.ts b/plugins/github/api.ts new file mode 100644 index 0000000..3d3b3f7 --- /dev/null +++ b/plugins/github/api.ts @@ -0,0 +1,56 @@ +interface Response { + ok: boolean; + err?: string; +} + +const BASE = "https://api.github.com"; + +const reToken = /^\w+$/; + +export async function verifyToken(token: string): Promise { + if (!token) return { ok: false, err: "Empty token" }; + token = token.trim(); + if (!reToken.test(token)) return { ok: false, err: "Malformed token" }; + const res = await fetch(BASE, { + headers: { + Authorization: `token ${token}`, + }, + }); + const body = await res.json(); + if (body.message === "Bad credentials") { + return { ok: false, err: "Bad credentials" }; + } + const scopes = res.headers.get("X-OAuth-Scopes"); + if (scopes && scopes.includes("repo")) { + return { ok: true }; + } else { + return { ok: false, err: "Missing scope" }; + } +} + +interface Release { + tag_name: string; + name: string; + body: string; + draft: boolean; + prerelease: boolean; +} + +export async function createRelease( + token: string, + owner: string, + repo: string, + release: Release, +): Promise { + if (!token) return { ok: false, err: "Empty token" }; + if (!reToken.test(token)) return { ok: false, err: "Malformed token" }; + const res = await fetch(`${BASE}/repos/${owner}/${repo}/releases`, { + method: "POST", + headers: { + Authorization: `token ${token}`, + }, + body: JSON.stringify(release), + }); + if (res.status !== 201) return { ok: false, err: "Release not created" }; + return { ok: true }; +} diff --git a/plugins/github/deps.ts b/plugins/github/deps.ts new file mode 100644 index 0000000..a7f7995 --- /dev/null +++ b/plugins/github/deps.ts @@ -0,0 +1,4 @@ +export * as log from "https://deno.land/x/branch@0.1.5/mod.ts"; +export { readLines } from "https://deno.land/std@0.88.0/io/mod.ts"; + +export { delay } from "https://deno.land/std@0.88.0/async/delay.ts"; diff --git a/plugins/github/mod.ts b/plugins/github/mod.ts new file mode 100644 index 0000000..3cf3562 --- /dev/null +++ b/plugins/github/mod.ts @@ -0,0 +1,86 @@ +import { log, readLines } from "./deps.ts"; + +import { + Action, + ReleaseConfig, + ReleasePlugin, + Repo, + store, +} from "../../plugin.ts"; +import { + Document, + Filter, + polyfillVersion, + pushTag, + render, +} from "../../src/changelog.ts"; + +import * as gh from "./api.ts"; +import { ReleaseError } from "../../src/error.ts"; + +const logger = log.create("gh"); +const encoder = new TextEncoder(); + +export const github = { + name: "GitHub", + async setup(): Promise { + let token = await store.get(store.known.github); + if (!token) { + logger.warning("GitHub token not found!"); + logger.info("Please enter your GitHub token with score"); + logger.info("(for more info https://git.io/JJyrT)"); + await Deno.stdout.write(encoder.encode("> ")); + for await (let line of readLines(Deno.stdin)) { + token = line; + break; + } + const res = await gh.verifyToken(token!); + if (!res.ok || !token) { + logger.critical(`GitHub token is not valid! (err: ${res.err})`); + Deno.exit(1); + } + logger.info("Token saved to local store!"); + await store.set(store.known.github, token.trim()); + } + }, + async postCommit( + repo: Repo, + action: Action, + from: string, + to: string, + config: ReleaseConfig, + ): Promise { + if (!repo.remote || !repo.remote.github) return; + const doc: Document = { sections: [], links: [] }; + + const [tags, commits] = polyfillVersion(repo, to); + const filters: Filter[] = [ + { + type: "feat", + title: "Features", + }, + { + type: "fix", + title: "Bug Fixes", + }, + ]; + + const latest = tags[0]; + const parent = tags[1]; + const belonging = commits.filter((_) => _.belongs?.hash === latest.hash); + pushTag(doc, repo, belonging, filters, latest, parent, "Changelog"); + + if (!config.dry) { + let token = (await store.get(store.known.github)) as string; + const { user, name } = repo.remote.github; + const result = await gh.createRelease(token, user, name, { + tag_name: to, + name: `v${to}`, + body: render(doc), + prerelease: action.startsWith("pre"), + draft: true, + }); + if (!result.ok) throw new ReleaseError("PLUGIN", result.err); + } + }, +}; diff --git a/plugins/regex/deps.ts b/plugins/regex/deps.ts new file mode 100644 index 0000000..36007c3 --- /dev/null +++ b/plugins/regex/deps.ts @@ -0,0 +1 @@ +export { join } from "https://deno.land/std@0.119.0/path/mod.ts"; diff --git a/plugins/regex/mod.ts b/plugins/regex/mod.ts new file mode 100644 index 0000000..b3953ef --- /dev/null +++ b/plugins/regex/mod.ts @@ -0,0 +1,31 @@ + +import type { + Action, + ReleaseConfig, + ReleasePlugin, + Repo, +} from "../../plugin.ts"; +import { join } from "./deps.ts"; + +export const regex = { + name: "Regex", + async preCommit( + repo: Repo, + _action: Action, + _from: string, + to: string, + config: ReleaseConfig, + ): Promise { + + const readmePath = "README.md" + let text = await Deno.readTextFile(readmePath) + // apply regex. This should come from a config loaded on setup step + // as a prototype, it is harcoded to update versions in urls + text = text.replace(/(?<=@)(.*)(?=\/)/gm, to) + if (!config.dry) { + await Deno.writeTextFile(join(repo.path, readmePath), text); + } else { + console.log(text); + } + }, +}; diff --git a/plugins/versionFile/deps.ts b/plugins/versionFile/deps.ts new file mode 100644 index 0000000..fc9b695 --- /dev/null +++ b/plugins/versionFile/deps.ts @@ -0,0 +1,2 @@ +export { join } from "https://deno.land/std@0.119.0/path/mod.ts"; +export { EOL } from "https://deno.land/std@0.119.0/fs/mod.ts"; diff --git a/plugins/versionFile/mod.ts b/plugins/versionFile/mod.ts new file mode 100644 index 0000000..ff9d0c0 --- /dev/null +++ b/plugins/versionFile/mod.ts @@ -0,0 +1,36 @@ +import type { + Action, + ReleaseConfig, + ReleasePlugin, + Repo, +} from "../../plugin.ts"; +import { EOL, join } from "./deps.ts"; + +/** + * Export a version file with the new version number + */ +export const versionFile = { + name: "versionFile", + async preCommit( + repo: Repo, + _action: Action, + _from: string, + to: string, + config: ReleaseConfig, + ): Promise { + const versionFile = "version.json"; + const version = { + version: to, + }; + if (!config.dry) { + await Deno.writeTextFile( + join(repo.path, versionFile), + JSON.stringify(version, null, 2) + + // to comply with deno fmt + "\n", + ); + } else { + console.log(versionFile); + } + }, +}; diff --git a/src/branch.ts b/src/branch.ts new file mode 100644 index 0000000..4c023cc --- /dev/null +++ b/src/branch.ts @@ -0,0 +1,8 @@ +import { git } from "./git.ts"; +import { ReleaseError } from "./error.ts"; + +export async function fetchBranch(repo: string): Promise { + const [status, output, err] = await git(repo, "rev-parse --abbrev-ref HEAD"); + if (!status.success) throw new ReleaseError("GIT_EXE", err); + return output.trim(); +} diff --git a/src/changelog.ts b/src/changelog.ts new file mode 100644 index 0000000..e02fd99 --- /dev/null +++ b/src/changelog.ts @@ -0,0 +1,121 @@ +import type { Repo } from "./repo.ts"; +import type { Commit } from "./commits.ts"; +import type { Tag } from "./tags.ts"; + +export interface Filter { + type: string; + title: string; +} + +export interface Document { + sections: string[]; + links: string[]; +} + +export function fmtLink(name: string, to: string): string { + return `[${name}]: ${to}`; +} + +export function pushHeader(doc: Document): void { + doc.links.push( + fmtLink("keep a changelog", "https://keepachangelog.com/en/1.0.0/"), + ); + doc.links.push( + fmtLink("semantic versioning", "https://semver.org/spec/v2.0.0.html"), + ); + doc.sections.push(`# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog], and this project adheres to +[Semantic Versioning].`); +} + +export function pushChanges( + doc: Document, + repo: Repo, + title: string, + commits: Commit[], +): void { + doc.sections.push(`### ${title}`); + const list: string[] = []; + for (let commit of commits) { + const { hash } = commit; + const { subject } = commit.cc; + const shortid = `\`${hash.substr(0, 7)}\``; + + if (repo.remote && repo.remote.github) { + const { user, name } = repo.remote.github; + let url = `https://github.com/${user}/${name}/`; + url = `${url}commit/${hash}`; + + list.push(`- ${subject} ([${shortid}])`); + doc.links.push(fmtLink(shortid, url)); + } else { + list.push(`- ${subject} (${shortid})`); + } + } + doc.sections.push(list.join("\n")); +} + +export function pushTag( + doc: Document, + repo: Repo, + commits: Commit[], + filters: Filter[], + tag: Tag, + parent?: Tag, + title?: string, +): void { + let year = tag.date.getUTCFullYear(); + let month = String(tag.date.getUTCMonth() + 1).padStart(2, "0"); + let day = String(tag.date.getUTCDate()).padStart(2, "0"); + + if (repo.remote && repo.remote.github) { + const { user, name } = repo.remote.github; + let url = `https://github.com/${user}/${name}/`; + + url = parent + ? `${url}compare/${parent.version}...${tag.version}` + : `${url}compare/${tag.version}`; + doc.links.push(fmtLink(tag.version, url)); + } + + if (title) { + doc.sections.push(`## ${title}`); + } else { + doc.sections.push(`## [${tag.version}] - ${year}-${month}-${day}`); + } + + for (let filter of filters) { + const filtered = commits.filter((_) => _.cc.type === filter.type); + if (filtered.length > 0) { + pushChanges(doc, repo, filter.title, filtered); + } + } +} + +export function render(doc: Document): string { + const sections = doc.sections.join("\n\n"); + const links = doc.links.join("\n"); + const full = [sections, links]; + return `${full.join("\n\n").trim()}\n`; +} + +export function polyfillVersion(repo: Repo, to: string): [Tag[], Commit[]] { + const newtag: Tag = { + tag: to, + version: to, + date: new Date(), + hash: "", + }; + const tags = [newtag, ...repo.tags]; + const commits = [...repo.commits]; + + for (let commit of commits) { + if (commit.belongs !== null) break; + commit.belongs = newtag; + } + + return [tags, commits]; +} diff --git a/src/commits.ts b/src/commits.ts new file mode 100644 index 0000000..c5025e1 --- /dev/null +++ b/src/commits.ts @@ -0,0 +1,136 @@ +import { CCCommit, ccparse } from "../deps.ts"; + +import { git } from "./git.ts"; +import type { Tag } from "./tags.ts"; +import { ReleaseError } from "./error.ts"; + +export interface RawCommit { + hash: string; + title: string; + description: string; + author: string; + cc: CCCommit; +} + +export async function fetchRawCommits( + repo: string, + rev?: string, +): Promise { + const inner = Date.now(); + const outer = inner - 1; + + // How the output shoud look like + const spec = ["s", "n", "ae", "b"]; // add at + const format = `${inner} %${spec.join(`${inner}%`)}${outer}`; + + const [status, output, err] = await git(repo, [ + "rev-list", + `--pretty=format:${format}`, + "--header", + rev ?? "HEAD", + ]); + if (!status.success) throw new ReleaseError("GIT_EXE", err); + + let commits: RawCommit[] = []; + const parts = output + .split(String(outer)) + .map((item) => item.trim()) + .filter((item) => item.length) + .map((item) => { + const splitted = item.split(String(inner)); + const details = splitted.map((i) => i.trim()).filter((i) => i); + const hash = details[0].split(" ")[1]; + const title = details[1] || ""; + const description = details[3] || ""; + const author = details[2]; + + const body = `${title}\n${description}`; + const cc = ccparse(body); + + return { + hash, + title, + description, + author, + cc, + }; + }) + .filter((i) => i); + + commits = commits.concat(parts); + return commits; +} + +export interface Commit extends RawCommit { + belongs: Tag | null; +} + +export async function fetchCommits( + repo: string, + tags: Tag[], +): Promise { + let all: Commit[] = []; + + async function add(rev: string | undefined, belongs: Tag | null) { + let commits = await fetchRawCommits(repo, rev); + all = all.concat( + commits.map((_) => ({ + ..._, + belongs, + })), + ); + } + + if (tags.length === 0) { + await add(undefined, null); + return all; + } + + let child = tags[0]; + let parent = tags[0]; + + if (child) { + await add(`${child.hash}..HEAD`, null); + } + + for (let i = 0; i < tags.length - 1; i++) { + child = tags[i]; + parent = tags[i + 1]; + await add(`${parent.hash}..${child.hash}`, child); + } + + if (parent) { + await add(parent.hash, parent); + } + + return all; +} + +// export interface NewCommits { +// all: Commit[], +// latest: Commit | undefined +// } + +// export async function fetchNewCommits(repo: string, tags: Tag[]): Promise { +// const [release, parent] = tags; +// let loadAll = false; + +// if (!release || !parent || !parent.hash || !release.hash) { +// loadAll = true; +// } + +// const rev = loadAll ? undefined : `${parent.hash}..${release.hash}`; + +// // Load the commits using `git rev-list` +// const all = await fetchCommits(repo, rev); + +// // Find the latest commit, as it's the release reference +// const latest = all.find((commit) => commit.hash === release.hash); +// const latestIndex = all.indexOf(latest as Commit); + +// // Remove the latest commit from the collection +// all.splice(latestIndex, 1); + +// // Hand back the commits +// return { all, latest }; +// } diff --git a/src/dirs.ts b/src/dirs.ts new file mode 100644 index 0000000..5ab846c --- /dev/null +++ b/src/dirs.ts @@ -0,0 +1,9 @@ +export function home(): string | null { + switch (Deno.build.os) { + case "linux": + case "darwin": + return Deno.env.get("HOME") ?? null; + case "windows": + return Deno.env.get("USERPROFILE") ?? null; + } +} diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 0000000..3464b96 --- /dev/null +++ b/src/error.ts @@ -0,0 +1,16 @@ +const causes = { + GIT_EXE: "non-zero status returned by a git command", + NO_REPO: "not a valid git repository", + UNINITIALIZED_REPO: "repo is not initialized", + PLUGIN: "plugin error", +}; + +export class ReleaseError extends Error { + code: string; + internal_cause: string; + constructor(code: keyof typeof causes, message?: string) { + super(message ?? causes[code]); + this.code = code; + this.internal_cause = causes[code]; + } +} diff --git a/src/git.ts b/src/git.ts new file mode 100644 index 0000000..0cc7838 --- /dev/null +++ b/src/git.ts @@ -0,0 +1,80 @@ +import { ini, join } from "../deps.ts"; + +import { ReleaseError } from "./error.ts"; + +const decoder = new TextDecoder(); + +export async function git( + repo: string, + args: string[] | string, +): Promise<[Deno.ProcessStatus, string, string]> { + const dir = `--git-dir=${join(repo, ".git")}`; + if (typeof args === "string") args = args.split(" "); + const process = Deno.run({ + cwd: repo, + cmd: ["git", dir, ...args], + stdout: "piped", + stderr: "piped", + }); + let output = await process.output(); + let err = await process.stderrOutput(); + let status = await process.status(); + process.close(); + return [status, decoder.decode(output), decoder.decode(err)]; +} + +export async function ezgit( + repo: string, + args: string[] | string, +): Promise { + const [status, _, err] = await git(repo, args); + if (!status.success) throw new ReleaseError("GIT_EXE", err); +} + +export async function fetchConfig(repo: string): Promise { + const path = join(repo, ".git", "config"); + let source = await Deno.readTextFile(path); + source = source.replace(/\[(\S+) "(.*)"\]/g, (m, $1, $2) => { + return $1 && $2 ? `[${$1} "${$2.split(".").join("\\.")}"]` : m; + }); + let config = ini.decode(source); + for (let key of Object.keys(config)) { + let m = /(\S+) "(.*)"/.exec(key); + if (!m) continue; + let prop = m[1]; + config[prop] = config[prop] || {}; + config[prop][m[2]] = config[key]; + delete config[key]; + } + return config; +} + +interface GitConfigCore { + [key: string]: unknown; +} + +export interface Remote { + url: string; + fetch: string; + [key: string]: unknown; +} + +interface GitConfigRemote { + [key: string]: Remote; +} + +export interface Branch { + remote: string; + merge: string; + [key: string]: unknown; +} + +interface GitConfigBranch { + [key: string]: Branch; +} + +export interface GitConfig { + core: GitConfigCore; + remote: GitConfigRemote; + branch: GitConfigBranch; +} diff --git a/src/remote.ts b/src/remote.ts new file mode 100644 index 0000000..85c275a --- /dev/null +++ b/src/remote.ts @@ -0,0 +1,8 @@ +import { git } from "./git.ts"; +import { ReleaseError } from "./error.ts"; + +export async function fetchRemote(repo: string): Promise { + const [status, output, err] = await git(repo, "rev-parse --abbrev-ref HEAD"); + if (!status.success) throw new ReleaseError("GIT_EXE", err); + return output; +} diff --git a/src/repo.ts b/src/repo.ts new file mode 100644 index 0000000..f3cb12d --- /dev/null +++ b/src/repo.ts @@ -0,0 +1,79 @@ +import { exists, join } from "../deps.ts"; +import { ReleaseError } from "./error.ts"; + +import { fetchBranch } from "./branch.ts"; +import { Commit, fetchCommits } from "./commits.ts"; +import { fetchTags, Tag } from "./tags.ts"; +import { fetchStatus, Status } from "./status.ts"; +import { fetchConfig, GitConfig } from "./git.ts"; + +export interface Github { + user: string; + name: string; +} + +export interface Remote { + raw: string; + github: Github | null; +} + +export interface Repo { + path: string; + branch: string; + remote: Remote | null; + tags: Tag[]; + commits: Commit[]; + status: Status; + config: GitConfig; +} + +export async function fetchRepo(path: string): Promise { + const repo = join(path, ".git"); + if (!(await exists(repo))) { + throw new ReleaseError("NO_REPO"); + } + + const branch = await fetchBranch(path); + if (branch === "HEAD") throw new ReleaseError("UNINITIALIZED_REPO"); + + const config = await fetchConfig(path); + + let remote: Remote | null = null; + if (config.branch && config.branch[branch]) { + const branchRef = config.branch[branch]; + const remoteRef = config.remote[branchRef.remote]; + remote = { + raw: remoteRef.url, + github: null, + }; + const reGithub = + /(?:(?:https?:\/\/github\.com\/)|git@github\.com:)(.*)\/(.*)/; + if (reGithub.test(remote.raw)) { + const match = reGithub.exec(remote.raw); + if (match) { + remote.github = { + user: match[1], + name: match[2], + }; + if (remote.github.name.endsWith(".git")) { + remote.github.name = remote.github.name.replace(".git", ""); + } + } + } + } + + const tags = await fetchTags(path); + const commits = await fetchCommits(path, tags); + + const status = await fetchStatus(path); + + return { + path, + branch, + remote, + commits, + tags, + status, + config, + }; +} diff --git a/src/status.ts b/src/status.ts new file mode 100644 index 0000000..17860ae --- /dev/null +++ b/src/status.ts @@ -0,0 +1,101 @@ +import { git } from "./git.ts"; +import { ReleaseError } from "./error.ts"; + +export interface Status { + untracked: string[]; + index: Changes; + tree: Changes; + raw: Raw[]; +} + +type Path = string; + +interface Raw { + path: Path; + x: string; + y: string; + to?: Path; +} + +interface Rename { + path: Path; + to: Path; +} + +interface Changes { + modified: Path[]; + added: Path[]; + deleted: Path[]; + copied: Path[]; + updated: Path[]; + renamed: Rename[]; +} + +export async function fetchStatus(repo: string): Promise { + const [status, output, err] = await git(repo, "status --porcelain"); + if (!status.success) throw new ReleaseError("GIT_EXE", err); + + const S: Status = { + raw: [], + untracked: [], + index: { + modified: [], + added: [], + deleted: [], + copied: [], + updated: [], + renamed: [], + }, + tree: { + modified: [], + added: [], + deleted: [], + copied: [], + updated: [], + renamed: [], + }, + }; + + function box(code: string, changes: Changes, path: string, to?: string) { + switch (code) { + case "M": + return changes.modified.push(path); + case "A": + return changes.added.push(path); + case "D": + return changes.deleted.push(path); + case "C": + return changes.copied.push(path); + case "U": + return changes.updated.push(path); + case "R": + return changes.renamed.push({ path, to: to! }); + } + } + + const entries = output.split("\n"); + for (const entry of entries) { + if (!entry) continue; + const x = entry.charAt(0); + const y = entry.charAt(1); + + let path: string; + let to: string | undefined = undefined; + if (entry.includes(" -> ")) { + const sep = entry.indexOf(" -> ", 3); + path = entry.substr(3, sep - 3); + to = entry.substr(sep + 4); + S.raw.push({ path, x, y, to }); + } else { + path = entry.substr(3); + S.raw.push({ path, x, y }); + } + box(x, S.index, path, to); + box(y, S.tree, path, to); + if (x === "?" && y === "?") { + S.untracked.push(path); + } + } + + return S; +} diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..6447268 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,37 @@ +import { ensureFile, exists, join } from "../deps.ts"; +import { home } from "./dirs.ts"; + +const STORE_PATH = [".config", "release"]; + +export const known = { + github: "GITHUB_TOKEN", +}; + +export async function get(name: string): Promise { + const ci = Deno.env.get(name); + if (ci) return ci; + + const oshome = home(); + if (!oshome) return null; + const path = join(oshome, ...STORE_PATH); + if (!(await exists(path))) return null; + const source = await Deno.readTextFile(path); + const constants = JSON.parse(source); + return constants[name]; +} + +export async function set(name: string, value: string) { + const oshome = home(); + if (!oshome) return; + const path = join(oshome, ...STORE_PATH); + let patch: string; + if (await exists(path)) { + const source = await Deno.readTextFile(path); + const constants = JSON.parse(source); + patch = JSON.stringify({ ...constants, [name]: value }); + } else { + await ensureFile(path); + patch = JSON.stringify({ [name]: value }); + } + return Deno.writeTextFile(path, patch); +} diff --git a/src/tags.ts b/src/tags.ts new file mode 100644 index 0000000..46f3c4f --- /dev/null +++ b/src/tags.ts @@ -0,0 +1,79 @@ +import { semver } from "../deps.ts"; + +import { git } from "./git.ts"; +import { ReleaseError } from "./error.ts"; + +const reTag = /tag:\s*([^,)]+)/g; +const reCommitDetails = /^(.+);(.+);(.+)$/; + +export interface Tag { + tag: string; + version: string; + hash: string; + date: Date; +} + +function filterByRange(tags: Tag[], range?: string) { + if (!range) { + return tags; + } + + return tags.filter((tag) => semver.satisfies(tag.version, range)); +} + +function extractCommit(refs: string): Omit[] { + const tagNames = []; + let match: RegExpExecArray | null; + + // Finding successive matches + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#Finding_successive_matches + while ((match = reTag.exec(refs)) !== null) { + tagNames.push(match[1]); + } + + return tagNames + .map((name) => ({ + tag: name, + version: semver.valid(name), + })) + .filter((tag) => tag.version != null) as Omit[]; +} + +export function parseLine(line: string): Tag[] { + const match = reCommitDetails.exec(line); + + if (!match || match.length < 4) { + return []; + } + + const tags = extractCommit(match[1]); + const hash = match[2].trim(); + const date = new Date(match[3].trim()); + + return tags.map((tag) => Object.assign(tag, { hash, date })); +} + +interface FetchOptions { + range: string; + rev?: string; +} + +export async function fetchTags(repo: string, options?: FetchOptions | string) { + if (typeof options === "string") options = { range: options }; + const range = options && options.range; + const rev = options && options.rev; + const fmt = '--pretty="%d;%H;%ci" --decorate=short'; + const cmd = rev + ? `log --simplify-by-decoration ${fmt} ${rev}` + : `log --no-walk --tags ${fmt}`; + + const [status, output, err] = await git(repo, cmd); + if (!status.success) throw new ReleaseError("GIT_EXE", err); + + const lines = output.split("\n"); + const tags = lines.map(parseLine).flat(); + + return filterByRange(tags, range).sort((a, b) => { + return semver.rcompare(a.version, b.version); + }); +} diff --git a/zen.ts b/zen.ts new file mode 100644 index 0000000..e010597 --- /dev/null +++ b/zen.ts @@ -0,0 +1,20 @@ +const quotes = [ + "It's not fully shipped until it's fast.", + "Practicality beats purity.", + "Avoid administrative distraction.", + "Mind your words, they are important.", + "Non-blocking is better than blocking.", + "Design for failure.", + "Half measures are as bad as nothing at all.", + "Favor focus over features.", + "Approachable is better than simple.", + "Encourage flow.", + "Anything added dilutes everything else.", + "Speak like a human.", + "Responsive is better than fast.", + "Keep it logically awesome.", +]; + +export function zen(): string { + return quotes[Math.floor(Math.random() * quotes.length)]; +}