From ca413cb6dd6d4b1e8b3d9305173d0c4d549b8645 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Fri, 4 Jun 2021 15:08:14 +0200 Subject: [PATCH] Introduce ormolu CI. --- .github/workflows/ormolu.yaml | 51 +++++++++++++ layout/ormolu.sh | 92 +++++++++++++++++++++++ layout/ormolu.version | 1 + layout/rebase-onto-formatter.sh | 125 ++++++++++++++++++++++++++++++++ 4 files changed, 269 insertions(+) create mode 100644 .github/workflows/ormolu.yaml create mode 100755 layout/ormolu.sh create mode 100644 layout/ormolu.version create mode 100644 layout/rebase-onto-formatter.sh diff --git a/.github/workflows/ormolu.yaml b/.github/workflows/ormolu.yaml new file mode 100644 index 0000000..f5f1421 --- /dev/null +++ b/.github/workflows/ormolu.yaml @@ -0,0 +1,51 @@ +# FUTUREWORK: add this to `ci.dhall`? + +name: Ormolu +on: + - pull_request + +jobs: + ormolu: + runs-on: ubuntu-18.04 + steps: + - uses: "actions/checkout@v1" + + - uses: "actions/setup-haskell@v1.1.4" + id: setup-haskell-cabal + with: + cabal-version: "${{ matrix.cabal }}" + enable-stack: false + ghc-version: "${{ matrix.ghc }}" + + - uses: "actions/cache@v2" + name: Cache + with: + key: "${{ runner.os }}" + path: | + "${{ steps.setup-haskell-cabal.outputs.cabal-store }}" + ~/.cabal/packages + ~/.cabal/store + ~/.cabal/bin + dist-newstyle + ~/.local/bin + + - name: Install dependencies + run: | + export PATH=$PATH:$HOME/.cabal/bin:$HOME/.local/bin + export ORMOLU_VERSION=$(cat ./layout/ormolu.version) + (ormolu -v 2>/dev/null | grep -q $ORMOLU_VERSION) || (cabal update && cabal install ormolu --constraint="ormolu ==$ORMOLU_VERSION") + test -e $HOME/.local/bin/yq || pip3 install yq + shell: bash + + - name: Ormolu + run: | + export PATH=$PATH:$HOME/.cabal/bin:$HOME/.local/bin + ./layout/ormolu.sh -c + shell: bash + + strategy: + matrix: + cabal: + - '3.2' + ghc: + - '8.10.1' diff --git a/layout/ormolu.sh b/layout/ormolu.sh new file mode 100755 index 0000000..ed19151 --- /dev/null +++ b/layout/ormolu.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +set -e + +cd "$( dirname "${BASH_SOURCE[0]}" )" + +command -v grep >/dev/null 2>&1 || { echo >&2 "grep is not installed, aborting."; exit 1; } +command -v awk >/dev/null 2>&1 || { echo >&2 "awk is not installed, aborting."; exit 1; } +command -v sed >/dev/null 2>&1 || { echo >&2 "sed is not installed, aborting."; exit 1; } +command -v yq >/dev/null 2>&1 || { echo >&2 "yq is not installed, aborting. See https://github.com/mikefarah/yq"; exit 1; } + +ORMOLU_VERSION=$(cat ormolu.version) +( ormolu -v 2>/dev/null | grep -q $ORMOLU_VERSION ) || ( echo "please install ormolu $ORMOLU_VERSION (eg., run 'cabal install ormolu' and ensure ormolu is on your PATH.)"; exit 1 ) +echo "ormolu version: $ORMOLU_VERSION" + +ARG_ALLOW_DIRTY_WC="0" +ARG_ORMOLU_MODE="inplace" + +USAGE=" +This bash script can either (a) apply ormolu formatting in-place to +all haskell modules in your working copy, or (b) check all modules for +formatting and fail if ormolu needs to be applied. + +(a) is mostly for migrating from manually-formatted projects to +ormolu-formatted ones; (b) can be run in by a continuous integration +service to make sure no branches with non-ormolu formatting make get +merged. + +For every-day dev work, consider using one of the ormolu editor +integrations (see https://github.com/tweag/ormolu#editor-integration). + +USAGE: $0 + -h: show this help. + -f: run even if working copy is dirty. default: ${ARG_ALLOW_DIRTY_WC} + -c: set ormolu mode to 'check'. default: 'inplace' + +" + +# Option parsing: +# https://sookocheff.com/post/bash/parsing-bash-script-arguments-with-shopts/ +while getopts ":fch" opt; do + case ${opt} in + f ) ARG_ALLOW_DIRTY_WC="1" + ;; + c ) ARG_ORMOLU_MODE="check" + ;; + h ) echo "$USAGE" 1>&2 + exit 0 + ;; + esac +done +shift $((OPTIND -1)) + +if [ "$#" -ne 0 ]; then + echo "$USAGE" 1>&2 + exit 1 +fi + +cd ".." + +if [ "$(git status -s | grep -v \?\?)" != "" ]; then + echo "working copy not clean." + if [ "$ARG_ALLOW_DIRTY_WC" == "1" ]; then + echo "running with -f. this will mix ormolu and other changes." + else + echo "run with -f if you want to force mixing ormolu and other changes." + exit 1 + fi +fi + +echo "ormolu mode: $ARG_ORMOLU_MODE" + +FAILURES=0 + +for hsfile in $(git ls-files | grep '\.hsc\?$'); do + FAILED=0 + ormolu --mode $ARG_ORMOLU_MODE --check-idempotence $LANGUAGE_EXTS "$hsfile" || FAILED=1 + if [ "$FAILED" == "1" ]; then + ((++FAILURES)) + echo "$hsfile... *** FAILED" + else + echo "$hsfile... ok" + fi +done + +if [ "$FAILURES" != 0 ]; then + echo "ormolu failed on $FAILURES files." + if [ "$ARG_ORMOLU_MODE" == "check" ]; then + echo -en "\n\nyou can fix this by running 'make format' from the git repo root.\n\n" + fi + exit 1 +fi diff --git a/layout/ormolu.version b/layout/ormolu.version new file mode 100644 index 0000000..473b31b --- /dev/null +++ b/layout/ormolu.version @@ -0,0 +1 @@ +0.1.4.1 diff --git a/layout/rebase-onto-formatter.sh b/layout/rebase-onto-formatter.sh new file mode 100644 index 0000000..0de82b6 --- /dev/null +++ b/layout/rebase-onto-formatter.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +# written by mheinzel + +set -euo pipefail + +command -v sed >/dev/null 2>&1 || { echo >&2 "sed is not installed, aborting."; exit 1; } + +BASE_COMMIT=${1:-} +TARGET_COMMIT=${2:-} +FORMATTING_COMMAND='make formatf' +USAGE=" +USAGE: $0 BASE_COMMIT TARGET_COMMIT + + BASE_COMMIT: + A commit that contains the changes to formatting version and + config already from TARGET_COMMIT, but not the automatically + applied formatting changes. Must be the first commit on the + branch you are about to rebase (not the one returned by + git-merge-base). It will be removed from the resulting branch. + TARGET_COMMIT: + The commit introducing the formatting that you want to rebase onto. + +Rebase a branch onto changes created by an automated formatter. The script +will keep the (linear) history of the branch intact and make the commits appear +as if the changes had been applied onto the newly-formatted version all along. + +INSTRUCTIONS: +1. Make a copy of your branch (or be prepared to salvage it from reflog). + $ git branch mybranch-backup +2. Find out what the base commit is. +3. Rebase onto the base commit yourself. + $ git rebase \$BASE_COMMIT +4. Make sure the formatting tool is installed with the correct version and settings. + $ stack install ormolu +5. Run this script. + $ $0 \$BASE_COMMIT \$TARGET_COMMIT + +" + +if [ -z "$BASE_COMMIT" ] || [ -z "$TARGET_COMMIT" ] || [ -z "$FORMATTING_COMMAND" ] +then + echo "$USAGE" 1>&2 + exit 1 +fi + +echo "Running the script now. This might take a while..." + +# The general idea is the following: +# +# We have a branch consisting of commits C1, C2, ... on top of our BASE_COMMIT C0. +# Also, from C0 an automated formatting change f was made on some branch (e.g. develop). +# +# C0 ----> C1 ----> C2 ----> ... ----> Cn +# | +# f +# | +# v +# C0' +# +# Now, how do we obtain versions of our commits operating on the formatted code (let's call them Ci')? +# +# C0 ----> C1 ----> C2 ----> ... ----> Cn +# | +# f +# | +# v +# C0' ---> C1' ---> C2' ---> ... ----> Cn' +# +# One useful thing is that since f is defined by an automated tool, +# we know f applied at every commit Ci, resulting in a hypothetical Ci'. +# +# C0 ----> C1 ----> C2 ----> ... ----> Cn +# | | | | +# f f f f +# | | | | +# v v v v +# C0' C1' C2' Cn' +# +# And we can also get its inverse g (applied at Ci') by reverting the commit. +# +# C0 ----> C1 ----> C2 ----> ... ----> Cn +# |^ |^ |^ |^ +# f| f| f| f| +# |g |g |g |g +# v| v| v| v| +# C0' C1' C2' Cn' +# +# Finally, we can get from C(i-1)' to Ci' by composing three arrows: +# - g at C(i-1) +# - Ci +# - f at C1 +# +# C0 ----> C1 ----> C2 ----> ... ----> Cn +# |^ |^ |^ |^ +# f| f| f| f| +# |g |g |g |g +# v| v| v| v| +# C0' ---> C1' ---> C2' ---> ... ----> Cn' + +set -x + +# edit every commit Ci, adding new commits representing f at Ci and it's inverse g +git rebase $BASE_COMMIT~1 --exec "$FORMATTING_COMMAND && git commit -am format && git revert HEAD --no-edit" + +# drop last commit (do not revert formatting at the end of the branch) +git reset HEAD~1 --hard + +# now for every Ci, squash with the previous and next commit (i.e. g at C(i-1) and f at Ci). +# However, we want to use Ci's commit message and author. +# To do this, we run the following command after each group of these 3 commits: +# Ci=$(git rev-parse HEAD~1); git reset --soft HEAD~3; git commit --reuse-message $Ci +# We do an interactive rebase, but instead of editing the commit sequence manually, +# we use sed for that, inserting an `exec` command after every 3 commits. +GIT_SEQUENCE_EDITOR='sed -i -e "4~3s/^\(pick \S* format\)$/\1\nexec Ci=\$(git rev-parse HEAD~1); git reset --soft HEAD~3; git commit --reuse-message \$Ci/"' \ + git rebase --interactive $BASE_COMMIT + +# rebase onto TARGET_COMMIT. +# Annoyingly, we still have this first "format" commit that should already be +# part of the TARGET_COMMIT. So we drop it. +GIT_SEQUENCE_EDITOR='sed -i "1s/pick/drop/"' \ + git rebase --interactive $BASE_COMMIT --onto $TARGET_COMMIT + +echo "Done." +echo "Please check that the history looks as it should and all expected commits are there."