From 200a7484344f8546ee3695629fc855beffc22ea7 Mon Sep 17 00:00:00 2001 From: Charlie Ozinga Date: Mon, 21 Sep 2020 18:19:58 -0600 Subject: [PATCH] feat: hooks and labels --- .github/snippets/check-cargo.yml | 9 -- .github/snippets/check-versio.yml | 10 -- .github/snippets/get-cargo-minimal.yml | 13 +++ .github/snippets/get-cargo.yml | 2 +- .github/snippets/job-cargo-checks.yml | 14 ++- .github/snippets/job-cratesio-publish.yml | 24 +++++ .github/snippets/job-project-matrixes.yml | 10 +- .github/snippets/job-versio-checks.yml | 15 ++- .github/snippets/job-versio-release.yml | 17 ++++ .github/snippets/matrix.yml | 10 -- .github/snippets/not-skip-ci.yml | 2 + .github/workflows-src/release.yml | 40 +------- .github/workflows/pr.yml | 9 +- .github/workflows/release.yml | 26 +++-- .gitignore | 3 + .versio.yaml | 5 +- docs/reference.md | 37 ++++++- docs/use_cases.md | 94 +++++++++++------ docs/vcs_levels.md | 14 +++ src/cli.rs | 47 ++++++++- src/commands.rs | 64 ++++++++++-- src/config.rs | 119 ++++++++++++++++++++-- src/init.rs | 85 +++++++++++++--- src/mark.rs | 10 +- src/mono.rs | 16 +-- src/output.rs | 21 ++++ src/scan/parts.rs | 10 ++ src/state.rs | 118 ++++++++++++++++----- 28 files changed, 651 insertions(+), 193 deletions(-) delete mode 100644 .github/snippets/check-cargo.yml delete mode 100644 .github/snippets/check-versio.yml create mode 100644 .github/snippets/get-cargo-minimal.yml create mode 100644 .github/snippets/job-cratesio-publish.yml create mode 100644 .github/snippets/job-versio-release.yml delete mode 100644 .github/snippets/matrix.yml create mode 100644 .github/snippets/not-skip-ci.yml diff --git a/.github/snippets/check-cargo.yml b/.github/snippets/check-cargo.yml deleted file mode 100644 index 5a084d7..0000000 --- a/.github/snippets/check-cargo.yml +++ /dev/null @@ -1,9 +0,0 @@ -key: 'check-cargo' -value: - - SNIPPET_get-cargo - - name: Check structure - run: cargo clippy - - name: Check format - run: cargo +nightly fmt -- --check - - name: Check tests - run: cargo test diff --git a/.github/snippets/check-versio.yml b/.github/snippets/check-versio.yml deleted file mode 100644 index 4dbde8c..0000000 --- a/.github/snippets/check-versio.yml +++ /dev/null @@ -1,10 +0,0 @@ -key: check-versio -value: - - name: Get versio - uses: chaaz/versio-actions/install@v1 - - name: Fetch history - run: git fetch --unshallow - - name: Check projects - run: versio check - - name: Output plan - run: versio plan diff --git a/.github/snippets/get-cargo-minimal.yml b/.github/snippets/get-cargo-minimal.yml new file mode 100644 index 0000000..d74e881 --- /dev/null +++ b/.github/snippets/get-cargo-minimal.yml @@ -0,0 +1,13 @@ +key: get-cargo-minimal +value: + - name: Get cargo stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: Cache cargo + uses: actions/cache@v1 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} diff --git a/.github/snippets/get-cargo.yml b/.github/snippets/get-cargo.yml index 7648924..4ca7774 100644 --- a/.github/snippets/get-cargo.yml +++ b/.github/snippets/get-cargo.yml @@ -12,7 +12,7 @@ value: components: rustfmt - name: Find paths id: cargo-find-paths - run: echo '::set-output name=cargo-lock-glob::${{ matrix.root }}/**/Cargo.lock' + run: 'echo ::set-output name=cargo-lock-glob::"${{ matrix.root }}"/**/Cargo.lock' - name: Cache cargo and target uses: actions/cache@v1 with: diff --git a/.github/snippets/job-cargo-checks.yml b/.github/snippets/job-cargo-checks.yml index d9f7f09..44070c7 100644 --- a/.github/snippets/job-cargo-checks.yml +++ b/.github/snippets/job-cargo-checks.yml @@ -4,11 +4,17 @@ value: runs-on: ubuntu-latest strategy: matrix: ${{ fromJson(needs.project-matrixes.outputs.cargo-matrix) }} - if: "!contains(github.event.head_commit.message, 'skip ci')" + if: SNIPPET_not-skip-ci defaults: run: working-directory: ${{ matrix.root }} steps: - - name: Checkout code - uses: actions/checkout@v2 - - SNIPPET_check-cargo + - name: Checkout code + uses: actions/checkout@v2 + - SNIPPET_get-cargo + - name: Check structure + run: cargo clippy + - name: Check format + run: cargo +nightly fmt -- --check + - name: Check tests + run: cargo test diff --git a/.github/snippets/job-cratesio-publish.yml b/.github/snippets/job-cratesio-publish.yml new file mode 100644 index 0000000..868866f --- /dev/null +++ b/.github/snippets/job-cratesio-publish.yml @@ -0,0 +1,24 @@ +key: job-cratesio-publish +value: + needs: + - project-matrixes + - versio-release + runs-on: ubuntu-latest + strategy: + matrix: ${{fromJson(needs.project-matrixes.outputs.cargo-matrix)}} + if: SNIPPET_not-skip-ci + defaults: + run: + working-directory: ${{ matrix.root }} + steps: + - name: Checkout release + uses: actions/checkout@v2 + with: + ref: main + - SNIPPET_get-cargo + - name: Login to crates.io + run: cargo login ${CRATES_IO_TOKEN} + env: + CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} + - name: Publish to crates.io + run: cargo publish diff --git a/.github/snippets/job-project-matrixes.yml b/.github/snippets/job-project-matrixes.yml index 0748957..5b8d94b 100644 --- a/.github/snippets/job-project-matrixes.yml +++ b/.github/snippets/job-project-matrixes.yml @@ -1,10 +1,18 @@ key: job-project-matrixes value: runs-on: ubuntu-latest + if: SNIPPET_not-skip-ci outputs: cargo-matrix: ${{ steps.find-cargo-matrix.outputs.matrix }} all-matrix: ${{ steps.find-all-matrix.outputs.matrix }} steps: - name: Checkout code uses: actions/checkout@v2 - - SNIPPET_matrix + - name: Get versio + uses: chaaz/versio-actions/install@v1 + - name: Find cargo matrix + id: find-cargo-matrix + run: 'echo "::set-output name=matrix::{\"include\":$(versio -l none info -l cargo -R -N)}"' + - name: Find all matrix + id: find-all-matrix + run: 'echo "::set-output name=matrix::{\"include\":$(versio -l none info -a -R -N)}"' diff --git a/.github/snippets/job-versio-checks.yml b/.github/snippets/job-versio-checks.yml index 1d3616e..cf64467 100644 --- a/.github/snippets/job-versio-checks.yml +++ b/.github/snippets/job-versio-checks.yml @@ -1,8 +1,15 @@ key: job-versio-checks value: runs-on: ubuntu-latest - if: "!contains(github.event.head_commit.message, 'skip ci')" + if: SNIPPET_not-skip-ci steps: - - name: Checkout code - uses: actions/checkout@v2 - - SNIPPET_check-versio + - name: Checkout code + uses: actions/checkout@v2 + - name: Get versio + uses: chaaz/versio-actions/install@v1 + - name: Fetch history + run: git fetch --unshallow + - name: Check projects + run: versio check + - name: Output plan + run: versio plan diff --git a/.github/snippets/job-versio-release.yml b/.github/snippets/job-versio-release.yml new file mode 100644 index 0000000..0976bf6 --- /dev/null +++ b/.github/snippets/job-versio-release.yml @@ -0,0 +1,17 @@ +key: job-versio-release +value: + needs: + - cargo-checks + - versio-checks + runs-on: ubuntu-latest + if: SNIPPET_not-skip-ci + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Get versio + uses: chaaz/versio-actions/install@v1 + - SNIPPET_get-cargo-minimal + - name: Fetch history + run: git fetch --unshallow + - name: Generate release + run: versio release diff --git a/.github/snippets/matrix.yml b/.github/snippets/matrix.yml deleted file mode 100644 index 093f1c8..0000000 --- a/.github/snippets/matrix.yml +++ /dev/null @@ -1,10 +0,0 @@ -key: matrix -value: - - name: Get versio - uses: chaaz/versio-actions/install@v1 - - name: Find cargo matrix - id: find-cargo-matrix - run: 'echo "::set-output name=matrix::{\"include\": $(versio -l none info -i 0 -R -N)}"' - - name: Find all matrix - id: find-all-matrix - run: 'echo "::set-output name=matrix::{\"include\": $(versio -l none info -a -R -N)}"' diff --git a/.github/snippets/not-skip-ci.yml b/.github/snippets/not-skip-ci.yml new file mode 100644 index 0000000..5ae7975 --- /dev/null +++ b/.github/snippets/not-skip-ci.yml @@ -0,0 +1,2 @@ +key: not-skip-ci +value: "!contains(github.event.head_commit.message, 'skip ci')" diff --git a/.github/workflows-src/release.yml b/.github/workflows-src/release.yml index aeedeb1..b99f46e 100644 --- a/.github/workflows-src/release.yml +++ b/.github/workflows-src/release.yml @@ -10,41 +10,5 @@ jobs: project-matrixes: SNIPPET_job-project-matrixes versio-checks: SNIPPET_job-versio-checks cargo-checks: SNIPPET_job-cargo-checks - - versio-release: - needs: - - cargo-checks - - versio-checks - runs-on: ubuntu-latest - if: "!contains(github.event.head_commit.message, 'skip ci')" - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Get versio - uses: chaaz/versio-actions/install@v1 - - name: Fetch history - run: git fetch --unshallow - - name: Generate release - run: versio release - - cargo-post: - needs: - - project-matrixes - - versio-release - runs-on: ubuntu-latest - strategy: - matrix: ${{fromJson(needs.project-matrixes.outputs.cargo-matrix)}} - if: "!contains(github.event.head_commit.message, 'skip ci')" - defaults: - run: - working-directory: ${{ matrix.root }} - steps: - - name: Checkout code - uses: actions/checkout@v2 - - SNIPPET_get-cargo - - name: Login to crates.io - run: cargo login ${CRATES_IO_TOKEN} - env: - CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - - name: Publish to crates.io - run: cargo publish + versio-release: SNIPPET_job-versio-release + cratesio-publish: SNIPPET_job-cratesio-publish diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 01ee554..a48e630 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,6 +1,6 @@ --- # DO NOT EDIT -# Created from "/Users/charlie/Documents/Projects/gits/versio-new/.github/workflows-src/pr.yml". +# Created from template "pr.yml". name: pr "on": - workflow_dispatch @@ -11,6 +11,7 @@ env: jobs: project-matrixes: runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'skip ci')" outputs: cargo-matrix: "${{ steps.find-cargo-matrix.outputs.matrix }}" all-matrix: "${{ steps.find-all-matrix.outputs.matrix }}" @@ -21,10 +22,10 @@ jobs: uses: chaaz/versio-actions/install@v1 - name: Find cargo matrix id: find-cargo-matrix - run: "echo \"::set-output name=matrix::{\\\"include\\\": $(versio -l none info -i 0 -R -N)}\"" + run: "echo \"::set-output name=matrix::{\\\"include\\\":$(versio -l none info -l cargo -R -N)}\"" - name: Find all matrix id: find-all-matrix - run: "echo \"::set-output name=matrix::{\\\"include\\\": $(versio -l none info -a -R -N)}\"" + run: "echo \"::set-output name=matrix::{\\\"include\\\":$(versio -l none info -a -R -N)}\"" versio-checks: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, 'skip ci')" @@ -63,7 +64,7 @@ jobs: components: rustfmt - name: Find paths id: cargo-find-paths - run: "echo '::set-output name=cargo-lock-glob::${{ matrix.root }}/**/Cargo.lock'" + run: "echo ::set-output name=cargo-lock-glob::\"${{ matrix.root }}\"/**/Cargo.lock" - name: Cache cargo and target uses: actions/cache@v1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21e7d85..35fe9bc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ --- # DO NOT EDIT -# Created from "/Users/charlie/Documents/Projects/gits/versio-new/.github/workflows-src/release.yml". +# Created from template "release.yml". name: release "on": - workflow_dispatch @@ -11,6 +11,7 @@ env: jobs: project-matrixes: runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'skip ci')" outputs: cargo-matrix: "${{ steps.find-cargo-matrix.outputs.matrix }}" all-matrix: "${{ steps.find-all-matrix.outputs.matrix }}" @@ -21,10 +22,10 @@ jobs: uses: chaaz/versio-actions/install@v1 - name: Find cargo matrix id: find-cargo-matrix - run: "echo \"::set-output name=matrix::{\\\"include\\\": $(versio -l none info -i 0 -R -N)}\"" + run: "echo \"::set-output name=matrix::{\\\"include\\\":$(versio -l none info -l cargo -R -N)}\"" - name: Find all matrix id: find-all-matrix - run: "echo \"::set-output name=matrix::{\\\"include\\\": $(versio -l none info -a -R -N)}\"" + run: "echo \"::set-output name=matrix::{\\\"include\\\":$(versio -l none info -a -R -N)}\"" versio-checks: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, 'skip ci')" @@ -63,7 +64,7 @@ jobs: components: rustfmt - name: Find paths id: cargo-find-paths - run: "echo '::set-output name=cargo-lock-glob::${{ matrix.root }}/**/Cargo.lock'" + run: "echo ::set-output name=cargo-lock-glob::\"${{ matrix.root }}\"/**/Cargo.lock" - name: Cache cargo and target uses: actions/cache@v1 with: @@ -86,11 +87,20 @@ jobs: uses: actions/checkout@v2 - name: Get versio uses: chaaz/versio-actions/install@v1 + - name: Get cargo stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: Cache cargo + uses: actions/cache@v1 + with: + path: "~/.cargo/registry\n~/.cargo/git\n" + key: "${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}" - name: Fetch history run: git fetch --unshallow - name: Generate release run: versio release - cargo-post: + cratesio-publish: needs: - project-matrixes - versio-release @@ -102,8 +112,10 @@ jobs: run: working-directory: "${{ matrix.root }}" steps: - - name: Checkout code + - name: Checkout release uses: actions/checkout@v2 + with: + ref: main - name: Get cargo stable uses: actions-rs/toolchain@v1 with: @@ -116,7 +128,7 @@ jobs: components: rustfmt - name: Find paths id: cargo-find-paths - run: "echo '::set-output name=cargo-lock-glob::${{ matrix.root }}/**/Cargo.lock'" + run: "echo ::set-output name=cargo-lock-glob::\"${{ matrix.root }}\"/**/Cargo.lock" - name: Cache cargo and target uses: actions/cache@v1 with: diff --git a/.gitignore b/.gitignore index faa3774..04679b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /target TODO* +/.versio-paused +/.versio-paused +/.versio-paused diff --git a/.versio.yaml b/.versio.yaml index 5d396fc..0038ff3 100644 --- a/.versio.yaml +++ b/.versio.yaml @@ -5,10 +5,13 @@ projects: - name: "versio" id: 0 tag_prefix: "" - changelog: 'CHANGELOG.html' + labels: cargo + changelog: "CHANGELOG.html" version: file: "Cargo.toml" toml: "package.version" + hooks: + post_write: cargo fetch sizes: use_angular: true diff --git a/docs/reference.md b/docs/reference.md index 403d2fc..dc3d003 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -25,6 +25,9 @@ behavior), you can always force Versio to stick to the local repository with `-l local`; or to not use the GitHub API with `-l remote`. See [VCS Levels](./vcs_levels.md) for more information. +See the `CI` sections in [Use Cases](./use_cases.md#ci-authorization) +for info on how to set up authorization for common CI/CD systems. + ### Git remotes [Git remotes]: #git-remotes @@ -44,7 +47,7 @@ expect Versio to keep your remote in sync. Make sure that commands like e.g. `git fetch` work from the command-line if versio is having trouble. If you don't have an agent set up, or if your agent is unable to -generate credentials, you can set the environment variables +negotiate credentials, you can set the environment variables `GITHUB_USER` and `GITHUB_TOKEN` to use more traditional user/password authorization. Note that the `GITHUB_TOKEN` can be the github password for this user, or (suggested) an access token generated for this user @@ -60,7 +63,8 @@ a new personal access token for this purpose via the GitHub web UI in your user's Settings -> Developer settings. Once you have the new token, you can set the environment variable -`GITHUB_TOKEN`, or you can add it to your user preferences in +`GITHUB_TOKEN` (this can be the same `GITHUB_TOKEN` used for `git` +authorization as well), or you can add it to your user preferences in `~/.versio/prefs.toml`. Here's an example of such a file: ``` @@ -136,17 +140,40 @@ along with their options and flags. You can always use `versio help` or - `plan`: View the update plan. - `release`: Apply the update plan: update version numbers, create/update changelogs, and commit/tag/push all changes. - - `--dry-run` (`-d`): Don't actually commit, push, tag, or change any - files, but otherwise run as if you would. - `--show-all` (`-a`): Show the run results for all projects, even those that weren't updated. + - `--pause` (`-p `): Pause the release process before a stage + of operation. Currently, only the `commit` stage is supported, which + means that Versio will exit after it writes any local files, but + before it commits, tags, or pushes to the remote repository. You can + use this feature to perform additional changes before committing + your version update. This will create a `.versio-paused` file at the + top level of your local repository that stores the planned resume + action: while this file exists, only the `release --resume` or + `release --abort` commands can be used. + - `--resume` will perform the planned commits, tags, pushes, etc. + which were paused from a previous `release --pause`. Any local file + changes made after the `release --pause` will also be committed. You + may supply a different VCS Level to this command than the original + `release --pause` command. + - `--abort` will simply delete the `.versio-paused` file from a + previous `release --pause`, discarding any planned commits, tags, + pushes. This command will *not* rollback any local changes made as + part of the previous `release --pause`; if needed, you should do + that yourself with e.g. `git checkout -- .`. You can't use both + `--resume` and `--abort`. + - `--dry-run` (`-d`): Don't actually commit, push, tag, or change any + files, but otherwise run as if you would. `dry-run` is incompatible + with `--pause`, `--resume`, and `--abort`. - `init`: - `--max-depth` (`-d `): The maximum directory depth that Versio will search for projects. Defaults to `5`. Run this command at the base directory of an uninitialized repository. It will search the repository for projects, and create a new - `.versio.yaml` config based on what it finds. + `.versio.yaml` config based on what it finds. It will also append + `/.versio-paused` to your `.gitignore` file, as a safety measure while + using the `release --pause` command. ## Common project types [Common project types]: #common-project-types diff --git a/docs/use_cases.md b/docs/use_cases.md index f170ec3..5a3208e 100644 --- a/docs/use_cases.md +++ b/docs/use_cases.md @@ -153,6 +153,29 @@ file with each of those projects listed. If you change later add, remove, or change the location of your projects, you should edit this file by hand to keep it up-to-date. +## CI/CD + +### GitHub Action Matrixes + +If you are using a monorepo, you may want to perform the same build step +on all your (for example) Node.js projects. You can build GitHub dynamic +matrixes using the `versio info` command, and then use those matrixes to +execute multiple projects in your repo. + +### Authorization + +In most CI/CD environments, you may not have a credentials agent +available to negotiate credentials to your git repo and/or github APIs. +Instead, your should set the `GITHUB_USER` and `GITHUB_TOKEN` +environment variables. For example, GitHub Actions provides these values +to you in various places: + +``` +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_USER: ${{ github.actor }} +``` + ## CI Pre-merge You can use Versio to check that a branch is ready to be merged to your @@ -170,9 +193,9 @@ project(s) version numbers. Note the use of `checkout@v2`, and the following `git fetch --unshallow` command, which is necessary to fill in the git history before `versio` is asked to analyze it. Also, we've provided a -`versio-actions-install@v1` command which installs the `versio` command -into the job. (Currently, the `versio-actions-installer` action only -works for linux-based runners.) +`versio-actions/install@v1` command which installs the `versio` command +into the job. (Currently, the `versio-actions/install` action only works +for linux-based runners.) ``` --- @@ -181,21 +204,22 @@ on: - pull_request env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_USER: ${{ github.actor }} jobs: versio-checks: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Get versio - uses: chaaz/versio-actions-install@v1 - - name: Fetch history - run: git fetch --unshallow - - name: Check projects - run: versio check - - name: Print changes - run: versio plan + - name: Checkout code + uses: actions/checkout@v2 + - name: Get versio + uses: chaaz/versio-actions/install@v1 + - name: Fetch history + run: git fetch --unshallow + - name: Check projects + run: versio check + - name: Print changes + run: versio plan ``` ## CI Release @@ -207,6 +231,16 @@ push all changes. You can set this action to run automatically when a branch has been merged to a release branch, or at any other time you want your software to be released. +It may be beneficial to generate some artifacts in your release +workflow: this serves to capture the binaries, documentation, etc. +associated with a particular version number. Typical targets for +releases include GitHub Releases and library repositories (such as +NPM.org, crates.io, Dockerhub, etc.). While Versio itself can't generate +these artifacts, most language and build systems have easy-to-use tools +that can. + +### About Timing + It's important to note that nothing can be pushed to the release branch during the short time that Versio is running, or `versio release` will fail. There are a number of ways you can deal with this: from locking @@ -216,22 +250,22 @@ and manually re-running the CI action if it gets stuck; and more. The strategy you use is dependent on the specifics of your organization and CI/CD process. -It may be beneficial to generate some artifacts in your release -workflow: this serves to capture the binaries, documentation, etc. -associated with a particular version number. Typical targets for -releases include GitHub Releases and library repositories (such as -NPM.org, crates.io, Dockerhub, etc.), as well as whatever custom -artifact storage you use in your org. In addition to being touchstones -for particular releases, you can use these artifacts in your CD -pipeline. While Versio itself (currently) can't generate or save these -artifacts, most language and build systems have easy-to-use tools that -can. - - +``` +jobs: + versio-release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Get versio + uses: chaaz/versio-actions/install@v1 + - name: Fetch history + run: git fetch --unshallow + - name: Generate release + run: versio release +``` diff --git a/docs/vcs_levels.md b/docs/vcs_levels.md index 173182a..f40963e 100644 --- a/docs/vcs_levels.md +++ b/docs/vcs_levels.md @@ -35,6 +35,20 @@ without `--dry-run` will not read any data from the remote, nor will it write to the remote: however, it may still write to the filesystem, and commit and tag any changes to the local repository. +### Vs Pause + +The VCS Level is distinct from the idea of a "pause". The "pause" flag +exits a command just before executing a stage (e.g. "commit"), but +otherwise doesn't affect operations or have any effect on the VCS level. +In fact, you can supply a different VCS level to commands with the +`--pause` and `--resume` commands, and that level will apply for the +portion of the operation it applies to. + +You could, for example, run `versio -l smart release --pause commit` to +gather information and write a new versions and changelogs based on pull +request information from the remote GitHub API, but then run `versio -l +local release --resume` to commit and tag only the local repo. + ## Calculation Every Versio command except for `versio init` calculates the final VCS diff --git a/src/cli.rs b/src/cli.rs index 27352cb..db5ea35 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -191,11 +191,31 @@ pub fn execute(info: &EarlyInfo) -> Result<()> { .display_order(1) .help("Also show unchnaged versions") ) + .arg( + Arg::with_name("pause") + .short("p") + .long("pause") + .takes_value(true) + .value_name("stage") + .possible_values(&["commit"]) + .display_order(1) + .help("Pause the release") + ) + .arg(Arg::with_name("resume").long("resume").takes_value(false).display_order(1).help("Resume after pausing")) + .arg( + Arg::with_name("abort") + .long("abort") + .takes_value(false) + .conflicts_with("resume") + .display_order(1) + .help("Abort after pausing") + ) .arg( Arg::with_name("dry") .short("d") .long("dry-run") .takes_value(false) + .conflicts_with_all(&["pause", "resume", "abort"]) .display_order(1) .help("Don't write new versions") ) @@ -250,10 +270,22 @@ pub fn execute(info: &EarlyInfo) -> Result<()> { .display_order(1) .help("Info on project name") ) + .arg( + Arg::with_name("label") + .short("l") + .long("label") + .takes_value(true) + .value_name("label") + .multiple(true) + .number_of_values(1) + .min_values(1) + .display_order(1) + .help("Info on projects with a label") + ) .arg( Arg::with_name("all").short("a").long("all").takes_value(false).display_order(1).help("Info on all projects") ) - .group(ArgGroup::with_name("which").args(&["id", "name", "all"]).required(false)) + .group(ArgGroup::with_name("which").args(&["id", "name", "label", "all"]).required(false)) .arg( Arg::with_name("showroot") .short("R") @@ -278,6 +310,12 @@ pub fn execute(info: &EarlyInfo) -> Result<()> { } fn parse_matches(m: ArgMatches) -> Result<()> { + match m.subcommand() { + ("release", Some(m)) if m.is_present("abort") => (), + ("release", Some(m)) if m.is_present("resume") => (), + _ => sanity_check()? + } + let pref_vcs = parse_vcs(&m)?; match m.subcommand() { @@ -296,13 +334,16 @@ fn parse_matches(m: ArgMatches) -> Result<()> { ("files", Some(_)) => files(pref_vcs)?, ("changes", Some(_)) => changes(pref_vcs)?, ("plan", Some(_)) => plan(pref_vcs)?, - ("release", Some(m)) => release(pref_vcs, m.is_present("all"), m.is_present("dry"))?, + ("release", Some(m)) if m.is_present("abort") => abort()?, + ("release", Some(m)) if m.is_present("resume") => resume(pref_vcs)?, + ("release", Some(m)) => release(pref_vcs, m.is_present("all"), m.is_present("dry"), m.is_present("pause"))?, ("init", Some(m)) => init(m.value_of("maxdepth").map(|d| d.parse().unwrap()).unwrap_or(5))?, ("info", Some(m)) => { let names = m.values_of("name").map(|v| v.collect::>()); + let labels = m.values_of("label").map(|v| v.collect::>()); let ids = m.values_of("id").map(|v| v.map(|i| i.parse()).collect::, _>>()).transpose()?; - info(pref_vcs, ids, names, m.is_present("all"), m.is_present("showname"), m.is_present("showroot"))? + info(pref_vcs, ids, names, labels, m.is_present("all"), m.is_present("showname"), m.is_present("showroot"))? } ("", _) => empty_cmd()?, (c, _) => unknown_cmd(c)? diff --git a/src/commands.rs b/src/commands.rs index a1e024e..f3561a8 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -5,9 +5,11 @@ use crate::errors::{Result, ResultExt}; use crate::git::Repo; use crate::mono::Mono; use crate::output::{Output, ProjLine}; -use crate::state::StateRead; +use crate::state::{CommitState, StateRead}; use crate::vcs::{VcsLevel, VcsRange}; use error_chain::bail; +use std::fs::{remove_file, File}; +use std::io::BufReader; use std::path::{Path, PathBuf}; pub fn early_info() -> Result { @@ -109,7 +111,7 @@ pub fn set(pref_vcs: Option, id: Option<&str>, name: Option<&str>, val mono.set_by_only(value)?; } - mono.commit(false) + mono.commit(false, false) } pub fn diff(pref_vcs: Option) -> Result<()> { @@ -151,8 +153,8 @@ pub fn plan(pref_vcs: Option) -> Result<()> { } pub fn info( - pref_vcs: Option, ids: Option>, names: Option>, all: bool, show_name: bool, - show_root: bool + pref_vcs: Option, ids: Option>, names: Option>, labels: Option>, + all: bool, show_name: bool, show_root: bool ) -> Result<()> { let mono = build(pref_vcs, VcsLevel::None, VcsLevel::Smart, VcsLevel::None, VcsLevel::Smart)?; let output = Output::new(); @@ -181,13 +183,20 @@ pub fn info( .into_iter() .map(|p| ProjLine::from(p, reader)) )?; + } else if let Some(labels) = labels { + output.write_projects( + labels + .iter() + .flat_map(|l| cfg.find_labelled(l).into_iter().map(|id| cfg.get_project(id).unwrap())) + .map(|p| ProjLine::from(p, reader)) + )?; } output.commit()?; Ok(()) } -pub fn release(pref_vcs: Option, all: bool, dry: bool) -> Result<()> { +pub fn release(pref_vcs: Option, all: bool, dry: bool, pause: bool) -> Result<()> { let mut mono = build(pref_vcs, VcsLevel::None, VcsLevel::Smart, VcsLevel::Local, VcsLevel::Smart)?; let output = Output::new(); let mut output = output.release(); @@ -238,17 +247,58 @@ pub fn release(pref_vcs: Option, all: bool, dry: bool) -> Result<()> { } if !dry { - mono.commit(true)?; - output.write_commit()?; + mono.commit(true, pause)?; + if pause { + output.write_pause()?; + } else { + output.write_commit()?; + output.write_done()?; + } } else { output.write_dry()?; } + output.commit()?; + Ok(()) +} + +pub fn resume(user_pref_vcs: Option) -> Result<()> { + let vcs = combine_vcs(user_pref_vcs, VcsLevel::None, VcsLevel::Smart, VcsLevel::Local, VcsLevel::Smart)?; + let repo = Repo::open(".", vcs.max())?; + let output = Output::new(); + let mut output = output.resume(); + + let mut commit: CommitState = { + let file = File::open(".versio-paused")?; + let reader = BufReader::new(file); + let commit: CommitState = serde_json::from_reader(reader)?; + + // We must remove the pausefile before resuming, or else it will be committed. + remove_file(".versio-paused")?; + commit + }; + commit.resume(&repo)?; + output.write_done()?; output.commit()?; + Ok(()) } +pub fn abort() -> Result<()> { + remove_file(".versio-paused")?; + println!("Release aborted. You may need to rollback your VCS \n(i.e `git checkout -- .`)"); + Ok(()) +} + +pub fn sanity_check() -> Result<()> { + if Path::new(".versio-paused").exists() { + bail!("versio is paused: use `release --resume` or `--abort`.") + } else { + Ok(()) + } +} + fn build( user_pref_vcs: Option, my_pref_lo: VcsLevel, my_pref_hi: VcsLevel, my_reqd_lo: VcsLevel, my_reqd_hi: VcsLevel diff --git a/src/config.rs b/src/config.rs index 683c2e6..b7fcb07 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,7 +16,7 @@ use glob::{glob_with, MatchOptions, Pattern}; use liquid::ParserBuilder; use log::trace; use regex::{escape, Regex}; -use serde::de::{self, DeserializeSeed, Deserializer, MapAccess, Unexpected, Visitor}; +use serde::de::{self, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Unexpected, Visitor}; use serde::ser::Serializer; use serde::{Deserialize, Serialize}; use std::borrow::Cow; @@ -61,10 +61,7 @@ impl fmt::Display for ProjectId { } impl Serialize for ProjectId { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer - { + fn serialize(&self, serializer: S) -> std::result::Result { if self.majors.is_empty() { serializer.serialize_u32(self.id) } else { @@ -135,6 +132,8 @@ impl Config { } pub fn old_tags(&self) -> &OldTags { self.state.old_tags() } + + pub fn hooks(&self) -> HashMap, &HookSet)> { self.file.hooks() } } impl Config { @@ -160,6 +159,10 @@ impl Config { Ok(id) } + pub fn find_labelled(&self, label: &str) -> Vec<&ProjectId> { + self.file.projects.iter().filter(|p| p.has_label(label)).map(|p| p.id()).collect() + } + pub fn annotate(&self) -> Result> { self.file.projects.iter().map(|p| p.annotate(&self.state)).collect() } @@ -252,6 +255,10 @@ impl ConfigFile { pub fn sizes(&self) -> &HashMap { &self.sizes } pub fn branch(&self) -> &Option { self.options.branch() } + pub fn hooks(&self) -> HashMap, &HookSet)> { + self.projects.iter().map(|p| (p.id().clone(), (p.root(), p.hooks()))).collect() + } + /// Check that IDs are unique, etc. fn validate(&self) -> Result<()> { let mut ids = HashSet::new(); @@ -321,9 +328,13 @@ pub struct Project { changelog: Option, #[serde(deserialize_with = "deserialize_version")] version: Location, + #[serde(default, deserialize_with = "deserialize_labels")] + labels: Vec, tag_prefix: Option, #[serde(default)] - subs: Option + subs: Option, + #[serde(default)] + hooks: HookSet } impl Project { @@ -331,6 +342,8 @@ impl Project { pub fn name(&self) -> &str { &self.name } pub fn depends(&self) -> &[ProjectId] { &self.depends } pub fn root(&self) -> Option<&String> { self.root.as_ref().and_then(|r| if r == "." { None } else { Some(r) }) } + pub fn has_label(&self, label: &str) -> bool { self.labels.iter().any(|l| l == label) } + pub fn hooks(&self) -> &HookSet { &self.hooks } fn annotate(&self, state: &S) -> Result { Ok(AnnotatedMark::new(self.id.clone(), self.name.clone(), self.get_value(state)?)) @@ -486,8 +499,10 @@ impl Project { depends: expand_depends(&self.depends, &sub), changelog: self.changelog.clone(), version: expand_version(&self.version, &sub), + labels: Default::default(), tag_prefix: self.tag_prefix.clone(), - subs: None + subs: None, + hooks: self.hooks.clone() }))) } else { Ok(E2::B(once(self))) @@ -528,6 +543,69 @@ impl Project { } } +#[derive(Clone, Debug)] +pub struct HookSet { + hooks: HashMap +} + +impl Default for HookSet { + fn default() -> HookSet { HookSet { hooks: Default::default() } } +} + +impl HookSet { + pub fn execute(&self, which: &str, root: &Option<&String>) -> Result<()> { + if let Some(hook) = self.hooks.get(which) { + hook.execute(root)?; + } + + Ok(()) + } + + pub fn execute_post_write(&self, root: &Option<&String>) -> Result<()> { self.execute("post_write", root) } +} + +impl<'de> Deserialize<'de> for HookSet { + fn deserialize>(desr: D) -> std::result::Result { + Ok(HookSet { hooks: Deserialize::deserialize(desr)? }) + } +} + +impl Serialize for HookSet { + fn serialize(&self, srlr: S) -> std::result::Result { self.hooks.serialize(srlr) } +} + +#[derive(Clone, Debug)] +pub struct Hook { + cmd: String +} + +impl Hook { + pub fn execute(&self, root: &Option<&String>) -> Result<()> { + use std::process::Command; + + let mut command = Command::new("bash"); + if let Some(root) = root { + command.current_dir(root); + } + let status = command.args(&["-e", "-c", &self.cmd]).status()?; + if !status.success() { + bail!("Unable to run hook {}.", self.cmd); + } else { + Ok(()) + } + } +} + +impl<'de> Deserialize<'de> for Hook { + fn deserialize>(desr: D) -> std::result::Result { + Ok(Hook { cmd: Deserialize::deserialize(desr)? }) + } +} + +impl Serialize for Hook { + fn serialize(&self, srlr: S) -> std::result::Result { self.cmd.serialize(srlr) } +} + fn expand_name(name: &str, sub: &SubExtent) -> String { match sub.dir() { Some(subdir) => format!("{}/{}", name, subdir), @@ -945,6 +1023,27 @@ fn deserialize_version<'de, D: Deserializer<'de>>(desr: D) -> std::result::Resul desr.deserialize_map(LocatorVisitor) } +fn deserialize_labels<'de, D: Deserializer<'de>>(desr: D) -> std::result::Result, D::Error> { + struct StringsVisitor; + type T = Vec; + + impl<'de> Visitor<'de> for StringsVisitor { + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string or list") } + + fn visit_str(self, v: &str) -> std::result::Result { Ok(vec![v.to_string()]) } + fn visit_borrowed_str(self, v: &'de str) -> std::result::Result { Ok(vec![v.to_string()]) } + fn visit_string(self, v: String) -> std::result::Result { Ok(vec![v]) } + + fn visit_seq>(self, seq: S) -> std::result::Result { + Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq)) + } + } + + desr.deserialize_any(StringsVisitor) +} + fn deserialize_sizes<'de, D: Deserializer<'de>>(desr: D) -> std::result::Result, D::Error> { struct MapVisitor; @@ -1286,6 +1385,8 @@ sizes: picker: Picker::Json(ScanningPicker::new(vec![Part::Map("version".into())])) }), tag_prefix: None, + labels: Default::default(), + hooks: Default::default(), subs: None }; @@ -1308,6 +1409,8 @@ sizes: picker: Picker::Json(ScanningPicker::new(vec![Part::Map("version".into())])) }), tag_prefix: None, + labels: Default::default(), + hooks: Default::default(), subs: None }; @@ -1329,6 +1432,8 @@ sizes: picker: Picker::Json(ScanningPicker::new(vec![Part::Map("version".into())])) }), tag_prefix: None, + labels: Default::default(), + hooks: Default::default(), subs: None }; diff --git a/src/init.rs b/src/init.rs index 655ed5f..4a23188 100644 --- a/src/init.rs +++ b/src/init.rs @@ -9,6 +9,8 @@ use error_chain::bail; use log::trace; use log::warn; use std::collections::HashSet; +use std::fs::OpenOptions; +use std::io::Write; use std::iter::once; use std::path::Path; @@ -22,9 +24,15 @@ pub fn init(max_depth: u16) -> Result<()> { println!("No projects found."); } write_yaml(&projs)?; + append_ignore()?; Ok(()) } +fn append_ignore() -> Result<()> { + let mut file = OpenOptions::new().create(true).append(true).open(".gitignore")?; + Ok(file.write_all(b"/.versio-paused\n")?) +} + fn find_projects(dir: &Path, depth: u16, max_depth: u16) -> Result> { trace!("Finding projects in {}", dir.to_string_lossy()); if depth > max_depth { @@ -68,12 +76,19 @@ fn find_projects_in(dir: &Path) -> impl Iterator> { if dir.join("package.json").exists() { let name = try_iter!(extract_name(dir, "package.json", |d| JsonScanner::new("name").find(&d))); - summaries.push(Ok(ProjSummary::new_file(name, dir.to_string_lossy(), "package.json", "json", "version"))); + summaries.push(Ok(ProjSummary::new_file(name, dir.to_string_lossy(), "package.json", "json", "version", &["npm"]))); } if dir.join("Cargo.toml").exists() { let name = try_iter!(extract_name(dir, "Cargo.toml", |d| TomlScanner::new("package.name").find(&d))); - summaries.push(Ok(ProjSummary::new_file(name, dir.to_string_lossy(), "Cargo.toml", "toml", "package.version"))); + summaries.push(Ok(ProjSummary::new_file( + name, + dir.to_string_lossy(), + "Cargo.toml", + "toml", + "package.version", + &["cargo"] + ))); } if dir.join("go.mod").exists() { @@ -85,31 +100,45 @@ fn find_projects_in(dir: &Path) -> impl Iterator> { } if !is_subdir { let name = dir.file_name().and_then(|n| n.to_str()).unwrap_or("project"); - summaries.push(Ok(ProjSummary::new_tags(name, dir.to_string_lossy(), true))); + summaries.push(Ok(ProjSummary::new_tags(name, dir.to_string_lossy(), true, &["go"]))); } } if dir.join("pom.xml").exists() { let name = try_iter!(extract_name(dir, "pom.xml", |d| XmlScanner::new("project.artifactId").find(&d))); - summaries.push(Ok(ProjSummary::new_file(name, dir.to_string_lossy(), "pom.xml", "xml", "project.version"))); + summaries.push(Ok(ProjSummary::new_file( + name, + dir.to_string_lossy(), + "pom.xml", + "xml", + "project.version", + &["mvn"] + ))); } if dir.join("setup.py").exists() { let name_reg = r#"name *= *['"]([^'"]*)['"]"#; let version_reg = r#"version *= *['"](\d+\.\d+\.\d+)['"]"#; let name = try_iter!(extract_name(dir, "setup.py", |d| find_reg_data(&d, &name_reg))); - summaries.push(Ok(ProjSummary::new_file(name, dir.to_string_lossy(), "setup.py", "pattern", version_reg))); + summaries.push(Ok(ProjSummary::new_file( + name, + dir.to_string_lossy(), + "setup.py", + "pattern", + version_reg, + &["pip"] + ))); } if try_iter!(dir.read_dir()) .filter_map(|e| e.ok().and_then(|e| e.file_name().into_string().ok())) .any(|n| n.ends_with("*.tf")) { - summaries.push(Ok(ProjSummary::new_tags("terraform", dir.to_string_lossy(), false))); + summaries.push(Ok(ProjSummary::new_tags("terraform", dir.to_string_lossy(), false, &["terraform"]))); } if dir.join("Dockerfile").exists() { - summaries.push(Ok(ProjSummary::new_tags("docker", dir.to_string_lossy(), false))); + summaries.push(Ok(ProjSummary::new_tags("docker", dir.to_string_lossy(), false, &["docker"]))); } try_iter!(add_gemspecs(dir, &mut summaries)); @@ -136,7 +165,14 @@ fn add_gemspecs(dir: &Path, summaries: &mut Vec>) -> Result< if Mark::new(vers.clone(), 0).validate_version().is_ok() { // Sometimes, the version is in the specfile. let version_reg = r#"spec\.version *= *['"](\d+\.\d+\.\d+)['"]"#; - summaries.push(Ok(ProjSummary::new_file(name, dir.to_string_lossy(), spec_file, "pattern", version_reg))); + summaries.push(Ok(ProjSummary::new_file( + name, + dir.to_string_lossy(), + spec_file, + "pattern", + version_reg, + &["gem"] + ))); } else if vers.ends_with("::VERSION") { // But other times, the version is in the gem itself i.e. 'MyGem::VERSION'. Search the standard place. let vers_file = Path::new("lib").join(&spec_file[.. spec_file.len() - spec_suffix.len()]).join("version.rb"); @@ -147,16 +183,24 @@ fn add_gemspecs(dir: &Path, summaries: &mut Vec>) -> Result< dir.to_string_lossy(), vers_file.to_string_lossy(), "pattern", - version_reg + version_reg, + &["gem"] ))); } else { warn!("Couldn't find VERSION file \"{}\". Please edit the .versio.yaml file.", vers_file.to_string_lossy()); - summaries.push(Ok(ProjSummary::new_file(name, dir.to_string_lossy(), "EDIT_ME", "pattern", "EDIT_ME"))); + summaries.push(Ok(ProjSummary::new_file( + name, + dir.to_string_lossy(), + "EDIT_ME", + "pattern", + "EDIT_ME", + &["gem"] + ))); } } else { // Still other times, it's too tough to find. warn!("Couldn't find version in \"{}\" from \"{}\". Please edit the .versio.yaml file.", spec_file, vers); - summaries.push(Ok(ProjSummary::new_file(name, dir.to_string_lossy(), "EDIT_ME", "pattern", "EDIT_ME"))); + summaries.push(Ok(ProjSummary::new_file(name, dir.to_string_lossy(), "EDIT_ME", "pattern", "EDIT_ME", &["gem"]))); } } @@ -186,6 +230,16 @@ fn generate_yaml(projs: &[ProjSummary]) -> String { } yaml.push_str(&format!(" id: {}\n", id + 1)); yaml.push_str(&format!(" tag_prefix: \"{}\"\n", proj.tag_prefix(projs.len(), &mut prefixes))); + if !proj.labels().is_empty() { + if proj.labels().len() == 1 { + yaml.push_str(&format!(" labels: {}\n", &proj.labels()[0])); + } else { + yaml.push_str(" labels:\n"); + for l in proj.labels() { + yaml.push_str(&format!(" - {}\n", l)); + } + } + } yaml.push_str(" version:\n"); proj.append_version(&mut yaml); if proj.subs() { @@ -203,6 +257,7 @@ fn generate_yaml(projs: &[ProjSummary]) -> String { struct ProjSummary { name: String, + labels: Vec, root: String, subs: bool, version: VersionSummary @@ -210,12 +265,14 @@ struct ProjSummary { impl ProjSummary { pub fn new_file( - name: impl ToString, root: impl ToString, file: impl ToString, file_type: impl ToString, parts: impl ToString + name: impl ToString, root: impl ToString, file: impl ToString, file_type: impl ToString, parts: impl ToString, + labels: &[impl ToString] ) -> ProjSummary { ProjSummary { name: name.to_string(), root: root.to_string(), subs: false, + labels: labels.iter().map(|s| s.to_string()).collect(), version: VersionSummary::File(FileVersionSummary::new( file.to_string(), file_type.to_string(), @@ -224,16 +281,18 @@ impl ProjSummary { } } - pub fn new_tags(name: impl ToString, root: impl ToString, subs: bool) -> ProjSummary { + pub fn new_tags(name: impl ToString, root: impl ToString, subs: bool, labels: &[impl ToString]) -> ProjSummary { ProjSummary { name: name.to_string(), root: root.to_string(), subs, + labels: labels.iter().map(|s| s.to_string()).collect(), version: VersionSummary::Tag(TagVersionSummary::new()) } } fn name(&self) -> &str { &self.name } + fn labels(&self) -> &[String] { &self.labels } fn root(&self) -> Option<&str> { if &self.root == "." { diff --git a/src/mark.rs b/src/mark.rs index ba20bdc..53c5685 100644 --- a/src/mark.rs +++ b/src/mark.rs @@ -5,12 +5,12 @@ use crate::scan::parts::{deserialize_parts, Part}; use crate::scan::{find_reg_data, scan_reg_data, JsonScanner, Scanner, TomlScanner, XmlScanner, YamlScanner}; use error_chain::bail; use regex::Regex; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::fmt; use std::marker::PhantomData; use std::path::{Path, PathBuf}; -#[derive(Clone, Deserialize, Debug)] +#[derive(Clone, Deserialize, Serialize, Debug)] #[serde(untagged)] pub enum Picker { Json(ScanningPicker), @@ -57,7 +57,7 @@ impl Picker { } } -#[derive(Deserialize)] +#[derive(Deserialize, Serialize)] pub struct ScanningPicker { #[serde(deserialize_with = "deserialize_parts")] parts: Vec, @@ -78,7 +78,7 @@ impl ScanningPicker { pub fn scan(&self, data: NamedData) -> Result { T::build(self.parts.clone()).scan(data) } } -#[derive(Clone, Deserialize, Debug)] +#[derive(Clone, Deserialize, Serialize, Debug)] pub struct LinePicker { pattern: String } @@ -96,7 +96,7 @@ impl LinePicker { pub fn scan(&self, data: NamedData) -> Result { scan_reg_data(data, &self.pattern) } } -#[derive(Clone, Deserialize, Debug)] +#[derive(Clone, Deserialize, Serialize, Debug)] pub struct FilePicker {} impl FilePicker { diff --git a/src/mono.rs b/src/mono.rs index 54cbb39..aac955d 100644 --- a/src/mono.rs +++ b/src/mono.rs @@ -6,7 +6,7 @@ use crate::either::{IterEither2 as E2, IterEither3 as E3}; use crate::errors::Result; use crate::git::{Auth, CommitInfoBuf, FromTag, FromTagBuf, FullPr, GithubInfo, Repo}; use crate::github::{changes, line_commits_head, Changes}; -use crate::state::{CurrentState, OldTags, PrevFiles, PrevTagMessage, StateRead, StateWrite}; +use crate::state::{CommitArgs, CurrentState, OldTags, PrevFiles, PrevTagMessage, StateRead, StateWrite}; use crate::vcs::VcsLevel; use chrono::{DateTime, FixedOffset}; use error_chain::bail; @@ -73,13 +73,17 @@ impl Mono { } } - pub fn commit(&mut self, advance_prev: bool) -> Result<()> { + pub fn commit(&mut self, advance_prev: bool, pause: bool) -> Result<()> { self.next.commit( &self.repo, - self.current.prev_tag(), - &self.last_commits, - &self.current.old_tags().current(), - advance_prev + CommitArgs::new( + self.current.prev_tag(), + &self.last_commits, + &self.current.old_tags().current(), + advance_prev, + &self.current.hooks(), + pause + ) ) } diff --git a/src/output.rs b/src/output.rs index d960f88..a367083 100644 --- a/src/output.rs +++ b/src/output.rs @@ -25,6 +25,7 @@ impl Output { pub fn changes(&self) -> ChangesOutput { ChangesOutput::new() } pub fn plan(&self) -> PlanOutput { PlanOutput::new() } pub fn release(&self) -> ReleaseOutput { ReleaseOutput::new() } + pub fn resume(&self) -> ResumeOutput { ResumeOutput::new() } } pub struct CheckOutput {} @@ -43,6 +44,22 @@ impl CheckOutput { } } +pub struct ResumeOutput {} + +impl Default for ResumeOutput { + fn default() -> ResumeOutput { ResumeOutput::new() } +} + +impl ResumeOutput { + pub fn new() -> ResumeOutput { ResumeOutput {} } + pub fn write_done(&mut self) -> Result<()> { Ok(()) } + + pub fn commit(&mut self) -> Result<()> { + println!("Release complete."); + Ok(()) + } +} + pub struct ProjOutput { wide: bool, vers_only: bool, @@ -408,6 +425,7 @@ impl ReleaseOutput { pub fn write_logged(&mut self, path: PathBuf) -> Result<()> { self.result.append_logged(path) } pub fn write_done(&mut self) -> Result<()> { self.result.append_done() } pub fn write_commit(&mut self) -> Result<()> { self.result.append_commit() } + pub fn write_pause(&mut self) -> Result<()> { self.result.append_pause() } pub fn write_dry(&mut self) -> Result<()> { self.result.append_dry() } pub fn write_changed(&mut self, name: String, prev: String, curt: String, targ: String) -> Result<()> { @@ -438,6 +456,7 @@ impl ReleaseResult { fn append_logged(&mut self, path: PathBuf) -> Result<()> { self.append(ReleaseEvent::Logged(path)) } fn append_done(&mut self) -> Result<()> { self.append(ReleaseEvent::Done) } fn append_commit(&mut self) -> Result<()> { self.append(ReleaseEvent::Commit) } + fn append_pause(&mut self) -> Result<()> { self.append(ReleaseEvent::Pause) } fn append_dry(&mut self) -> Result<()> { self.append(ReleaseEvent::Dry) } fn append_changed(&mut self, name: String, prev: String, curt: String, targ: String) -> Result<()> { @@ -505,6 +524,7 @@ enum ReleaseEvent { NoChange(bool, String, Option, String), New(bool, String, String), Commit, + Pause, Dry, Done } @@ -515,6 +535,7 @@ impl ReleaseEvent { ReleaseEvent::Logged(p) => println!("Wrote changelog at {}.", p.to_string_lossy()), ReleaseEvent::Done => println!("Release complete."), ReleaseEvent::Commit => println!("Changes committed."), + ReleaseEvent::Pause => println!("Paused for commit: use --resume to continue."), ReleaseEvent::Dry => println!("Dry run: no actual changes."), ReleaseEvent::Changed(name, prev, curt, targ) => { if prev == curt { diff --git a/src/scan/parts.rs b/src/scan/parts.rs index e3e3331..cd41c2d 100644 --- a/src/scan/parts.rs +++ b/src/scan/parts.rs @@ -1,6 +1,7 @@ //! Targets for finding a version in a file. use serde::de::{self, Deserialize, Deserializer, SeqAccess, Visitor}; +use serde::ser::{Serialize, Serializer}; use std::fmt; pub trait IntoPartVec { @@ -121,3 +122,12 @@ impl<'de> Deserialize<'de> for Part { desr.deserialize_any(PartVisitor) } } + +impl Serialize for Part { + fn serialize(&self, ser: S) -> std::result::Result { + match self { + Part::Seq(v) => v.serialize(ser), + Part::Map(v) => v.serialize(ser) + } + } +} diff --git a/src/state.rs b/src/state.rs index 2c80aa1..1b239a6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,6 +1,6 @@ //! The mechanisms used to read and write state, both current and historical. -use crate::config::ProjectId; +use crate::config::{HookSet, ProjectId}; use crate::errors::{Result, ResultExt as _}; use crate::git::{FromTagBuf, Repo, Slice}; use crate::mark::{NamedData, Picker}; @@ -8,6 +8,8 @@ use log::{trace, warn}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; +use std::fs::OpenOptions; +use std::mem::take; use std::path::{Path, PathBuf}; pub trait StateRead: FilesRead { @@ -130,6 +132,7 @@ impl OldTags { pub fn slice_to_prev(&self) -> Result { Ok(OldTags::new(self.prev.clone(), HashMap::new())) } } +#[derive(Deserialize, Serialize)] pub struct StateWrite { writes: Vec, proj_writes: HashSet, @@ -175,65 +178,122 @@ impl StateWrite { Ok(()) } - pub fn commit( - &mut self, repo: &Repo, prev_tag: &str, last_commits: &HashMap, - old_tags: &HashMap, advance_prev: bool - ) -> Result<()> { + pub fn commit(&mut self, repo: &Repo, data: CommitArgs) -> Result<()> { for write in &self.writes { write.write()?; } let did_write = !self.writes.is_empty(); self.writes.clear(); - if did_write { + for proj_id in &self.proj_writes { + if let Some((root, hooks)) = data.hooks.get(proj_id) { + hooks.execute_post_write(root)?; + } + } + + let me = take(self); + let prev_tag = data.prev_tag.to_string(); + let last_commits = data.last_commits.clone(); + let old_tags = data.old_tags.clone(); + let mut commit_state = CommitState::new(me, did_write, prev_tag, last_commits, old_tags, data.advance_prev); + + if data.pause { + let file = OpenOptions::new().create(true).write(true).truncate(true).open(".versio-paused")?; + Ok(serde_json::to_writer(file, &commit_state)?) + } else { + commit_state.resume(repo) + } + } +} + +pub struct CommitArgs<'a> { + prev_tag: &'a str, + last_commits: &'a HashMap, + old_tags: &'a HashMap, + advance_prev: bool, + hooks: &'a HashMap, &'a HookSet)>, + pause: bool +} + +impl<'a> CommitArgs<'a> { + pub fn new( + prev_tag: &'a str, last_commits: &'a HashMap, old_tags: &'a HashMap, + advance_prev: bool, hooks: &'a HashMap, &'a HookSet)>, pause: bool + ) -> CommitArgs<'a> { + CommitArgs { prev_tag, last_commits, old_tags, advance_prev, hooks, pause } + } +} + +fn fill_from_old(old: &HashMap, new_tags: &mut HashMap) -> Result<()> { + for (proj_id, tag) in old { + if !new_tags.contains_key(proj_id) { + new_tags.insert(proj_id.clone(), tag.clone()); + } + } + Ok(()) +} + +/// A command to commit, tag, and push everything +#[derive(Deserialize, Serialize)] +pub struct CommitState { + write: StateWrite, + did_write: bool, + prev_tag: String, + last_commits: HashMap, + old_tags: HashMap, + advance_prev: bool +} + +impl CommitState { + pub fn new( + write: StateWrite, did_write: bool, prev_tag: String, last_commits: HashMap, + old_tags: HashMap, advance_prev: bool + ) -> CommitState { + CommitState { write, did_write, prev_tag, last_commits, old_tags, advance_prev } + } + + pub fn resume(&mut self, repo: &Repo) -> Result<()> { + if self.did_write { trace!("Wrote files, so committing."); repo.commit()?; } else { trace!("No files written, so not committing."); } - for tag in &self.tag_head { + for tag in &self.write.tag_head { repo.update_tag_head(tag)?; } - self.tag_head.clear(); + self.write.tag_head.clear(); - for (tag, proj_id) in &self.tag_head_or_last { - if self.proj_writes.contains(&proj_id) { + for (tag, proj_id) in &self.write.tag_head_or_last { + if self.write.proj_writes.contains(&proj_id) { repo.update_tag_head(tag)?; - } else if let Some(oid) = last_commits.get(proj_id) { + } else if let Some(oid) = self.last_commits.get(proj_id) { repo.update_tag(tag, oid)?; } else { warn!("Latest commit for project {} unknown: tagging head.", proj_id); repo.update_tag_head(tag)?; } } - self.tag_head_or_last.clear(); - self.proj_writes.clear(); + self.write.tag_head_or_last.clear(); + self.write.proj_writes.clear(); - for (tag, oid) in &self.tag_commit { + for (tag, oid) in &self.write.tag_commit { repo.update_tag(tag, oid)?; } - self.tag_commit.clear(); + self.write.tag_commit.clear(); - if advance_prev { - fill_from_old(old_tags, &mut self.new_tags)?; - let msg = serde_json::to_string(&PrevTagMessage::new(std::mem::replace(&mut self.new_tags, HashMap::new())))?; - repo.update_tag_head_anno(prev_tag, &msg)?; + if self.advance_prev { + fill_from_old(&self.old_tags, &mut self.write.new_tags)?; + let msg = + serde_json::to_string(&PrevTagMessage::new(std::mem::replace(&mut self.write.new_tags, HashMap::new())))?; + repo.update_tag_head_anno(&self.prev_tag, &msg)?; } Ok(()) } } -fn fill_from_old(old: &HashMap, new_tags: &mut HashMap) -> Result<()> { - for (proj_id, tag) in old { - if !new_tags.contains_key(proj_id) { - new_tags.insert(proj_id.clone(), tag.clone()); - } - } - Ok(()) -} - #[derive(Deserialize, Serialize)] pub struct PrevTagMessage { versions: HashMap @@ -248,6 +308,7 @@ impl PrevTagMessage { pub fn into_versions(self) -> HashMap { self.versions } } +#[derive(Deserialize, Serialize)] enum FileWrite { Write { path: PathBuf, val: String }, Update { pick: PickPath, val: String } @@ -268,6 +329,7 @@ impl FileWrite { } } +#[derive(Deserialize, Serialize)] pub struct PickPath { file: PathBuf, picker: Picker